Red Hat Training

A Red Hat training course is available for Red Hat Fuse

5.4. Error Handling and Rollbacks

Overview

While you can use standard Apache Camel error handling techniques in a transactional route, it is important to understand the interaction between exceptions and transaction demarcation. In particular, you need to bear in mind that thrown exceptions usually cause transaction rollback.

How to roll back a transaction

You can use one of the following approaches to roll back a transaction:

Runtime exceptions as rollbacks

The most common way to roll back a Spring transaction is to throw a runtime (unchecked) exception—that is, where the exception is an instance or subclass of java.lang.RuntimeException. Java errors, of java.lang.Error type, also trigger transaction rollback. Checked exceptions, on the other hand, do not trigger rollback. Figure 5.3, “Errors and Exceptions that Trigger Rollback” summarises how Java errors and exceptions affect transactions, where the classes that trigger rollback are shaded gray.

Figure 5.3. Errors and Exceptions that Trigger Rollback

Errors and Exceptions that Trigger Rollback
Note
The Spring framework also provides a system of XML annotations that enable you to specify which exceptions should or should not trigger rollbacks. For details, see Rolling back in the Spring Reference Guide.
Warning
If a runtime exception is handled within the transaction (that is, before the exception has the chance to percolate up to the code that does the transaction demarcation), the transaction will not be rolled back. See the section called “How to define a dead letter queue” for details.

The rollback() DSL command

If you want to trigger a rollback in the middle of a transacted route, you can do this by calling the rollback() DSL command, which throws an org.apache.camel.RollbackExchangeException exception. In other words, the rollback() command uses the standard approach of throwing a runtime exception to trigger the rollback.
For example, if you decide that there should be an absolute limit on the size of money transfers in the account services application, you could trigger a rollback when the amount exceeds 100, using the following code:

Example 5.2. Rolling Back an Exception with rollback()

from("file:src/data?noop=true")
    .transacted()
    .beanRef("accountService","credit")
    .choice().when(xpath("/transaction/transfer[amount > 100]"))
        .rollback()
    .otherwise()
        .to("direct:txsmall");

from("direct:txsmall")
    .beanRef("accountService","debit")
    .beanRef("accountService","dumpTable")
    .to("file:target/messages/small");
Note
If you trigger a rollback in the preceding route, it will get trapped in an infinite loop. The reason for this is that the RollbackExchangeException exception thrown by rollback() propagates back to the file endpoint at the start of the route. The File component has a built-in reliability feature that causes it to resend any exchange for which an exception has been thrown. Upon resending, of course, the exchange just triggers another rollback, leading to an infinite loop.

The markRollbackOnly() DSL command

The markRollbackOnly() DSL command enables you to force the current transaction to roll back, without throwing an exception. This can be useful in cases where (as in Example 5.2, “Rolling Back an Exception with rollback()”) throwing an exception has unwanted side effects.
For example, Example 5.3, “Rolling Back an Exception with markRollbackOnly()” shows how to modify Example 5.2, “Rolling Back an Exception with rollback()” by replacing rollback() with markRollbackOnly(). This version of the route solves the problem of the infinite loop. In this case, when the amount of the money transfer exceeds 100, the current transaction is rolled back, but no exception is thrown. Because the file endpoint does not receive an exception, it does not retry the exchange, and the failed transactions is quietly discarded.

Example 5.3. Rolling Back an Exception with markRollbackOnly()

from("file:src/data?noop=true")
    .transacted()
    .beanRef("accountService","credit")
    .choice().when(xpath("/transaction/transfer[amount > 100]"))
        .markRollbackOnly()
    .otherwise()
        .to("direct:txsmall");
...
The preceding route implementation is not ideal, however. Although the route cleanly rolls back the transaction (leaving the database in a consistent state) and avoids the pitfall of infinite looping, it does not keep any record of the failed transaction. In a real-world application, you would typically want to keep track of any failed transaction. For example, you might want to write a letter to the relevant customer in order to explain why the transaction did not succeed. A convenient way of tracking failed transactions is to add a dead-letter queue to the route.

How to define a dead letter queue

In order to keep track of failed transactions, you can define an onException() clause, which enables you to divert the relevant exchange object to a dead-letter queue. When used in the context of transactions, however, you need to be careful about how you define the onException() clause, because of potential interactions between exception handling and transaction handling. Example 5.4, “How to Define a Dead Letter Queue” shows the correct way to define an onException() clause, assuming that you need to suppress the rethrown exception.

Example 5.4. How to Define a Dead Letter Queue

// Java
import org.apache.camel.spring.SpringRouteBuilder;

public class MyRouteBuilder extends SpringRouteBuilder {
    ...
    public void configure() {
        onException(IllegalArgumentException.class)
            .maximumRedeliveries(1)
            .handled(true)
            .to("file:target/messages?fileName=deadLetters.xml&fileExist=Append")
            .markRollbackOnly();  // NB: Must come *after* the dead letter endpoint.

        from("file:src/data?noop=true")
            .transacted()
            .beanRef("accountService","credit")
            .beanRef("accountService","debit")
            .beanRef("accountService","dumpTable")
            .to("file:target/messages");
    }
}
In the preceding example, onException() is configured to catch the IllegalArgumentException exception and send the offending exchange to a dead letter file, deadLetters.xml (of course, you can change this definition to catch whatever kind of exception arises in your application). The exception rethrow behavior and the transaction rollback behavior are controlled by the following special settings in the onException() clause:
  • handled(true)—suppress the rethrown exception. In this particular example, the rethrown exception is undesirable because it triggers an infinite loop when it propagates back to the file endpoint (see the section called “The markRollbackOnly() DSL command”). In some cases, however, it might be acceptable to rethrow the exception (for example, if the endpoint at the start of the route does not implement a retry feature).
  • markRollbackOnly()—marks the current transaction for rollback without throwing an exception. Note that it is essential to insert this DSL command after the to() command that routes the exchange to the dead letter queue. Otherwise, the exchange would never reach the dead letter queue, because markRollbackOnly() interrupts the chain of processing.

Catching exceptions around a transaction

Instead of using onException(), a simple approach to handling exceptions in a transactional route is to use the doTry() and doCatch() clauses around the route. For example, Example 5.5, “Catching Exceptions with doTry() and doCatch()” shows how you can catch and handle the IllegalArgumentException in a transactional route, without the risk of getting trapped in an infinite loop.

Example 5.5. Catching Exceptions with doTry() and doCatch()

// Java
import org.apache.camel.spring.SpringRouteBuilder;

public class MyRouteBuilder extends SpringRouteBuilder {
    ...
    public void configure() {
        from("file:src/data?noop=true")
            .doTry()
                .to("direct:split")
            .doCatch(IllegalArgumentException.class)
                .to("file:target/messages?fileName=deadLetters.xml&fileExist=Append")
            .end();
        
        from("direct:split")
            .transacted()
            .beanRef("accountService","credit")
            .beanRef("accountService","debit")
            .beanRef("accountService","dumpTable")
            .to("file:target/messages");
    }
}
In this example, the route is split into two segments. The first segment (from the file:src/data endpoint) receives the incoming exchanges and performs the exception handling using doTry() and doCatch(). The second segment (from the direct:split endpoint) does all of the transactional work. If an exception occurs within this transactional segment, it propagates first of all to the transacted() command, causing the current transaction to be rolled back, and it is then caught by the doCatch() clause in the first route segment. The doCatch() clause does not rethrow the exception, so the file endpoint does not do any retries and infinite looping is avoided.