Sample demo application with setup steps to get started with MicroProfile LRA specification
You can choose to follow the steps described in this document or you can directly move the final solution in the End solution directory and follow the startup steps there.
For our examples we will use Throntail as the runtime. Go to https://thorntail.io/generator/ and create an application called lra-service with JAX-RS and CDI fractions:
Extract your downloaded ZIP into a work directory (let’s call it WORK_DIR
):
unzip /path/to/lra-service.zip -d $WORK_DIR
cd $WORK_DIR
and open the project in your favourite IDE. Also please change the
<build><finalName>
in your generated pom.xml
from demo
to
${project.artifactId}
which allows us to differenciate our services.
To start with LRA we need to include two maven dependencies:
<dependency>
<groupId>org.eclipse.microprofile.lra</groupId>
<artifactId>microprofile-lra-api</artifactId>
<version>1.0-RC1</version>
</dependency>
<dependency>
<groupId>org.jboss.narayana.rts</groupId>
<artifactId>narayana-lra</artifactId>
<version>5.9.8.Final</version>
</dependency>
The first dependency is a MicroProfile LRA API binary containing all of the required classes that users interact with. The second one is the dependency on the Narayana implementation of the MP LRA specification.
Note
|
In fact, only the narayana-lra dependency is required as it also transitively pulls MP LRA dependency. But in the environments where the implementation of the specification can be provided by the container, only the MP LRA dependency can be used as users interact only with the LRA specification API. |
Add these two dependencies to the LRA service pom.xml
.
LRA specification is based on the JAX-RS. For a start, let’s create a new JAX-RS resource called LraResource:
package io.xstefank.example.lra.lraservice.rest;
import org.eclipse.microprofile.lra.annotation.ws.rs.LRA;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import java.net.URI;
@Path("/lra")
@ApplicationScoped
public class LraResource {
@GET
@Path("/perform")
@LRA
public Response performLRA(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId) {
System.out.println("PERFORMING LRA work " + lraId);
return Response.ok(lraId.toString()).build();
}
}
As you can see, this is a normal JAX-RS resource enhanced with the @LRA
annotation.
The @LRA
annotation is the main entry point for the LRA specification. The
default behavior starts a new LRA when this method is invoked and finishes it
when the method end.
We also added a specification defined LRA header parameter
(LRA.LRA_HTTP_CONTEXT_HEADER == "Long-Running-Action"
) in which the
implementation passes the LRA identification which is represented by a
unique URI. This LRA id can be used later for the work associated with this
particular LRA.
The Narayana implementation is based on the orchestration approach which means that Narayana provides an LRA coordinator microservice that is responsible for the LRA processing and execution. The LRA coorodinator is a required service for the LRA to funtion properly. It is provided as a Docker image so to start the LRA coordinator you can run:
docker run --rm --name lra-coordinator --network host jbosstm/lra-coordinator
or alternatively you can download the Narayana LRA source code from GitHub and build it and run it locally. The coordinator jar is localted in the lra-coordinator directory.
Now that the LRA coordinator is running we can start our LRA service:
build the service with: mvn clean package
and run it with: java -jar -Dthorntail.http.port=8081 -Dlra.http.port=8080 -jar target/lra-service-thorntail.jar
Note
|
We need to start the LRA service on the port 8081 as the LRA coordinator
already occupies the port 8080. We also have to specify where the coordinator
runs by lra.http.port property (the default host value is localhost).
|
Now we can start our first LRA by invoking this endpoint:
http :8081/lra/perform
Note
|
I am using HTTPie but if you are more used to curl, perform a GET call
like this curl -X GET http://localhost:8081/lra/perform .
|
Congratulations, we have started and finished our first LRA. For now, it’s
not doing anything else so let’s enlist our LraResource
as a participant
in the newly started LRA.
To enlist LraResource
as an LRA participant we need to include a PUT JAX-RS
method annotated by the @Compensate
annotation:
@PUT
@Path("/compensate")
@Compensate
public Response compensate(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId) {
System.out.println("COMPENSATING LRA " + lraId);
return Response.ok().build();
}
This method represents the compesnation action that is going to be invoked
when the LRA (which the resource joined and also started in our case) is
cancelled. This happens when the @LRA
method execution fails with a
specific JAX-RS response code which by default is in the families of 4xx and
5xx codes. Let’s change our @LRA
method to return 412 Precondition Failed
status code to cancel the LRA rather than close it:
@GET
@Path("/perform")
@LRA
public Response performLRA(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("PERFORMING LRA work " + lraId);
System.out.println("PERFORM recovery id: " + recoveryId);
return Response.status(Response.Status.PRECONDITION_FAILED).build();
}
Notice that we also added a new header paramater called
LRA.LRA_HTTP_RECOVERY_HEADER
. This header represents a unique URI that
is received as the idendification of this participant inclusion in the
particular LRA. You can look at it as a subscription id. Performing operations
with this recoveryId can help resource identify which particular LRA is being
compensated/closed when it is joining several LRAs in parallel but also helps,
as name suggests, with the recovery if the service needs to be restarted. The
recoveryId can also be retrieved in the @Compensate
method:
@PUT
@Path("/compensate")
@Compensate
public Response compensate(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("COMPENSATING LRA " + lraId);
System.out.println("COMPENSATE recovery id: " + recoveryId);
return Response.ok().build();
}
Now we are ready to enlist our LraResource
in the LRA. Restart the LRA service:
kill the previous process: Ctrl+C
build it again: mvn clean package
and run it: java -jar -Dthorntail.http.port=8081 -Dlra.http.port=8080 -jar target/lra-service-thorntail.jar
Execute the LRA again with: http :8081/lra/perform
. You will see that the
LRA was now cancelled because of the returned JAX-RS return code and the
@Compensate
method was called.
Congratulations, you’ve successfully started a new LRA, elisted a resource with it, and then cancelled it which triggered the compensating action of the enlisted resource.
In some cases, you might need to perform some form of clean up actions even
in the case the LRA is successfully finished (for instance, you must
remember the Order ID for the possible compensation). For this reason, the LRA
specification also provides a callback for successfull completition called
@Complete
:
@PUT
@Path("/complete")
@Complete
public Response complete(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("COMPLETING LRA " + lraId);
System.out.println("COMPLETE recovery id: " + recoveryId);
return Response.ok().build();
}
As you can see, the signature is almost identical to the Compensate callback.
Let’s now change the LRA operation to close the LRA successfully again:
@GET
@Path("/perform")
@LRA
public Response performLRA(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("PERFORMING LRA work " + lraId);
System.out.println("PERFORM recovery id: " + recoveryId);
return Response.ok(lraId.toString()).build();
}
Now you can again repeat reloading steps but when you execute the LRA you
will see that @Complete
method is called instead of @Compensate
.
So now we are in a system with a single microservice (LRA service) which is starting LRA, elisting with it, and then closing it. Since the MicroProfile is directed to be used in the microservices architecture, how can we propagate LRA to a different microservice?
Let’s create a new microservice by copying the one that we already have:
cp -a lra-service lra-service-2
Open a new terminal window and cd into the lra-service-2 directory. Open it
in your favourite IDE. First rename the artifactId
of the service in the
pom.xml
to lra-service-2
to differenciate this service.
Let’s now rename our LraResource
to ParticipantResource
to avoid confusion in naming moving on.
Now we are all set to propagate the LRA to the lra-service-2
. We just
need to call it from lra-service
.
Let’s move back to the lra-service
and LraResource
. Modify the performLRA
method:
@GET
@Path("/perform")
@LRA
public Response performLRA(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("PERFORMING LRA work " + lraId);
System.out.println("PERFORM recovery id: " + recoveryId);
// call lra-service-2
ClientBuilder.newClient().target("http://localhost:8082/lra/perform")
.request().get();
return Response.ok(lraId.toString()).build();
}
And that is all that’s need to be done. The implementation of LRA will
automatically detect outgoing JAX-RS call and add LRA id as a header parameter
"Long-Running-Action"
to the request. If you add this header yourself it
will be used instead. But if the LRA is not detected the LRA will include
the current active LRA it knows of which is sufficient for our example.
Now we can start our services:
In the first terminal run: mvn clean package && java -jar -Dthorntail.http.port=8081 -Dlra.http.port=8080 -jar target/lra-service-thorntail.jar
And in the second one run: mvn clean package && java -jar -Dthorntail.http.port=8082 -Dlra.http.port=8080 -jar target/lra-service-2-thorntail.jar
Now we have three services running:
-
LRA coordinator on port 8080
-
LRA service on port 8081
-
LRA service 2 on port 8082
So let’s excerise our microservices system by invoking the LRA service which start the new LRA and propagates it to the LRA service 2:
http :8081/lra/perform
Now you will see that the lra-service-2
also enlisted ParticipantResource
in the received LRA and Complete methods have been called on both services as
the LRA outcome closed successfully.
Note
|
now we are closing the LRA started in lra-service in the
lra-service-2 .
|
Let’s modify for the completness the lra-service-2
to fail with 412 to
cancel instead of close:
@Path("/lra")
public class ParticipantResource {
@GET
@Path("/perform")
@LRA
public Response performLRA(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("PERFORMING LRA work " + lraId);
System.out.println("PERFORM recovery id: " + recoveryId);
return Response.status(Response.Status.PRECONDITION_FAILED).build();
}
And recompile and restart lra-service-2
again (in the second terminal):
mvn clean package && java -jar -Dthorntail.http.port=8082 -Dlra.http.port=8080 -jar target/lra-service-2-thorntail.jar
And now when you execute the scenario again (http :8081/lra/perform
) you
will see that Compensate methods have been called on both services.
If you plan to continue with the advanced part of this tutorial please return
lra-service-2
back to returning 200:
@Path("/lra")
public class ParticipantResource {
@GET
@Path("/perform")
@LRA
public Response performLRA(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("PERFORMING LRA work " + lraId);
System.out.println("PERFORM recovery id: " + recoveryId);
return Response.ok(lraId.toString()).build();
}
Congratulations! We have covered all of the basic usage of the LRA and now you are ready to start using it in your services. Next sections will dive a little more into the detailed usage and tuning of the LRA.
So far we’ve covered three LRA states:
-
LRAStatus.Active
- an active LRA -
LRAStatus.Closed
- successfully closed LRA -
LRAStatus.Cancelled
- a successfully compensated LRA
However, there are also a few more:
-
LRAStatus.FailedToClose
- LRA couldn’t be fully closed -
LRAStatus.FailedToCancel
- LRA couldn’t be fully cancelled
These two states represent exceptional conditions in which one or more of the participants cannot perform their ending operations (Complete or Compensate). This state must be logged by the implementation and probably a manual interaction is required to resolve potentional conflicts. Also an implementor may choose to utilize some form of heuristics in these cases.
The last two LRA statuses are:
-
LRAStatus.Closing
- LRA is currently closing (calling Complete callbacks) -
LRAStatus.Cancelling
- LRA is currently cancelling (calling Compensate callbacks)
These two states represent intermediate states between LRA being asked to end and its actual end.
For long running Completitions or Compensations that would require a long periods of time to finish, the specification allows to return these progressive states from Complete or Compensate callbacks. This can be done in several ways but for our use-case it’s enought to return just 202 Accepted status code from the Complete or Compensate method. This will allow the implementation to know that it needs to replay ending phase for this particular participant again after some predefined timeout.
Let’s modify lra-service
:
private boolean accepted = true;
@PUT
@Path("/complete")
@Complete
public Response complete(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("COMPLETING LRA " + lraId);
System.out.println("COMPLETE recovery id: " + recoveryId);
return accepted ? Response.accepted().build() : Response.ok().build();
}
@GET
@Path("clearAccepted")
public void clearAccepted() {
accepted = false;
}
As you can see, if the end phase method return Closing or Cancelling (202) response this end phase method will be eventually called again so it must be idempotent.
Now you can recompile and restart services and replay the scenario again:
http :8081/lra/perform
but notice that if you wait for some time (by default it’s 2 minutes) the
Complete call at lra-service
will be called again. If you don’t want to
wait, you can trigger the recovery on LRA coordinator by a call:
http :8080/lra-recovery-coordinator/recovery
Now we need to actually finish our Complete operation (so it returns 200 instead). To do that invoke clearAccepted endpoint:
http :8081/lra/clearAccepted
and wait or replay recovery again:
http :8080/lra-recovery-coordinator/recovery
The call will now return an empty JSON array which means that the LRA is finished.
You can also verify that the LRA is ended by a call to http :8080/lra-coordinator
.
As we saw previously, when the end phase call cannot be completed immediately
the Complete or Compensate method will be called repeatedly so it must be
idempotent. If you can’t make it idempotent, the specification allows you
to specify a new method annotated with the @Status
annotation that will be
called when the implementation processes recovery instead.
Let’s add a @Status
method to lra-service
:
@GET
@Path("/status")
@Status
public Response status(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("STATUS FOR LRA " + lraId);
System.out.println("STATUS recovery id: " + recoveryId);
return accepted ? Response.accepted().build() : Response.ok(ParticipantStatus.Completed.name()).build();
}
Note
|
@Status method must be in JAX-RS case a GET JAX-RS endpoint.
|
And recompile and restart the scenario again.
Now you notice that first time when the LRA is asked to complete, the Complete
method is called at lra-service
. However, on the recovery (triggered by
timeout or manually) you can notice that Status method is called instead.
Note
|
Notice that we need to return ParticipantStatus from the Status method.
|
Since the participant may need to remember some information in case the
potential compensation is needed (e.g. the order id to know which order
needs to be cancelled) the MP LRA specification provides an annotation
called @Forget
that the participant may use to denote a method that
will be called when the LRA cannot be finished successfully (
FailedToClose
or FailedToCancel
states) to clean up no longer relevant
information.
Let’s add the @Forget
method to lra-service
:
@DELETE
@Path("/forget")
@Forget
public Response forget(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("FORGET FOR LRA " + lraId);
System.out.println("FORGET recovery id: " + recoveryId);
return Response.ok().build();
}
Note
|
@Forget method must be in JAX-RS case a DELETE JAX-RS endpoint.
|
And change the Status method to actually fail the close of the LRA by returning
ParticipantStatus.FailedToComplete
:
@GET
@Path("/status")
@Status
public Response status(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("STATUS FOR LRA " + lraId);
System.out.println("STATUS recovery id: " + recoveryId);
return accepted ? Response.accepted().build() : Response.ok(ParticipantStatus.FailedToComplete.name()).build();
}
You can notice now that when the LRA is finished (after accepted status is cleared) the Forget method is also called.
Now is the right time to investigate the @LRA
annotation in more detail. The
most important attribute of this annotation is value
parameter which is
setting the transactional type of the LRA executed in the annotatated method.
The possible values are:
-
REQUIRED
- default (and what we used up to this point). Starts a new LRA only if there is no LRA context (represented byLRA.LRA_HTTP_CONTEXT_HEADER
header) received in the invoking call. -
REQUIRES_NEW
- always starts a new LRA even if there is one received. -
MANDATORY
- must be called with LRA context otherwise it returns 412 Precondition Failed status code. -
SUPPORTS
- may be called with LRA context but doesn’t have to. -
NOT_SUPPORTED
- method will be executed without LRA context (LRA will be resumed after the method ends). -
NEVER
- if executed with the LRA context it returns 412 Precondition Failed. -
NESTED
- starts a new LRA which will be nested under the received context or a new LRA if no context is received.
For the example purposes we don’t need to invastigate individual LRA types in more detail but feel free to consult the specification text and JavaDoc for more details.
Another important attribute is called end
which is a boolean value
indicating whether the LRA should be ended (closed/cancelled) when the method
is finished. The default value is true
. Let’s experiment a little with
this attribute.
Update the lra-service
@LRA
method to this:
@GET
@Path("/perform")
@LRA(end = false)
public Response performLRA(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("PERFORMING LRA work " + lraId);
System.out.println("PERFORM recovery id: " + recoveryId);
// call lra-service-2
ClientBuilder.newClient().target("http://localhost:8082/lra/perform")
.request().get();
return Response.ok(lraId.toString()).build();
}
Warning
|
Also remove Status and Forget methods and accepted responses if you followed the previous part. |
And lra-service-2
@LRA
method like this if needed:
@GET
@Path("/perform")
@LRA(end = false)
public Response performLRA(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam(LRA.LRA_HTTP_RECOVERY_HEADER) URI recoveryId) {
System.out.println("PERFORMING LRA work " + lraId);
System.out.println("PERFORM recovery id: " + recoveryId);
return Response.ok(lraId.toString()).build();
}
Note
|
Note, that we are now returning status 200 OK because cancellation has a
priority over end = false .
|
And add another method to the lra-service
that will close the LRA:
@GET
@Path("/end")
@LRA(value = LRA.Type.MANDATORY)
public Response endLRA(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId) {
System.out.println("ENDING LRA " + lraId);
return Response.ok().build();
}
Now you can restart both services and replay the example scenario:
mvn clean package && java -jar -Dthorntail.http.port=8081 -Dlra.http.port=8080 -jar target/lra-service-thorntail.jar
mvn clean package && java -jar -Dthorntail.http.port=8082 -Dlra.http.port=8080 -jar target/lra-service-2-thorntail.jar
And call the lra-service
to start a new LRA but do not close it:
http :8081/lra/perform
You may notice that PERFORM calls have been executed but the Complete calls have not yet been delivered. You can also query the LRA coordinator directly to request all active LRAs that it knows of:
http :8080/lra-coordinator
This will return a JSON object with only one LRA which is still active. To close
the LRA we’ve started we need to call a method which has a value of end = true
(remember that it is a MANDATORY endpoint so we must pass the LRA we want
to close in LRA.LRA_HTTP_CONTEXT_HEADER
header):
http :8081/lra/end Long-Running-Action:http://localhost:8080/lra-coordinator/0_ffffc0a80066_3cb52bba_5da089cb_45
Note
|
Copy the LRA Id URI from the log of any of the services or from the
first call to the lra-service .
|
Now the LRA is closed and the Complete calls are received at both services.
The next attributes to mention deal with the conditions on which the LRA should be cancelled:
-
cancelOn
- HTTP response codes on which to cancel -
cancelOnFamily
- families of response codes on which to cancel (default are 4xx and 5xx)
These attributes, as the name says, specify the cancellation conditions. We already used them for our compensation examples so we don’t need to excercise them again.
The last attributes of @LRA
annotation are timeLimit
and timeUnit
which allow you to specify the timeout of the LRA after which it will
became eligible for cancellation. Again, example would be pretty
straitforward so we will not include it here.
Any LRA microservice (not necessarily a participant) can optionally enlist
for a notification which is received when the LRA is finished. This can be done
by annotating any method of the class that contains a different @LRA
annotated method with the @AfterLRA
annotation.
Let’s add an @AfterLRA
method to lra-service
:
@PUT
@Path("/after")
@AfterLRA
public Response after(@HeaderParam(LRA.LRA_HTTP_ENDED_CONTEXT_HEADER) URI endedLRA) {
System.out.println("AFTER_LRA ended LRA: " + endedLRA);
return Response.ok().build();
}
And replay any of the previous scenarios which finishes the started LRA. You will see that this method is invoked. This functionality may be used to, for instance, start a new LRA once another one ended or for the evidence of all LRAs that are being passed in the system.
This would be all for this tutorial the finished solution which followed these steps can be found in the End solution directory. Hopefully, you learned how to use MicroProfile LRA specification and you can start now using it in your microservices applications.
You can always query the all LRAs that the LRA coordinator knows of by a call:
http :8080/lra-coordinator
which return JSON array of LRAs. If you get stuck just try to restart the individual services and LRA coordinator. Otherwise, please raise an issue.