1

I am using Spring 4.1.x APIs, Spring Integration 4.1.x APIs and Spring Integration Java DSL 1.0.x APIs for an EIP flow where we consume messages from an Oracle database table using a JdbcPollingChannelAdpater as the entry point into the flow.

Even though we have an ErrorHandler configured on the JdbcPollingChannelAdapter's Poller, we are seeing that a session's Transaction is still rolled back and not committed when a RuntimeException is thrown and correctly handled by the ErrorHandler.

After reading through this thread: Spring Transactions - Prevent rollback after unchecked exceptions (RuntimeException), I get the feeling that it is not possible to prevent a rollback and instead force a commit. Is this correct? And, if there is a way, what is the cleanest way to force a commit instead of a rollback when an error is safely handled?

Current Configuration:

IntegrationConfig.java:

@Bean
public MessageSource<Object> jdbcMessageSource() {

    JdbcPollingChannelAdapter adapter = new JdbcPollingChannelAdapter(
            dataSource,
            "select * from SERVICE_TABLE where rownum <= 10 for update skip locked");
    adapter.setUpdateSql("delete from SERVICE_TABLE where SERVICE_MESSAGE_ID in (:id)");
    adapter.setRowMapper(serviceMessageRowMapper);
    adapter.setMaxRowsPerPoll(1);
    adapter.setUpdatePerRow(true);
    return adapter;
}

@SuppressWarnings("unchecked")
@Bean
public IntegrationFlow inFlow() {

    return IntegrationFlows
            .from(jdbcMessageSource(),
                    c -> {
                        c.poller(Pollers.fixedRate(100)
                                .maxMessagesPerPoll(10)
                                .transactional(transactionManager)
                                .errorHandler(errorHandler));
                    })
                .channel(inProcessCh()).get();
}

ErrorHandler.java

@Component
public class ErrorHandler implements org.springframework.util.ErrorHandler {

    @Autowired
    private PlatformTransactionManager transactionManager;

    private static final Logger logger = LogManager.getLogger();

    @Override
    public void handleError(Throwable t) {

        logger.trace("handling error:{}", t.getMessage(), t);

        // handle error code here...

        // we want to force commit the transaction here?
        TransactionStatus txStatus = transactionManager.getTransaction(null);
        transactionManager.commit(txStatus);
    }
}

--- EDITED to include ExpressionEvaluatingRequestHandlerAdvice Bean ---

@Bean
public Advice expressionEvaluatingRequestHandlerAdvice() {
    ExpressionEvaluatingRequestHandlerAdvice expressionEvaluatingRequestHandlerAdvice = new ExpressionEvaluatingRequestHandlerAdvice();
    expressionEvaluatingRequestHandlerAdvice.setTrapException(true);
    expressionEvaluatingRequestHandlerAdvice.setOnSuccessExpression("payload");
    expressionEvaluatingRequestHandlerAdvice
            .setOnFailureExpression("payload");
    expressionEvaluatingRequestHandlerAdvice.setFailureChannel(errorCh());
    return expressionEvaluatingRequestHandlerAdvice;
}

--- EDITED to show Dummy Test Message handler ---

    .handle(Message.class,
                    (m, h) -> {

                        boolean forceTestError = m.getHeaders().get("forceTestError");
                        if (forceTestError) {
                            logger.trace("simulated forced TestException");
                            TestException testException = new TestException(
                                    "forced test exception");
                            throw testException;
                        }

                        logger.trace("simulated successful process");

                        return m;
                    }, e-> e.advice(expressionEvaluatingRequestHandlerAdvice())

--- EDITED to show ExecutorChannelInterceptor method ---

@Bean
public IntegrationFlow inFlow() {

    return IntegrationFlows
            .from(jdbcMessageSource(), c -> {
                c.poller(Pollers.fixedRate(100).maxMessagesPerPoll(10)
                        .transactional(transactionManager));
            })
            .enrichHeaders(h -> h.header("errorChannel", errorCh(), true))
            .channel(
                    MessageChannels.executor("testSyncTaskExecutor",
                            syncTaskExecutor()).interceptor(
                            testExecutorChannelInterceptor()))
            .handle(Message.class, (m, h) -> {
                    boolean forceTestError = m.getHeaders().get("forceTestError");
                    if (forceTestError) {
                        logger.trace("simulated forced TestException");
                        TestException testException = new TestException(
                                "forced test exception");
                        throw testException;
                    }

                    logger.trace("simulated successful process");
            }).channel("nullChannel").get();
}
Community
  • 1
  • 1
Going Bananas
  • 2,265
  • 3
  • 43
  • 83

1 Answers1

1

It won't work just because your ErrorHandler works already after the finish of TX.

Here is a couple lines of source code (AbstractPollingEndpoint.Poller):

@Override
public void run() {
    taskExecutor.execute(new Runnable() {
        @Override
        public void run() {

           .............
                try {
                        if (!pollingTask.call()) {
                            break;
                        }
                        count++;
                    }
                    catch (Exception e) {
            ....
                    }
                }
            }
        });
    }

Where:

  1. The ErrorHandler is applied for the taskExecutor (SyncTaskExecutor) by default.

  2. TransactionInterceptor being as Aspect is applied for the Proxy around that pollingTask.

Therefore TX is done around the pollingTask.call() and goes out. And only after that your ErrorHandler starts to work inside taskExecutor.execute().

