0

I'm using Spring integration and scatter gather pattern in my project. Here I've applied the Rest Template timeout and also I'm using ExpressionEvaluatingRequestHandlerAdvice() to catch timeout exception. But I want to catch that exception in failure flow and want to throw my own custom exception and I have my own exception handler in place to handle that exception so that I can show proper error message to the user. But here the exception is being thrown but my custom exception handler is not able to handle that exception so user is not getting my custom msg back.

//configuration

 @Bean
  public IntegrationFlow mainFlow() {
    return flow ->
        flow.split()
            .channel(c -> c.executor(Executors.newCachedThreadPool()))
            .scatterGather(
                scatterer ->
                    scatterer
                        .applySequence(true)
                        .recipientFlow(flow1())
                        .recipientFlow(flow2()),
                gatherer ->
                    gatherer
                        .releaseLockBeforeSend(true)
                        .releaseStrategy(group -> group.size() == 1))
            .aggregate()
            .to(anotherFlow());
  }

@Bean
  public IntegrationFlow flow2() {
    return flow -> {
        flow.channel(c -> c.executor(Executors.newCachedThreadPool()))
            .handle(
                Http.outboundGateway(
                        "http://localhost:4444/test", dummyService.restTemplate())
                    .httpMethod(HttpMethod.POST)
                    .expectedResponseType(String.class),
                c -> c.advice(expressionAdvice()));
    };
  }

 @Bean
  public Advice expressionAdvice() {
    ExpressionEvaluatingRequestHandlerAdvice advice =
        new ExpressionEvaluatingRequestHandlerAdvice();
    advice.setSuccessChannelName("success.input");
    advice.setOnSuccessExpressionString("payload + ' was successful'");
    advice.setFailureChannelName("failure.input");
    advice.setOnFailureExpressionString("'Failed ' + #exception.cause.message");
    advice.setReturnFailureExpressionResult(true);
    advice.setTrapException(true);
    return advice;
  }

  @Bean
  public IntegrationFlow success() {
    return f -> f.handle(System.out::println);
  }

  @Bean
  public IntegrationFlow failure() {
    return f ->
        f.handle(
                (p, h) -> {
                  if (p.toString().contains("Read timed out"))
                    throw new MyCustomException(ErrorCode.TIMEOUT_ERROR.getErrorData());
                  else throw new MyCustomException(ErrorCode.SERVICE_ERROR.getErrorData());
                });
  }

//DummyService class

@Configuration
public class DummyService {

  private final int TIMEOUT = (int) TimeUnit.SECONDS.toMillis(6);

  @Bean
  public RestTemplate restTemplate()
       {

    HttpComponentsClientHttpRequestFactory requestFactory =
        new HttpComponentsClientHttpRequestFactory();

    requestFactory.setHttpClient(httpClient);
    requestFactory.setConnectTimeout(TIMEOUT);
    requestFactory.setReadTimeout(TIMEOUT);
    RestTemplate restTemplate = new RestTemplate(requestFactory);
    return restTemplate;
  }
}

Here I'm trying to throw new exception in failure() flow but exception is being thrown properly but my custom exception handler framework is not able to catch that exception. In all other cases it's able to catch but inside the spring integration configuration class it's not working.

  • You don't show what is your "custom exception handler framework". Instead you show not related Scatter-Gather configuration. Please, learn how to ask here on Stack Overflow properly: https://stackoverflow.com/help/how-to-ask. – Artem Bilan Aug 01 '22 at 15:35
  • Sorry if I asked stupid question. I showed the configuration because I was seeing that in all other cases my exception handler can handle any exception but inside scatter-gather it's not able to do that. So I thought there could be some changes that I need to make to the scatter-gather pattern. – Somnath Mukherjee Aug 01 '22 at 15:41
  • Please, share your error handler anyway and how it is connected to that your scatter-gather configuration to have the whole picture in the question. – Artem Bilan Aug 01 '22 at 16:20

1 Answers1

1

When you do an async hand-off like with your flow.channel(c -> c.executor(Executors.newCachedThreadPool())), you cannot re-throw exceptions from that thread: the control is lost. You can have an errorChannel header populated for this flow, so an async ErrorHandler on the task executor is going to publish the ErrorMessage properly to that channel for some logic. From there you can return so-called compensation message for waiting gateway - gatherer in your case. What is very important is to preserve request headers. Otherwise it won't be able to correlated request with reply.

UPDATE

Your understanding of async error handling is a bit not correct.

The ExecutorChannel wraps a provided Executor into an ErrorHandlingTaskExecutor, which has the logic like this:

