Transaction Retries using JavaEE and CDI with CMTs
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 @TransactionManagement
annotation 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)
.
Subscribe to my newsletter
Read articles from Kai Niemi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by