Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quarkus Reactive Mutiny - Composite Exception encapsulation on Runtime Exceptions and @ExceptionHandler #11557

Closed
harry9111985 opened this issue Aug 24, 2020 · 22 comments
Labels
area/mutiny area/smallrye kind/question Further information is requested

Comments

@harry9111985
Copy link

harry9111985 commented Aug 24, 2020

Description

I am evaluating quarkus 1.6 and reactive Mutiny for reactive routes. My experience with quarkus mutiny exception handling is not so great . The current problem I have is if I throw a RuntimeException down in the service layer or data layer . The exception gets encapsulated as a CompositeException with a list of causes where one of the causes is the the RuntimeException thrown. This means that I have to always Parse the CompositeException to retrieve the actual exception which I have thrown to determine the HTTP status response ( of whether to throw a 500 or 422 or something else) .

Code Snippet - Current :

return Uni.createFrom().item(getRequest).flatMap(request ->{
  log.info("Request Received successfully {}", kv("someRequest", request));
  return xyzService.get(request, transactionContext);
}).onItemOrFailure().apply((paList, throwable) ->{
  if (paList != null) {
    return Response.ok(somePOJO).build();
  }

  if (throwable != null) {
    if (throwable instanceof CompositeException) {
      CompositeException compositeException = (CompositeException) throwable;
      Optional < Throwable > exception = isExceptionInstanceOf(compositeException, XYZRuntimeException.class);
      if (exception.isPresent()) {
        return Response.serverError().status(500).entity(somePOJO).build();
      }
    }

    if (throwable instanceof CircuitBreakerOpenException) {
      return Response.serverError().status(500).entity(somePOJO).build();

    }
  }

  return Response.serverError().status(500).entity(somePOJO).build();

});

Code Snippet - Expected :


//Controller logic 
return Uni.createFrom().item(getRequest)
                .flatMap(request -> {
                    log.info("Request Received successfully {}", kv("someRequest", request));
                    return xyzService.get(request, transactionContext);
                })
                .onItem()
                .apply(paList -> {
                    if (paList != null) {
                        return Response.ok(somePOJO)
                                .build();
                    }

                   return Response.serverError()
                            .status(500)
                            .entity(somePOJO)
                            .build();

                });

//Exception Handler can be in the same class or a different class ( different class will be annotated as @ControllerAdvice)
@ExceptionHandler
public Uni<Response> handleXYZException(XYZRuntimeException exception, <<FrameworkContextObject>> obj){
   
                                return Response.serverError()
                                        .status(500)
                                        .entity(somePOJO)
                            .build();
     
}

Implementation ideas

Can we have something similar to spring webflux , where if a RuntimeException is thrown from the data layer

a) Exactly same exception is captured in the error block of the controller class without needing to parse an encapsulated exception .

b) The Spring Boot framework has a @ControllerAdvice and @ExceptionHandler annotations to cleanly segregating the class for handling exceptions and controller. Can the same be available for Quarkus Reactive Mutiny extension.

c) The Spring boot framework has ServerWebExchange object which we can use as a context object to pass custom context . The good thing of this serverwebExchange object is that it is injected automatically as part of @ExceptionHandler Method making it easy to pass Context objects to exception handling methods . Can we have something similar in quarkus / reactive mutiny ?

