Introduction
In my last post I showed how to run a Long Running Action (LRA) within a single JAX-RS resource method using quarkus features to build and run the application. I showed how to create and start an LRA coordinator and then generated a basic hello
application, showing how to modify the application to run with a long running action (by adding dependencies on the org.eclipse.microprofile.lra:microprofile-lra-api
and org.jboss.narayana.rts:narayana-lra
artifacts, which together provide annotations for controlling the lifecycle of LRAs). That post also includes links to the the LRA specification and to the javadoc for the annotation API.
In this follow up post I will indicate how to include a second resource in the LRA. To keep things interesting I’ll deploy the second resource to another microservice and use quarkus’s MicroProfile Rest Client support to implement the remote service invocations. The main difference between this example and the one I developed in the earlier post, apart from the technicalities of using Rest Client, is that we will set the LRA.end attribute to false
in the remote service so that the LRA will remain active when the call returns. In this way the initiating service method has the option of calling other microservices before ending the LRA.
Creating and starting an LRA coordinator
LRA relies on a coordinator to manage the lifecycle of LRAs so you will need one to be running for this demo to work successfully. The previous post showed how to build and run coordinators. Alternatively, download or view some scripts which execute all of the steps required in the current post and it includes a shell script called coordinator.sh which will build a runnable coordinator jar (it’s fairly simple and short so you can just read it and create your own jar or just run it as is).
Generate a project for booking tickets
Since the example will be REST based, include the resteasy
and rest-client
extensions (on line 6 next):
1: mvn io.quarkus:quarkus-maven-plugin:2.0.1.Final:create \ 2: -DprojectGroupId=org.acme \ 3: -DprojectArtifactId=ticket \ 4: -DclassName="org.acme.ticket.TicketResource" \ 5: -Dpath="/tickets" \ 6: -Dextensions="resteasy,rest-client" 7: cd ticket
You will need the mvn
program to run the plugin (but the generated projects will include the mvnw
maven wrapper).
Modify the generated TicketResource.java
source file to add Microprofile LRA support. The changes that you will need for LRA are on lines 26 and 27. Line 26 says that the bookTicket
method must run with an LRA (if one is not present when the method is invoked then one will be automatically created). Note that we have set the end
attribute to false to stop the LRA from being automatically closed when the method finishes. By keeping the LRA active when the ticket is booked, the caller can invoke other services in the context of the same LRA. Most services will require the LRA context for tracking updates which typically will be useful for knowing which actions to compensate for if the LRA is later cancelled: the context is injected as a JAX-RS method parameter on line 27.
You will also need to include callbacks for when the LRA is later closed or cancelled (the methods are defined on lines 37 and line 46, respectively).
1: package org.acme.ticket; 2: 3: import static javax.ws.rs.core.MediaType.APPLICATION_JSON; 4: 5: // import annotation definitions 6: import org.eclipse.microprofile.lra.annotation.ws.rs.LRA; 7: import org.eclipse.microprofile.lra.annotation.Compensate; 8: import org.eclipse.microprofile.lra.annotation.Complete; 9: // import the definition of the LRA context header 10: import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER; 11: 12: // import some JAX-RS types 13: import javax.ws.rs.GET; 14: import javax.ws.rs.PUT; 15: import javax.ws.rs.Path; 16: import javax.ws.rs.Produces; 17: import javax.ws.rs.core.Response; 18: import javax.ws.rs.HeaderParam; 19: 20: @Path("/tickets") 21: @Produces(APPLICATION_JSON) 22: public class TicketResource { 23: 24: @GET 25: @Path("/book") 26: @LRA(value = LRA.Type.REQUIRED, end = false) // an LRA will be started before method execution if none exists and will not be ended after method execution 27: public Response bookTicket(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) { 28: System.out.printf("TicketResource.bookTicket: %s%n", lraId); 29: String ticket = "1234" 30: return Response.ok(ticket).build(); 31: } 32: 33: // ask to be notified if the LRA closes: 34: @PUT // must be PUT 35: @Path("/complete") 36: @Complete 37: public Response completeWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) { 38: System.out.printf("TicketResource.completeWork: %s%n", lraId); 39: return Response.ok().build(); 40: } 41: 42: // ask to be notified if the LRA cancels: 43: @PUT // must be PUT 44: @Path("/compensate") 45: @Compensate 46: public Response compensateWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) { 47: System.out.printf("TicketResource.compensateWork: %s%n", lraId); 48: return Response.ok().build(); 49: } 50: }
Skip the tests:
rm src/test/java/org/acme/ticket/*
Add dependencies on microprofile-lra-api
and narayana-lra
to the pom to include the MicroProfile LRA annotations and the narayana implementation of them so that the LRA context will be propagated during interservice communications:
<dependencies> <dependency> <groupId>org.eclipse.microprofile.lra</groupId> <artifactId>microprofile-lra-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.jboss.narayana.rts</groupId> <artifactId>narayana-lra<\/artifactId> <version>5.12.0.Final</version> </dependency>
We are creating ticket and trip microservices so they need to listen on different ports, configure the ticket service to run on port 8081
:
1: quarkus.arc.exclude-types=io.narayana.lra.client.internal.proxy.nonjaxrs.LRAParticipantRegistry,io.narayana.lra.filter.ServerLRAFilter,io.narayana.lra.client.internal.proxy.nonjaxrs.LRAParticipantResource 2: quarkus.http.port=8081 3: quarkus.http.test-port=8081
The excludes are pulled in by the org.jboss.narayana.rts:narayana-lra
maven dependency. As mentioned in my previous post this step will not be necessary when the pull request for the io.quarkus:quarkus-narayana-lra
extension is approved.
Now build and test the ticket service, making sure that you have already started a coordinator as described in the previous blog (or you can use the shell scripts linked above):
./mvnw clean package -DskipTests # skip tests java -jar target/quarkus-app/quarkus-run.jar & # run the application in the background curl http://localhost:8081/tickets/book TicketResource.bookTicket: http://localhost:8080/lra-coordinator/0_ffffc0a8000e_8b2b_60f6a8d4_2 1234
The bookTicket()
method prints the method name and the id of the active LRA followed by the hard-coded booking id 1234
.
Generate a project for booking trips
Now create a second microservice which will be used for booking trips. It will invoke other microservices to complete trip bookings. In order to simplify the example there is just the single remote ticket service involved in the booking process.
First generate the project. Like the ticket service, the example will be REST based so include the resteasy
and rest-client
extensions:
mvn io.quarkus:quarkus-maven-plugin:2.0.1.Final:create \ -DprojectGroupId=org.acme \ -DprojectArtifactId=trip \ -DclassName="org.acme.trip.TripResource" \ -Dpath="/trips" \ -Dextensions="resteasy,rest-client" cd trip
The rest-client
extension includes support for MicroProfile REST Client which we shall use to perform the remote REST invocations from the trip to the ticket service. For REST Client we need a TicketService
and we need to register it as shown on line 12 of the following listing:
1: package org.acme.trip; 2: 3: import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; 4: 5: import javax.ws.rs.GET; 6: import javax.ws.rs.Path; 7: import javax.ws.rs.Produces; 8: import javax.ws.rs.core.MediaType; 9: 10: @Path("/tickets") 11: @Produces(MediaType.APPLICATION_JSON) 12: @RegisterRestClient 13: public interface TicketService { 14: 15: @GET 16: @Path("/book") 17: String bookTicket(); 18: }
Let’s also create a TripService
and inject an instance of the TicketService
into it, marking it with the @RestClient
annotation on line 11. The quarkus rest client support will configure this injected instance such that it will perform remote REST calls to the ticket service (the remote endpoint for the ticket service will be configured below in the application.properties
file):
1: package org.acme.trip; 2: 3: import org.eclipse.microprofile.rest.client.inject.RestClient; 4: import javax.enterprise.context.ApplicationScoped; 5: import javax.inject.Inject; 6: 7: @ApplicationScoped 8: public class TripService { 9: 10: @Inject 11: @RestClient 12: TicketService ticketService; 13: 14: String bookTrip() { 15: return ticketService.bookTicket(); // only one service will be used for the trip booking 16: 17: // if other services need to be part of the trip they would be called here 18: // and the TripService would associate each step of the booking with the id of the LRA 19: // (although I've not shown it being passed in this example) and that would form the 20: // basis of the ability to compensate or clean up depending upon the outcome. 21: // We may include a more comprehensive/realistic example in a later blog. 22: } 23: }
And now we can inject an instance of this service into the generated TripResource (src/main/java/org/acme/trip/TripResource.java
) on line 26. I have also annotated the bookTrip()
method with an LRA annotation so that a new LRA will be started before the method is started (if one wasn’t already present) and I have added @Complete
and @Compensate
callback methods (these will be called when the LRA closes or cancels, respectively):
1: package org.acme.trip; 2: 3: import javax.inject.Inject; 4: import javax.ws.rs.GET; 5: import javax.ws.rs.Path; 6: import javax.ws.rs.Produces; 7: import javax.ws.rs.core.Response; 8: 9: import static javax.ws.rs.core.MediaType.APPLICATION_JSON; 10: 11: // import annotation definitions 12: import org.eclipse.microprofile.lra.annotation.ws.rs.LRA; 13: import org.eclipse.microprofile.lra.annotation.Compensate; 14: import org.eclipse.microprofile.lra.annotation.Complete; 15: // import the definition of the LRA context header 16: import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER; 17: 18: // import some JAX-RS types 19: import javax.ws.rs.PUT; 20: import javax.ws.rs.HeaderParam; 21: 22: @Path("/trips") 23: @Produces(APPLICATION_JSON) 24: public class TripResource { 25: 26: @Inject 27: TripService service; 28: 29: // annotate the hello method so that it will run in an LRA: 30: @GET 31: @LRA(LRA.Type.REQUIRED) // an LRA will be started before method execution and ended after method execution 32: @Path("/book") 33: public Response bookTrip(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) { 34: System.out.printf("TripResource.bookTrip: %s%n", lraId); 35: String ticket = service.bookTrip(); 36: return Response.ok(ticket).build(); 37: } 38: 39: // ask to be notified if the LRA closes: 40: @PUT // must be PUT 41: @Path("/complete") 42: @Complete 43: public Response completeWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) { 44: System.out.printf("TripResource.completeWork: %s%n", lraId); 45: return Response.ok().build(); 46: } 47: 48: // ask to be notified if the LRA cancels: 49: @PUT // must be PUT 50: @Path("/compensate") 51: @Compensate 52: public Response compensateWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) { 53: System.out.printf("TripResource.compensateWork: %s%n", lraId); 54: return Response.ok().build(); 55: } 56: }
For the blog we can skip the tests:
rm src/test/java/org/acme/trip/*
Configure the trip service to listen on port 8082
(line 2). Also configure the remote ticket endpoint as required by the MicroProfile REST Client specification (line 5):
1: quarkus.arc.exclude-types=io.narayana.lra.client.internal.proxy.nonjaxrs.LRAParticipantRegistry,io.narayana.lra.filter.ServerLRAFilter,io.narayana.lra.client.internal.proxy.nonjaxrs.LRAParticipantResource 2: quarkus.http.port=8082 3: quarkus.http.test-port=8082 4: 5: org.acme.trip.TicketService/mp-rest/url=http://localhost:8081 6: org.acme.trip.TicketService/mp-rest/scope=javax.inject.Singleton
Add dependencies on microprofile-lra-api
and narayana-lra
to the pom to include the MicroProfile LRA annotations and the narayana implementation of them so that the application can request that the LRA context be propagated during interservice communications:
<dependency> <groupId>org.eclipse.microprofile.lra</groupId> <artifactId>microprofile-lra-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.jboss.narayana.rts</groupId> <artifactId>narayana-lra</artifactId> <version>5.12.0.Final</version> </dependency>
and finally, build and run the microservice:
./mvnw clean package -DskipTests java -jar target/quarkus-app/quarkus-run.jar &
Use curl
to book a trip. The HTTP GET request to the trips/book
endpoint is handled by the trip service bookTrip()
method and it then invokes the ticket
service to book a ticket.
When the bookTrip()
method finishes the LRA will be closed (since the default value for the LRA.end
attribute is true
), triggering calls to the service @Complete
methods of the two services:
curl http://localhost:8082/trips/book TripResource.bookTrip: http://localhost:8080/lra-coordinator/0_ffffc0a8000e_8b2b_60f6a8d4_52c TicketResource.bookTrip: http://localhost:8080/lra-coordinator/0_ffffc0a8000e_8b2b_60f6a8d4_52c TripResource.completeWork: http://localhost:8080/lra-coordinator/0_ffffc0a8000e_8b2b_60f6a8d4_52c TicketResource.bookTrip: http://localhost:8080/lra-coordinator/0_ffffc0a8000e_8b2b_60f6a8d4_52c TicketResource.completeWork: http://localhost:8080/lra-coordinator/0_ffffc0a8000e_8b2b_60f6a8d4_52c 1234
Notice the output shows the bookTrip and bookTicket methods being called and also notice that the @Complete
methods of both services (completeWork()
) were called.
The id
of the LRA on all calls should be the same value as shown in the example output, this is worthwhile noting since the completion and compensation methods will typically use it in order to determine which actions it should clean up for or compensate for when the LRA closes or cancels.
Not shown here, but if there was a problem booking the ticket then the ticket service should return a JAX-RS status code (4xx
and 5xx
HTTP codes by default) that triggers the cancellation of the LRA, and this would then cause the @Compensate
methods of all services involved in the LRA to be invoked.