Distributed transactions for microservices are known as SAGA design patterns and are defined by the {microprofile-lra-spec-url}[Micro Profile Long Running Actions specification]. Unlike well known XA protocol, LRA is asynchronous and therefore much more scalable. Every LRA JAX-RS resource (participant) defines endpoints to be invoked when transaction needs to be completed or compensated.
<dependency>
<groupId>io.helidon.microprofile.lra</groupId>
<artifactId>helidon-microprofile-lra</artifactId>
</dependency>
<!-- Support for Narayana coordinator -->
<dependency>
<groupId>io.helidon.lra</groupId>
<artifactId>helidon-lra-coordinator-narayana-client</artifactId>
</dependency>LRA transactions need to be coordinated over REST API by the LRA coordinator. Coordinator
keeps track of all transactions and calls the @Compensate or @Complete endpoints for all participants involved in the particular
transaction. LRA transaction is first started, then joined by participant.
Participant reports the successful finish of transaction by calling complete. Coordinator then calls the JAX-RS
complete endpoint that was registered during the join of each
participant. As the completed or compensated participants don’t have to be on same instance,
the whole architecture is highly scalable.
In case of error during the LRA transaction, participant reports cancel of LRA to coordinator. Coordinator calls compensate on all the joined participants.
When participant joins the LRA with timeout defined @LRA(value = LRA.Type.REQUIRES_NEW, timeLimit = 5, timeUnit = ChronoUnit.MINUTES), coordinator compensate if timeout occurs before close is reported by participants.
The Participant, or Compensator, is an LRA resource with at least one of the JAX-RS(or non-JAX-RS) methods annotated with @Compensate or @AfterLRA.
Marks JAX-RS method which should run in LRA context and needs to be accompanied by at least minimal set of mandatory participant methods(Compensate or AfterLRA).
LRA options:
-
-
REQUIRED join incoming LRA or create and join new
-
REQUIRES_NEW create and join new LRA
-
MANDATORY join incoming LRA or fail
-
SUPPORTS join incoming LRA or continue outside LRA context
-
NOT_SUPPORTED always continue outside LRA context
-
NEVER Fail with 412 if executed in LRA context
-
NESTED create and join new LRA nested in the incoming LRA context
-
-
timeLimit max time limit before LRA gets cancelled automatically by coordinator
-
timeUnit time unit if the timeLimit value
-
end when false LRA is not closed after successful method execution
-
cancelOn which HTTP response codes of the method causes LRA to cancel
-
cancelOnFamily which family of HTTP response codes causes LRA to cancel
Method parameters:
-
Header LRA_HTTP_CONTEXT_HEADER - id of the LRA transaction
@PUT
@LRA(LRA.Type.REQUIRES_NEW, timeLimit = 500, timeUnit = ChronoUnit.MILLIS)
@Path("start-example")
public Response startLra(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId, String data)|
Warning
|
Expected to be called by LRA coordinator only! |
Compensate method is called by coordinator when LRA is cancelled, usually by error during execution of method body of @LRA annotated method. If the method responds with 500 or 202, coordinator will eventually try the call again. If participant has @Status annotated method, coordinator retrieves the status to find out if retry should be done.
-
Header LRA_HTTP_CONTEXT_HEADER - id of the LRA transaction
-
Header LRA_HTTP_PARENT_CONTEXT_HEADER - parent LRA id in case of nested LRA
@PUT
@Path("/compensate")
@Compensate
public Response compensateWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parent){
return LRAResponse.compensated();
}|
Warning
|
Expected to be called by LRA coordinator only! |
Complete method is called by coordinator when LRA is successfully closed. If the method responds with 500 or 202, coordinator will eventually try the call again. If participant has @Status annotated method, coordinator retrieves the status to find out if retry should be done.
-
Header LRA_HTTP_CONTEXT_HEADER - id of the LRA transaction
-
Header LRA_HTTP_PARENT_CONTEXT_HEADER - parent LRA id in case of nested LRA
@PUT
@Path("/complete")
@Complete
public Response complete(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLraId)|
Warning
|
Expected to be called by LRA coordinator only! |
Complete and compensate methods can fail(500) or report that compensation/completion is in progress(202). In such case participant needs to be prepared to report its status over @Status annotated method to coordinator. When coordinator decides all the participants have finished, method annotated with @Forget is called.
-
Header LRA_HTTP_CONTEXT_HEADER - id of the LRA transaction
-
Header LRA_HTTP_PARENT_CONTEXT_HEADER - parent LRA id in case of nested LRA
@DELETE
@Path("/forget")
@Forget
public Response forget(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parent)Method annotated with @Leave called with LRA context(with header LRA_HTTP_CONTEXT_HEADER) informs coordinator that current participant is leaving the LRA. Method body is executed after leave signal is sent. As a result, participant methods complete and compensate won’t be called when the particular LRA ends.
-
Header LRA_HTTP_CONTEXT_HEADER - id of the LRA transaction
@PUT
@Path("/leave")
@Leave
public Response leaveLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraIdtoLeave)|
Warning
|
Expected to be called by LRA coordinator only! |
If the coordinator’s call to the participant’s method fails, then it will retry the call. If the participant is not idempotent, then it may need to report its state to coordinator by declaring method annotated with @Status for reporting if previous call did change participant status. Coordinator can call it and decide if compensate or complete retry is needed.
-
Header LRA_HTTP_CONTEXT_HEADER - id of the LRA transaction
-
ParticipantStatus - Status of the participant reported to coordinator
@GET
@Path("/status")
@Status
public Response reportStatus(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
return Response.status(ParticipantStatus.FailedToCompensate).build();
}-
URI with LRA id
-
ParticipantStatus - Status of the participant reported to coordinator
@Status
public Response reportStatus(URI lraId){
return Response.ok(ParticipantStatus.FailedToCompensate).build();
}|
Warning
|
Expected to be called by LRA coordinator only! |
Method annotated with @AfterLRA in the same class as the one with @LRA annotation gets invoked after particular LRA finishes.
-
Header LRA_HTTP_ENDED_CONTEXT_HEADER - id of the finished LRA transaction
-
Header LRA_HTTP_PARENT_CONTEXT_HEADER - parent LRA id in case of nested LRA
-
LRAStatus - Final status of the LRA (Cancelled, Closed, FailedToCancel, FailedToClose)
@PUT
@Path("/finished")
@AfterLRA
public Response whenLRAFinishes(@HeaderParam(LRA_HTTP_ENDED_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLraId,
LRAStatus status)-
URI with finished LRA id
-
LRAStatus - Final status of the LRA (Cancelled, Closed, FailedToCancel, FailedToClose)
public void whenLRAFinishes(URI lraId, LRAStatus status)mp.lra:
coordinator.url: http://localhost:8070/lra-coordinator (1)
propagation.active: true (2)
participant.url: http://coordinator.visible.host:80/awesomeapp (3)-
Url of coordinator
-
Propagate LRA headers LRA_HTTP_CONTEXT_HEADER and LRA_HTTP_PARENT_CONTEXT_HEADER through non-LRA endpoints
-
Url of the LRA enabled service overrides standard base uri, so coordinator can call load-balancer instead of the service
For more information continue to {microprofile-lra-spec-url}[Micro Profile Long Running Actions specification].
The following example shows how a simple LRA participant starts and joins a transaction after calling the '/start-example' resource.
When startExample method finishes successfully, close is reported to coordinator
and /complete-example endpoint is called by coordinator to confirm successful closure of the LRA.
If an exception occurs during startExample method execution, coordinator receives cancel call and /compensate-example
is called by coordinator to compensate for cancelled LRA transaction.
@PUT
@LRA(LRA.Type.REQUIRES_NEW) (1)
@Path("start-example")
public Response startExample(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId, (2)
String data) {
if (data.contains("BOOM")) {
throw new RuntimeException("BOOM 💥"); (3)
}
LOGGER.info("Data " + data + " processed 🏭");
return Response.ok().build(); (4)
}
@PUT
@Complete (5)
@Path("complete-example")
public Response completeExample(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
LOGGER.log(Level.INFO, "LRA id: {0} completed 🎉", lraId);
return LRAResponse.completed();
}
@PUT
@Compensate (6)
@Path("compensate-example")
public Response compensateExample(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
LOGGER.log(Level.SEVERE, "LRA id: {0} compensated 🦺", lraId);
return LRAResponse.compensated();
}-
This JAX-RS PUT method will start new LRA transactions and join it before method body gets executed
-
LRA id assigned by coordinator to this LRA transaction
-
When method execution finishes exceptionally, cancel signal for this particular LRA is sent to coordinator
-
When method execution finishes successfully, complete signal for this particular LRA is sent to coordinator
-
Method which will be called by coordinator when LRA is completed
-
Method which will be called by coordinator when LRA is canceled
Coordinator is a service that tracks all LRA transactions and calls the compensate REST endpoints of the participants when the LRA transaction gets cancelled or completes (in case it gets closed). In addition, participant also keeps track of timeouts, retries participant calls, and assigns LRA ids.
-
Helidon LRA coordinator
|
Caution
|
Experimental tool, usage in production is not advised. |
docker build -t helidon/lra-coordinator https://github.com/oracle/helidon.git#:lra/coordinator/server
docker run -dp 8070:8070 --name lra-coordinator --network="host" helidon/lra-coordinatorHelidon LRA coordinator is compatible with Narayana clients, you need to add an additional dependency for Narayana client:
<dependency>
<groupId>io.helidon.lra</groupId>
<artifactId>helidon-lra-coordinator-narayana-client</artifactId>
</dependency>Narayana is a transaction manager supporting LRA. To use Narayana LRA coordinator with Helidon LRA client you need to add an additional dependency for Narayana client:
<dependency>
<groupId>io.helidon.lra</groupId>
<artifactId>helidon-lra-coordinator-narayana-client</artifactId>
</dependency>The simplest way to run Narayana LRA coordinator locally:
wget https://search.maven.org/remotecontent?filepath=org/jboss/narayana/rts/lra-coordinator-quarkus/5.11.1.Final/lra-coordinator-quarkus-5.11.1.Final-runner.jar \
-O narayana-coordinator.jar \
&& java -Dquarkus.http.port=8070 -jar narayana-coordinator.jarNarayana LRA coordinator is running by default under lra-coordinator context,
with port 8070 defined in the snippet above you need to configure your Helidon LRA app as follows:
mp.lra.coordinator.url=http://localhost:8070/lra-coordinator
-
{microprofile-lra-spec-url}[Micro Profile Long Running Actions specification]