Transaction Retries using JavaEE and CDI with CMTs

Transaction Retries using JavaEE and CDI with CMTs

Implementing client-side transaction retries on TomEE and JavaEE 8

·

4 min read

In this post, we'll use the same concept as in Transaction Retries using JavaEE and CDI with BMTs. Only this time with container-managed transactions, or CMTs which is the default mode of operation with JTA.

Transaction Retries in JavaEE

This article demonstrates an AOP-driven retry strategy for JavaEE apps using Stateless Session Beans with container-managed transactions (CMT), using the same stack and demo use case as in Transaction Retries using JavaEE and CDI with BMTs.

Source Code

The source code for examples of this article can be found on GitHub.

What's the difference

To use bean-managed transactions, you would just add the @TransactionManagementannotation and set the transaction attributes accordingly:

@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
@TransactionAttribute(REQUIRES_NEW)
public class OrderService {

    @TransactionBoundary
    public Order placeOrder(Order order) {
        Assert.isTrue(entityManager.isJoinedToTransaction(), "Expected transaction!");

        entityManager.persist(order);
        return order;
    }
}

With container-managed transactions (the default), you can either be explicit or leave out the @TranactionManagement annotation. Then add @TransactionAttribute(NOT_SUPPORTED)` alongside @TransactionBoundary in the boundary methods. This will inform the container to not start a new transaction when invoking this method:

@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
public class OrderService {

    @TransactionBoundary
    @TransactionAttribute(NOT_SUPPORTED)
    public Order placeOrder(Order order) {
        Assert.isTrue(entityManager.isJoinedToTransaction(), "Expected transaction!");

        entityManager.persist(order);
        return order;
    }
}

So the interesting question now is: How can that assertion still be true, given that NOT_SUPPORTED propagation is used?

The answer sits in the @TransactionBoundary annotation which uses an @InterceptorBinding to wire in the TransactionRetryInterceptor, which in turn invokes a transaction service with REQUIRES_NEW propagation. The effect is that although it appears like that service boundary method is not transactional, it actually is but it's invocation is now deferred to a retry loop in the interceptor.

This is a less intrusive approach to add retry logic to session beans and service activators (message listeners) when already invested in BMTs. No major refactoring efforts are needed.

Demo

To try this out, we'll use the same order system designed to produce unrepeatable read (aka read/write) conflicts to activate the retry mechanism.

Building

Prerequisites

  • JDK8+ with 1.8 language level (OpenJDK compatible)

  • Maven 3+ (optional, embedded)

  • CockroachDB v22.1+ database

Install the JDK (Linux):

sudo apt-get -qq install -y openjdk-8-jdk

Clone the project

git clone git@github.com/kai-niemi/retry-demo.git
cd retry-demo

Build the project

chmod +x mvnw
./mvnw clean install

Setup

Create the database:

cockroach sql --insecure --host=localhost -e "CREATE database orders"

Create the schema:

cockroach sql --insecure --host=locahlost --database orders  < src/resources/conf/create.sql

Start the app:

../mvnw clean install tomee:run

The default listen port is 8090 (can be changed in pom.xml):

Usage

Open another shell and check that the service is up and connected to the DB:

curl http://localhost:8090/api

Get Order Request Form

This prints out an order form template that we will use to create new orders:

curl http://localhost:8090/api/order/template| jq

Alternatively, pipe it to a file:

curl http://localhost:8090/api/order/template > form.json

Submit Order Form

Create a new purchase order:

curl http://localhost:8090/api/order -i -X POST \
-H 'Content-Type: application/json' \
-d '{
    "billAddress": {
        "address1": "Street 1.1",
        "address2": "Street 1.2",
        "city": "City 1",
        "country": "Country 1",
        "postcode": "Code 1"
    },
    "customerId": -1,
    "deliveryAddress": {
        "address1": "Street 2.1",
        "address2": "Street 2.2",
        "city": "City 2",
        "country": "Country 2",
        "postcode": "Code 2"
    },
    "requestId": "bc3cba97-dee9-41b2-9110-2f5dfc2c5dae"
}'

Or using the file:

curl http://localhost:8090/api/order -H "Content-Type:application/json" -X POST \
-d "@form.json"

Produce a Read/Write Conflict

Assuming we have an order with ID 1 in status PLACED. We will now read that order and change the status to something else by using concurrent transactions. This is known as the unrepeatable read conflict, prevented by 1SR from happening.

To have a predictable outcome, we'll use two sessions with a controllable delay between the read and write operations.

Overview of SQL operations:

select * from purchase_order where id=1; -- T1 
-- status is `PLACED`
wait 5s -- T1 
select * from purchase_order where id=1; -- T2
wait 5s -- T2
update status='CONFIRMED' where id=1; -- T1
update status='PAID' where id=1; -- T2
commit; -- T1
commit; -- T2 ERROR!

Prepare to run the first command:

curl http://localhost:8090/api/order/1?status=CONFIRMED\&delay=5000 -i -X PUT

Open another session, and prepare to run a similar command in less than 5sec after the first one:

curl http://localhost:8090/api/order/1?status=PAID\&delay=5000 -i -X PUT

When both commands are executed serially, it will cause a serialization conflict like this:

ERROR: restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write for key /Table/109/1/12/0 at timestamp 1669990868.355588000,0 too old; wrote at 1669990868.778375000,3: "sql txn" meta={id=92409d02 key=/Table/109/1/12/0 pri=0.03022202 epo=0 ts=1669990868.778375000,3 min=1669990868.355588000,0 seq=0} lock=true stat=PENDING rts=1669990868.355588000,0 wto=false gul=1669990868.855588000,0

The interceptor will however catch this error since it has a state code 40001, retry the business method and eventually succeed and deliver a 200 OK to the client.

Conclusion

In this article, we implemented a transaction retry strategy for JavaEE stateless session beans using container-managed transactions and a custom interceptor with interceptor bindings.

This reduces the amount of retry logic in ordinary service beans to simply add a @TransactionBoundary meta-annotation and change the transaction attribute to @TransactionAttribute(NOT_SUPPORTED).