This project demonstrates how to use Eventuate Tram Saga on a concrete use case.
It has been used at various talks given at DevoXX France, Bordeaux JUG, DevFest Lille, BreizhCamp, Paris JUG, Nantes JUG, Lyon JUG.
A similar example based on MicroProfile Long Runing Actions is available on GitHub.
It is made of 3 modules :
-
Holiday service: the edge service exposed to the outside. Orchestrates the SAGA and invokes specialized backend services such as trip, hotel and car. In this version, only trip is implemented. Feel free to download the code and to complete it with the 2 other services. This will be a good exercice!
-
Trip service: a backend service invoked by Holiday
-
Trip API messaging: defines the messages (commands and replies) handled by Trip.
Comments:
-
Both services are implemented with Quarkus
-
They expose a REST API and uses CDI and JPA internally
-
They communicate with asynchronous messaging
-
Both services have their own MySQL database
-
CDC stands for Change Data Capture. It is an Eventuate component that implements the Transactional outbox pattern. Eventuate messages (commands, replies, events) are written in a message table before being published on a Kafka topic. The CDC acts as message relay and ensures transactional messaging and duplication detection
-
Kafka is used as message broker
-
Howl is a Kafka GUI enabling to easily browse topics, messages, consumer groups
-
Zookeeper supervises both Kafka and CDC.
In production, it is possible to run several CDC similar instances running in parallel (sharing the same EVENTUATELOCAL_CDC_LEADERSHIP_LOCK_PATH value), but only one is elected active by Zookeeper and able to read the message table. More information on Configuring the Eventuate CDC Service.
SAGA offer a lightweight distributed transaction model on top of local ACID transactions.
The corner stone of the programming model is the SAGA definition. It is based on a specific DSL (Domain Specific Language) enabling to define steps. Each step is made of actions (local or remote) and optional compensation operations:
The execution of a SAGA is asynchronous. Its state is persisted in database and its execution can be suspended, for instance when waiting for a command reply. When resuming, it can be run by another thread. The log messages displayed by Holiday enables to understand how threading is managed behind the hood.
This model based on a DSL is called Orchestration. Eventuate offers an alternative based on domain events called Choreography. Orchestration is recommended for complex use cases.
HolidayBookRequest is received in a synchronous way on a REST endpoint (see HolidayResource). In contrast, the execution of the SAGA is asynchronous.
For the demo, in order to display the complete response in Swagger-UI, it has been necessary to bridge the 2 modes of execution:
-
The answer to HolidayBookRequest is asynchronous using a CompetionStage<Response> response code. It is completed afterwards when the execution of the SAGA is finished
-
The end of a SAGA is notified by a HolidayBookSagaFinishedEvent domain event. This event is published by HolidayBookSaga (see onSagaCompletedSuccessfully and onSagaRolledBack methods)
-
This domain event is consumed by HolidayEventConsumer to complete the pending JAX-RS Response
-
SagaToCompletableFuture stores the association of SAGAs instance and pending CompletableFuture JAX-RS Responses.
As of this date (Jan 2023), the following technical context has been used:
-
Java 17
-
Quarkus 2.13.3.Final
-
Eventuate Platform 2022.2.RELEASE
There are several TCP ports used by this demo:
-
Zookeeper: 2181
-
Kafka: 9092,29092
-
Kafka Howl: 9088
-
MySQL-Holiday: 3306
-
MySQL-Trip: 3308
-
cdc-holiday: 9086
-
cdc-trip: 9084
-
Quarkus-Trip: 9082
-
Quarkus-Holiday: 9080
Zookeeper, Kafka, Kafka Howl, PostgreSQLs and CDCs are run with docker compose.
To start them:
-
cd saga-infra
-
./start-infra.sh: previous containers and volumes are pruned to start with a fresh situation.
After the infrastructure has been started, you can check both CDCs to ensure that they are connected to the database and Kafka:
-
cdc-holiday: http://localhost:9086/actuator/health
-
cdc-trip: http://localhost:9084/actuator/health
CDCs can be configured in 2 modes to read the message table:
-
SQL polling mode offers a generic approach that can be used for all SQL databases, it is clearly not optimal in production
-
by tailing the database server transaction log: only available for MySQL and PostgreSQL. It is highly recommended in production to improve performance and scalability.
Before running the demo, it is important to understand the processing in place:
Comments:
-
Holiday acts as the edge service exposed to the outside. When receiving a HolidayBookResource, it starts a SAGA that orchestrates the processing
-
It invokes Trip which checks the departure (accepted value: Paris) and the destination (accepted values: London, Dublin, Budapest, Barcelona), determines the transport (BOAT, TRAIN, PLANE) and the time schedule
-
For the sake of simplicity, invoking Hotel and Car is not implemented
-
Holiday checks the total price that shouldn’t exceed 500.00
The request is rejected if:
-
customer id is NOK
-
departure or destination are NOK
-
total price exceeds the maximum value.
It is accepted if all checks are OK.
Eventuate needs some database tables to work:
PS6PY can be used to discover how they are accessed behind the scene. You can activate it on Holiday by setting application.properties:
quarkus.datasource.jdbc.driver=com.p6spy.engine.spy.P6SpyDriver
P6SPY is configured in src/main/ressources/spy.properties. SQL requests are written in spy.log.
To give you an insight, the following tables are used for a confirmed SAGA :
-
saga_instance: 1 insert, 7 updates, 6 selects
-
received_messages: 3 inserts
-
message: 3 inserts
Monitoring those 3 tables is highly critical to achieve good performance in production.
Warning: P6SPY does not work with Quarkus in prod mode.
All the demo can be run from Holiday Swagger UI: http://localhost:9080/q/swagger-ui/
Kafka traffic can be checked from Kafka Howl: http://localhost:9088/topics
Trip Swagger UI can also be used to check the status of Trip entities: http://localhost:9082/q/swagger-ui/
When the application is launched, several Kafka topics are created:
Comments:
-
tripService is used by Trip to receive commands
-
*-reply is used by Trip to send command responses
-
Holiday is used to publish Holiday domain events.
From Holiday Swagger UI:
-
Chose HolidayResource POST "Book a Holiday with LRA"
-
Select "Let’s go to London" from the examples
-
Try and execute it.
The response status should be ACCEPTED.
You can check the Kafka messages that have been exchanged between Holiday and Trip with Kafka Howl by digging in the Topics. In particular, you can check that Trip reply (HolidayBookSaga topic) header reply_outcome-type is set to SUCCESS.
Check the consistency of the Trip entity:
-
Get the trip_id value of the response in Holiday Swagger UI
-
Go to Trip Swagger UI and select "find by id"
-
The status should be ACCEPTED.
From holiday Swagger UI:
-
Change the customer id value to 4
-
Execute it.
The request has been rejected by Holiday with a business error "Unknown customer".
From holiday Swagger UI:
-
Reset the customer id value to 42
-
Change the destination to "Londonx"
-
Execute it.
The request has been rejected by Trip with a business error "Rejected destination Londonx".
Check the consistency of the Trip entity:
-
Get the trip_id value of the response in Holiday Swagger UI
-
Go to Trip Swagger UI and select "find by id"
-
The status should be REJECTED
With Kafka Howl, you can check that Trip reply (HolidayBookSaga topic) header reply_outcome-type is set to FAILURE. This triggers a SAGA compensation.
From holiday Swagger UI:
-
Reset the destination value to "London"
-
Change the value of people_count to 2
-
Execute it
The request has been rejected by Holiday with a business error "Max pricing exceeded".
Check the consistency of the Trip entity:
-
Get the trip_id value of the response in Holiday Swagger UI
-
Switch to Trip Swagger UI and select "find by id"
-
The status should be CANCELED.
Building Holiday in native mode does not work yet. There is an error due to the use of java.util.Random.
How long does it take to run a SAGA?
In this demo, the response time is measured for each request (see x-response-time HTTP header in the response). It includes the complete SAGA execution. On average, on my laptop it is around 190 msec for a successfull request.
The response time of a SAGA execution highly depends on the infrastructure and the CDC configuration. On my laptop, the best results have been achieved using MySQL in transaction log tailing.
This is the default configuration, 2 other docker compose files are available if you want to switch to PostgreSQL (see saga-infrastructure directory). Quarkus application.properties files are also provided in Trip and Holiday. BTW do not forget to also change your Maven dependency from quarkus-jdbc-mysql to quarkus-jdbc-postgresql to switch to PostgreSQL.
How does it compare with Long Running Actions?
Let’s wrap up our findings:
-
REST without LRA: 30 msec
-
REST with LRA: 120 msec
-
Eventuate SAGA with orchestration: 240 msec
These results have been measured in the following context:
-
it has been run on a laptop
-
all components running locally
-
without any concurrency and multi-threading
-
the business logic is very simple
-
no optimization for Kafka and Database
Warning: these results just provide an indication and should not be taken for granted. There are potentially numerous ways to fine tune these solutions in production.Do not forget to run your own benchmark before going in production