public void execute(final Runnable task) {
    this.executor.execute(() -> {
        try {
            task.run();
        }
        catch (Throwable t) { //NOSONAR
            ErrorHandlingTaskExecutor.this.errorHandler.handleError(t);
        }
    });
}

So, as you see that errorHandler is called inside the task, where re-throwing an exception from there will just end-user in void, according to your ThreadPoolExecutor:

protected void afterExecute(Runnable r, Throwable t) { }

That's why we talk about a compensation message instead of re-throwing some custom exception. That compensation message, though, could be a new ErrorMessage, but it has to preserve headers from request. something like this:

    @ServiceActivator(inputChannel = "scatterGatherErrorChannel")
    public Message<?> processAsyncScatterError(MessagingException payload) {
        return MessageBuilder.withPayload(payload.getCause())
                .copyHeaders(payload.getFailedMessage().getHeaders())
                .build();
    }

This message is going to be returned to the scatter-gather as a reply. Probably you are not interested in this, so you can bypass a reply for scatter-gather and propagate your custom error directly to the origin gateway's error channel. Something like this:

    @ServiceActivator(inputChannel = "scatterGatherErrorChannel")
    public Message<?> processAsyncScatterError(MessagingException payload) {
        MessageHeaders requestHeaders = payload.getFailedMessage().getHeaders();
        return MessageBuilder.withPayload(payload.getCause())
                .copyHeaders(requestHeaders)
                .setHeader(MessageHeaders.REPLY_CHANNEL, requestHeaders.get("originalErrorChannel"))
                .build();
    }

That originalErrorChannel comes really from the request message to the scatter-gather and is waited by the origin gateway.

Artem Bilan
  • 113,505
  • 11
  • 91
  • 118
  • I can't post all my exception handler framework because it's long. Just to give u a brief my error handler framework has a wrapper class which inherit from RuntimeException and then I handle that exception using @ControllerAdvice , so ideally any RuntimeException is there means it should be able to handle that. Why it's not able to handle? – Somnath Mukherjee Aug 03 '22 at 12:04
  • No, no need to share that one. To be sure that we are on the same page: you want to re-throw an exception from one of the recipient sub-flows and handle it in the MVC controller level and discard the rest if recipients? – Artem Bilan Aug 03 '22 at 12:40
  • Yes Artem, u r correct. Do I have to use AsyncExceptionHandler() ? – Somnath Mukherjee Aug 03 '22 at 12:44
  • I’m not sure what is that. I’d suggest you to investigate error handling in the Scatter-Gather: https://docs.spring.io/spring-integration/docs/current/reference/html/message-routing.html#scatter-gather-error-handling – Artem Bilan Aug 03 '22 at 12:48
  • Even that's not working. Can u pls tell me how do I add header and take out the error msg n all whatever u mentioned in the answer. One code example would be really helpful. – Somnath Mukherjee Aug 03 '22 at 16:08
  • See an UPDATE in my answer. – Artem Bilan Aug 03 '22 at 17:51
  • Thanks for the explanation Artem, that explains a lot. – Somnath Mukherjee Aug 03 '22 at 20:41
  • Hi @Artem, if there is an error in anotherFlow() which is after aggregate, how do I handle that error – Somnath Mukherjee Aug 04 '22 at 11:02
  • It depends how async is that flow. Technically you have that `errorChannel` header populated from the origin gateway and when an async error like that happens, the `MessagePublishingErrorHandler` is able to extract that `errorChannel` from headers and send an `ErrorMessage` to that origin gateway for handling. – Artem Bilan Aug 04 '22 at 14:17
  • some code will help me understand more Artem. If u could share that it'll be really helpful – Somnath Mukherjee Aug 04 '22 at 15:33
  • See docs for error handling in messaging gateways: https://docs.spring.io/spring-integration/docs/current/reference/html/messaging-endpoints.html#gateway-error-handling. I'm not sure what the code you'd like to see from me. You just configure an `errorChannel` on your `@MessagingGateway` and have some handler to process that error. – Artem Bilan Aug 04 '22 at 15:36
  • Just one question Artem, how do I take out the error messages that are coming from different flows. I don't see specific error messges anywhere. – Somnath Mukherjee Aug 04 '22 at 16:08
  • I'm not sure what you are talking about. The `errorChannel` produces an `ErrorMessage` with an exception to its subscriber. You can use something like `ErrorMessageExceptionTypeRouter` from there to propagate the error to respective handler per error type: https://docs.spring.io/spring-integration/docs/current/reference/html/message-routing.html#router-implementations-exception-router – Artem Bilan Aug 04 '22 at 16:16
  • Okay I'll raise another question with sample code. – Somnath Mukherjee Aug 04 '22 at 18:02