(If you have any implementation ideas, they can go here, however please note that all design change proposals should be posted to the Quarkus developer mailing list (or the corresponding Google Group; see the decisions process document for more information).

@harry9111985 harry9111985 added the kind/enhancement New feature or request label Aug 24, 2020
@quarkusbot
Copy link

/cc @cescoffier

@cescoffier
Copy link
Member

Cc @jponge

@jponge
Copy link
Member

jponge commented Aug 24, 2020

Would you mind reformatting the issue? The code is not always in Markdown code blocks, and not indented which makes it hard to read.

@harry9111985
Copy link
Author

Done @jponge , is it better now ?

@jponge
Copy link
Member

jponge commented Aug 25, 2020

Better, although you could have syntax highlighting with ```java fenced blocks 😉

I'll have a look at the issue

@jponge
Copy link
Member

jponge commented Aug 25, 2020

On having something similar to @ExceptionHandler: this is not related to Mutiny but to Quarkus, that should could be a suggestion in another issue. I'm however not sure that this is a great match with a reactive programming model, but it can be debated somewhere else.

On using onItemOrFailure: in your case you get a handler method that has to deal with both a result or a failure, and do some conditional branching logic. I'd suggest you move the whole code to a separate method and lambda reference to keep your Mutiny pipeline readable. You could also use onItem().(...) and onFailure().... to cleanly split the result and error handling.

On exceptions being composite: this is expected when multiple failures need to be reported.

@harry9111985
Copy link
Author

@jponge : if i am throwing one xyz runtime exception from down my chain why should it be wrapped as a CompositeException... can it not be simply thrown up the reactive chain as xyz runtimeException. ?

I will raise a separate issue for @ExceptionHandler. I am coming from a context that if i have multiple APIs need same exceptions handled , it need not be code in the controller reactive chain but should be in a separate method annotated.

@cescoffier
Copy link
Member

if i am throwing one xyz runtime exception from down my chain why should it be wrapped as a CompositeException... can it not be simply thrown up the reactive chain as xyz runtimeException. ?

CompositeExceptions are only created if your first failure (the RuntimeException) triggers a second failure. So, you have at least 2 failures.

Another situation where CompositeException is used is when you combine multiple actions and explicitly require to collect all the failures. In this case, if there is more than one a CompositeException is used. Again, you have multiple failures.

Note that you should be able to retrieve the initial "main" failure using getCause().

@cescoffier
Copy link
Member

About ExceptionHandler, you can register a reactive route handling failure (set the type attribute to failure).

@harry9111985
Copy link
Author

if i am throwing one xyz runtime exception from down my chain why should it be wrapped as a CompositeException... can it not be simply thrown up the reactive chain as xyz runtimeException. ?

CompositeExceptions are only created if your first failure (the RuntimeException) triggers a second failure. So, you have at least 2 failures.

Another situation where CompositeException is used is when you combine multiple actions and explicitly require to collect all the failures. In this case, if there is more than one a CompositeException is used. Again, you have multiple failures.

Note that you should be able to retrieve the initial "main" failure using getCause().

I am only throwing a single XYZ Runtime Exception and no other failure is triggered. When the exception lands in the Controller its a Composite Exception with one cause which I think is an unnecessary thing to parse exceptions especially if its just one exception thrown.

@harry9111985
Copy link
Author

About ExceptionHandler, you can register a reactive route handling failure (set the type attribute to failure).

Could you help me out with a sample documentation on this please ? Would like to see how its done.

@cescoffier
Copy link
Member

I am only throwing a single XYZ Runtime Exception and no other failure is triggered. When the exception lands in the Controller its a Composite Exception with one cause which I think is an unnecessary thing to parse exceptions especially if its just one exception is thrown.

Can you provide the code? What do you call controller?

But, as an example, if you use onItemOfFailure().transform((res, fail) -> { ...}), and that function is called with a failure (fail != null) and the function throwns an exception, you end up with a CompositeException with fail as primary exception and the other thrown exception.

Could you help me out with a sample documentation on this please ? Would like to see how its done.

Check the following example:

https://github.com/quarkusio/quarkus-quickstarts/blob/d7d286a36409bbfe6475ba12bf9d929d5db00580/hibernate-reactive-routes-quickstart/src/main/java/org/acme/hibernate/reactive/FruitsRoutes.java#L92-L111

In this example, the route checks for the exception type and build the expected response (404, 402...).
You could even imagine having multiple failure handlers if they called next if they don't handle the failure. However, most of the time, there is only one, so everything is in a single place.

@harry9111985
Copy link
Author

harry9111985 commented Aug 25, 2020

I am only throwing a single XYZ Runtime Exception and no other failure is triggered. When the exception lands in the Controller its a Composite Exception with one cause which I think is an unnecessary thing to parse exceptions especially if its just one exception is thrown.

Can you provide the code? What do you call controller?

But, as an example, if you use onItemOfFailure().transform((res, fail) -> { ...}), and that function is called with a failure (fail != null) and the function throwns an exception, you end up with a CompositeException with fail as primary exception and the other thrown exception.

Could you help me out with a sample documentation on this please ? Would like to see how its done.

Check the following example:

https://github.com/quarkusio/quarkus-quickstarts/blob/d7d286a36409bbfe6475ba12bf9d929d5db00580/hibernate-reactive-routes-quickstart/src/main/java/org/acme/hibernate/reactive/FruitsRoutes.java#L92-L111

In this example, the route checks for the exception type and build the expected response (404, 402...).
You could even imagine having multiple failure handlers if they called next if they don't handle the failure. However, most of the time, there is only one, so everything is in a single place.

@cescoffier . Thanks for ur help and inputs .Yes I can provide code. And if you can help me out in correcting it so that I can get only one RuntimeException that would be useful.

The controller code remains the same as presented earlier. The controller calls the service which pretty much calls the DAO layer. The service code is just a delegate at the moment and has no exception handling there.

@Override
@Timeout(100)
@CircuitBreaker(requestVolumeThreshold = 5, delay = 1000, failOn = {
  PAInternalServerException.class,
  CompositeException.class,
  RuntimeException.class
})
public Uni < String > create( << SomePOJO >> somePOJO) {

  //Setting POJO to first insert query
  .....

  //Setting POJO to second insert query
  ........

  return SqlClientHelper.inTransactionUni(mySQLPool, tx ->{
    Uni < RowSet < Row >> insertQueryOne = tx.query( << insert query one >> ).execute();
    Uni < RowSet < Row >> insertQueryTwo = tx.query( << insert query two >> ).execute();

    return insertQueryOne.and(insertQueryTwo).onItem().ignore().andContinueWithNull();

  }).map(response -> <<some string>> )
    .onItemOrFailure()
    .apply((id, throwable) ->{
             if (id != null) {
                  return id;
             }

            if (throwable != null) {
               // this is the exception thrown which i am interested in catching it directly at the controller.
              throw new PAInternalServerException("Something wrong happened while inserting data");
           }

           return "";
    });
}

For the routes example , it looks better than what I have coded. There is now a common block which I can use as an interceptor to create HTTPResponse for exceptions using RoutingContext object.

Couple of things :

a) Is there a feature enhancement in pipeline that there would be support for exception based methods instead one common error method so that we wouldnt need to do unneccessary instanceofs like from line 99 to line 105 in your code snippet?

Example annotation enhancement:

@Route(path = "/*", type = FAILURE , exception = {XYZRuntimeException.class,IIegalArgumentException.class})
public void handleExceptionTypeX(RoutingContext context) {}

@Route(path = "/*", type = FAILURE , exception = {DEFRuntimeException.class})
public void handleExceptionTypeY(RoutingContext context) {}

b) When I tried using the error method .. the exception returned in context.failure() was a CompositeException with the cause being the original RollbackException thrown by reactive database framework instead of my XYZRuntimeException . The XYZRuntimeException thrown was part of SuppressedExceptions list. I was expecting either CompositeException and the cause of the CompositeException to be my XYZRuntimeException or the exception in the context.failure() to be XYZRuntimeException itself. could u help throw light on that please ? .

@cescoffier
Copy link
Member

Thanks for the code, which explain why you get a Composite exception. In onItemOfFailure().apply, throwing an exception creates a composite exception when invoked with a failure.

Here is the code from Mutiny:

@Override
public void onFailure(Throwable failure) {
    if (!subscriber.isCancelledOrDone()) {
        O outcome;
        try {
            outcome = mapper.apply(null, failure);
            // We cannot call onItem here, as if onItem would throw an exception
            // it would be caught and onFailure would be called. This would be illegal.
        } catch (Throwable e) { // NOSONAR
            // Be sure to not call the mapper again with the failure.
            subscriber.onFailure(new CompositeException(failure, e)); // <--- That's where the CompositeException gets created.
            return;
        }

        subscriber.onItem(outcome);
    }
}

The CompositeException is created using {failure, e} where failure is the RollbackException and e the PAInternalServerException. That explains the behavior you are seeing.

So, I would rewrite your code as follows:

@Override
@Timeout(100)
@CircuitBreaker(requestVolumeThreshold = 5, delay = 1000, failOn = {
  PAInternalServerException.class,
  CompositeException.class,
  RuntimeException.class
})
public Uni < String > create( << SomePOJO >> somePOJO) {

  //Setting POJO to first insert query
  .....

  //Setting POJO to second insert query
  ........

  return SqlClientHelper.inTransactionUni(mySQLPool, tx ->{
    Uni < RowSet < Row >> insertQueryOne = tx.query( << insert query one >> ).execute();
    Uni < RowSet < Row >> insertQueryTwo = tx.query( << insert query two >> ).execute();

    // I've change it to do the 2 insertions concurrently as they are not dependent
    return Uni.combine().all().unis(insertQueryOne, insertQueryTwo).asTuple() 
        .map(x -> null);

  }).map(response -> <<some string>> ) // Here response is always `null`, it is expected?
    .onFailure().apply((failure) -> {        
              return new PAInternalServerException("Something wrong happened while inserting data");
    }
}

Instead of using onItemOrFailure() and throwing an exception, I've used onFailure().apply transforming the failure.

About having the list of exceptions in failure handlers, that's a good idea. I will let @mkouba comment on this.

@harry9111985
Copy link
Author

Thanks @cescoffier . that works .

@mkouba : Is there a plan on adding this feature for exception type based method resolvers in the next 1,2 or 3 month roadmap for vertx-io web ?

@Route(path = "/*", type = FAILURE , exception = {XYZRuntimeException.class,IIegalArgumentException.class})
public void handleExceptionTypeX(RoutingContext context) {}
@Route(path = "/*", type = FAILURE , exception = {DEFRuntimeException.class})
public void handleExceptionTypeY(RoutingContext context) {}

@mkouba
Copy link
Contributor

mkouba commented Aug 27, 2020

@harry9111985 I like your proposal. We could even consider something like:

@Route(path = "/*", type = FAILURE)
void handleExceptionTypeY(DEFRuntimeException exception, RoutingContext context) {}

I will create a separate issue. We could target Quarkus 1.9.

@harry9111985
Copy link
Author

@mkouba : that works for me too ... thanks for putting it in the enhancement roadmap.. the code will get more cleaner :-)

@harry9111985
Copy link
Author

harry9111985 commented Sep 22, 2020

Hey @mkouba .. is this feature going to be part of the Quarkus 1.9 milestone ? I dont see 1.9 milestone attached for the same .

@mkouba
Copy link
Contributor

mkouba commented Sep 22, 2020

@harry9111985 yes, I forgot to create the issue: #12249

@harry9111985
Copy link
Author

harry9111985 commented Nov 23, 2020

Works well @mkouba . Quick question before I close the issue . Does this segregation of exception blocks work for non-reactive Vertx Web Routes or this capability is only implemented for Reactive Mutiny Vertx Routes ?

@mkouba
Copy link
Contributor

mkouba commented Nov 23, 2020

Does this segregation of exception blocks work for non-reactive Vertx Web Routes or this capability is only implemented for Reactive Mutiny Vertx Routes ?

I'm not quite sure I understand your question but a failure handler declared via @Route is used to handle any failure from a matching route (path, HTTP method, etc.), no matter whether it's using Mutiny type or not...

@harry9111985
Copy link
Author

Does this segregation of exception blocks work for non-reactive Vertx Web Routes or this capability is only implemented for Reactive Mutiny Vertx Routes ?

I'm not quite sure I understand your question but a failure handler declared via @Route is used to handle any failure from a matching route (path, HTTP method, etc.), no matter whether it's using Mutiny type or not...

Thanks . Thats what I was looking for . You answered my question . I am closing this issue .

@gsmet gsmet added kind/question Further information is requested and removed kind/enhancement New feature or request labels Nov 25, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/mutiny area/smallrye kind/question Further information is requested
Projects
None yet
Development

No branches or pull requests

6 participants