To fix your issue, you need to figure out which downstream flow part isn't so critical for TX rallback and make there some try...catch or use ExpressionEvaluatingRequestHandlerAdvice to "burke" that RuntimeException.

But as you have noticed by my reasoning that must be done within TX.

Artem Bilan
  • 113,505
  • 11
  • 91
  • 118
  • We do handle errors using `try...catch` but what we do then (for all our flows), is wrap the error into a business-specific `RuntimeException` which gets thrown back and routed to a business-specific `errorChannel`. I've created `ExpressionEvaluatingRequestHandlerAdvice` (See bean in main question). To test, I've created a dummy flow with a simple service which has the said Advice wrapped around it but now, neither successful process or error process test shows the tx commit taking place. Is there a spring-integration-java-dsl example showing this Advice in use somewhere? – Going Bananas Aug 12 '15 at 10:35
  • Actually, it looks like the Advice is configured properly and transactions are committing during success and error process tests. (Sorry, Eclipse IDE wasn't showing test had paused at a behind-the-scenes breakpoint therefore commit not completing) – Going Bananas Aug 12 '15 at 10:51
  • One more question though, in my test, I wrapped a specific message handler (service) with the Advice. Is there a way to set this Advice globally so that it intercepts all Message handlers across the Spring Context's flows or at least across all Message handlers in a given synchronous transacted flow? – Going Bananas Aug 12 '15 at 10:53
  • Yes, it is possible. By the standrad Spring AOP framework, bean names for `MessageHandler`s or just `MessageHandler.handleMessage()` point-cut pattern. – Artem Bilan Aug 13 '15 at 20:39
  • First time working with AOP... so to start with, I've created an `@Aspect` annotated class and I can make it intercept calls to a message handler handling a `@Service` component by annotating a method with something like: `@After("execution(* abc.example.service.TestService.consume(..))")` - this is intercepted ok but, I can't intercept the owning `MessageHandler`'s `.handleMessage()` call itself. I have tried: `@After("execution(* org.springframework.messaging.MessageHandler.*(..))")` but no interception occurs in this case. What am I doing wrong? – Going Bananas Aug 14 '15 at 13:28
  • Yeah... That's true. Because all Spring Integration components extends `AbstractMessageHandler` which has its `handleMessage()` as **final**. That prevents to proxy the method. From other side, please, take a look to the `ExecutorChannelInterceptor` implementation option. – Artem Bilan Aug 14 '15 at 16:52
  • I've edited the flow to run with an `ExecutorChannelInterceptor` (See most recent EDIT). The Interceptor implementation doesn't do anything other than implement the interface methods at their simplest and I have set it on a `SyncTaskExecutor` over a message channel. Is this the correct way of using an interceptor in this case? Using the interceptor method, the rollbacks have been replaced by commits (which is great) but, I am now having problems making this method work over multiple concurrent threads. Is this not possible? – Going Bananas Aug 18 '15 at 15:34
  • You should use there `org.springframework.messaging.support.ExecutorSubscribableChannel`. The `ExecutorChannelInterceptor` is supported by the Spring Integration since version `4.2`, which is in its `RC1` with `GA` just before SpringOne this September. From other side I can't say anything about your concurrency issue because I don't see the source code for your `testExecutorChannelInterceptor()`... – Artem Bilan Aug 18 '15 at 15:40
  • Ah, yes. Adding the `ExecutorChannelInterceptor` to an `ExecutorSubscribableChannel` instead has made a whole lot of difference. And the lack of full support until `4.2` probably explains why previously I was only seeing some of the interceptor's methods being called and not ones like `beforeHandle()` and `afterMessageHandled()` (which I can see now). Please ignore multi-threading issue question earlier (not an issue any more). I will run a full batch of functional tests on the full flows tomorrow just to confirm all is well and will accept and close thread then. Cheers! – Going Bananas Aug 18 '15 at 16:40
  • After further tests and debugging, it has become clear that transactions are still not being committed (instead of rolled back) when the `ExecutorChannelInterceptor` is called. I can force-commit the transaction in the `afterMessageHandled()` method for example but, if I do that, the `TransactionInterceptor` still attempts to rollback the transaction and, at that point, a rollback exception occurs. Therefore, the commit occurs, but a rollback attempt also occurs. How can we suppress the rollback attempt? – Going Bananas Aug 20 '15 at 09:50
  • 1
    OK. I see that I wasn't right. The `ExecutorSubscribableChannel` has a code to *re-throw* Exception after performing `triggerAfterMessageHandled`. So, any manipulation inside won't help. I see only the way to `try...catch` the target service to prevent rollback or anything else... Otherwise you should wait for the https://jira.spring.io/browse/INT-3770 or rely on the `ExpressionEvaluatingRequestHandlerAdvice` for each `` – Artem Bilan Aug 20 '15 at 14:11
  • In that case, we will go with your suggestion of the `ExpressionEvaluatingRequestHandlerAdvice` advice for each `@ServiceActivator` along the flow and we will keep an eye out for [INT-3770](https://jira.spring.io/browse/INT-3770) to become available in `4.3` and see what the possibilities are then. Something like a `JMS DMLC`'s `setAcknowledgeMode()`/`setTransacted()` options would be ideal for us if anything like that would be at all possible with a `JDBC polling adapter` type flow scenario. – Going Bananas Aug 21 '15 at 10:12