Software project: Creating a real-life transactional microservices application on Kubernetes

Mika Rinne, Oracle EMEA
7 min readJan 22, 2024

--

Part 5: Writing the code to end the LRA

In Part 4 I covered how to create the LRA with Gymuser and Gyminstructor microservices as LRA participants. Now let’s cover some code what happens when either of the participants ends the LRA.

The Gymapp use case to create the LRA is when Gymuser attends a class created by Gyminstructor. To end the LRA the following use cases are involved:

  • Gyminstructor closes the class as held/completed
  • Gymuser decides to cancel his/her class attendance and money is returned
  • Gyminstructor decides to cancel his/her gym class and money is returned to all attendees (Gymusers)

Gyminstructor can also confirm/unconfirm a class but here LRA is not involved as it is just informational to Gymusers.

The first use case of Gyminstructor closing a class as held is consists of the following actions:

  • Find all signups for the Instructor class to complete the LRA and update the status as “held” (or “done”) for each
  • Update the Instructor class status by ID as “held”

To do this the code of the Gyminstructor REST API (http POST) in InstructorclassResource.java method is below.

I will cover the Authenticated annotation in the part to add security to the Gymapp but for now let’s just assume the context header param contains a valid JWT access token of the Gyminstructor (coming from the UI) to make the REST API call and the call will fail if JWT does not exist.

@Authenticated
@POST
@Path("/complete/{id}")
@Produces(MediaType.APPLICATION_JSON)
@Transactional(Transactional.TxType.REQUIRED)
public Response completeInstructorclass(@Context SecurityContext context,
@HeaderParam("Authorization") String auth,
@PathParam("id") Long id) {
Optional<Principal> userPrincipal = context.userPrincipal();
String userEmail = userPrincipal.get().getName();
Instructorclass instructorclass = entityManager.find(Instructorclass.class, id);
if (instructorclass == null) {
throw new NotFoundException("Unable to find/complete gym class with id " + id);
}
if(instructorclass.getInstructorEmail().trim().compareTo(userEmail.trim()) != 0) {
System.out.println("User does not match to the owner of the class:" + instructorclass.getInstructorEmail() + ", " + userEmail);
return Response.serverError().build();
}
//Complete all signups for this class
try {
Response signUpsResponse = ClientBuilder.newClient()
.target("http://gyminstructor:8081")
.path("signups/" + id)
.request()
.header(HttpHeaders.AUTHORIZATION, auth)
.get();
if(signUpsResponse.getStatus() == Response.Status.OK.getStatusCode())
{
String json = signUpsResponse.readEntity(String.class);
if(json.length() > 0)
{
ObjectMapper objectMapper = new ObjectMapper();
ClassAttendance[] signups = objectMapper.readValue(json, ClassAttendance[].class);
for (ClassAttendance signup : signups) {
Response completeSignUpResponse = ClientBuilder.newClient()
.target("http://gyminstructor:8081")
.path("signups/complete/" + signup.getId())
.request()
.header(HttpHeaders.AUTHORIZATION, auth)
.header(LRA.LRA_HTTP_CONTEXT_HEADER, new URI(signup.getLra()))
.post(null);
if(completeSignUpResponse.getStatus() == Response.Status.OK.getStatusCode())
{
System.out.println("Signup completed, ID:" + signup.getId() + ", LRA: " + signup.getLra());
} else {
System.out.println("Signup completing ERROR:" + completeSignUpResponse.getStatusInfo());
return Response.serverError().build();
}
}
}
} else {
System.out.println("get signUps ERROR: " + signUpsResponse.getStatusInfo());
return Response.serverError().build();
}
} catch(Exception e)
{
e.printStackTrace();
return Response.serverError().build();
}
instructorclass.setStatus(5);
entityManager.persist(instructorclass);
return Response.ok().build();
}

In the code above first the owner of the Instructorclass is matched to the principal of the JWT and to make sure Gyminstructor can only update his/her own classes.

Then we get all signups for the class and looping them thru update each signup as “held” involving the LRA and finally update the Instructorclass status as “held”. To do this InstructorclassResource.java calls two other REST APIs that are part of the Gyminstructor itself over http.

Here’s the code in ClassAttendanceResource.java to complete a single signup using it’s ID below. This involves the LRA that is required as a header param in the REST API (http POST):

@Authenticated
@POST
@Path("/complete/{id}")
@LRA(value = LRA.Type.MANDATORY, end = true)
@Produces(MediaType.APPLICATION_JSON)
@Transactional(Transactional.TxType.REQUIRED)
public Response completeSignup(@Context SecurityContext context,
@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@PathParam("id") Long id) {
ClassAttendance classAttendance = entityManager.find(ClassAttendance.class, id);
if (classAttendance == null) {
return Response.serverError().build();
}
Optional<Principal> userPrincipal = context.userPrincipal();
String userEmail = userPrincipal.get().getName();
Instructorclass instructorclass = entityManager.find(Instructorclass.class,classAttendance.getInstructorclassid());
if(instructorclass.getInstructorEmail().trim().compareTo(userEmail.trim()) != 0) {
System.out.println("User does not match to the owner of the class:" + instructorclass.getInstructorEmail() + ", " + userEmail);
return Response.serverError().build();
}
classAttendance.setStatus(5);
entityManager.persist(classAttendance);
return Response.ok().build();
}

In the code above again the owner of the Instructor is matched to the principal of the JWT and to make sure Gyminstructor can only a signup update for his/her own class. If the owner is matched then it simply updates the status of the signup as held.

The LRA is required to be present per the annotation and the call fails it is not (value = LRA.Type.MANDATORY) and it also tells the LRA coordinator to end the LRA (end = true):

@LRA(value = LRA.Type.MANDATORY, end = true)

This annotation makes the coordinator to to call the complete method in Gymapp microservices, the Gymuser and the Gyminstructor.

The LRA complete code for the signup is very simple in Gyminstructor ClassAttendanceResource.java:

@Complete
public Response complete(URI lraId) {
return Response.ok(ParticipantStatus.Completed.name()).build();
}

Gymuser’s MyclassResource.java has a bit more logic to update the class status to “held”:

@Complete
@Transactional(Transactional.TxType.REQUIRED)
public Response complete(URI lraId) {
TypedQuery<Myclass> query = entityManager.createNamedQuery("getMyclassByLra", Myclass.class);
Myclass myclass = query.setParameter("lra", lraId.toString()).getSingleResult();
if (myclass == null) {
System.out.println("COMPLETE ERROR: Unable to complete myclass with lra " + lraId);
return Response.serverError().build();
} else {
myclass.setStatus(5);
entityManager.persist(myclass);
return Response.ok(ParticipantStatus.Completed.name()).build();
}
}

In no errors occur and both methods above return Response.ok(ParticipantStatus.Completed.name()).build() the LRA is closed by the coordinator.

The second use case Gymuser to cancelling his/her class attendance involves money being refunded to Gymuser in PayPal®. Here is REST API (http DELETE) code in MyClassResource.java:

@Authenticated
@DELETE
@Path("/{id}")
@LRA(value = LRA.Type.MANDATORY, end = true, cancelOn = Response.Status.OK)
@Produces(MediaType.APPLICATION_JSON)
@Transactional(Transactional.TxType.REQUIRED)
public Response cancelMyclass(@Context SecurityContext context,
@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@PathParam("id") Long id) {
Optional<Principal> userPrincipal = context.userPrincipal();
String userEmail = userPrincipal.get().getName();
Myclass myclass = entityManager.find(Myclass.class, id);
if (myclass == null) {
System.out.println("Myclass not found, id=" + id);
return Response.serverError().build();
}
if(myclass.getUserEmail().trim().compareTo(userEmail.trim()) != 0) {
System.out.println("User does not match to the owner of the class:" + myclass.getUserEmail() + ", " + userEmail);
return Response.serverError().build();
}
myclass.setStatus(8);
myclass.setSelfCancelled(1);
entityManager.persist(myclass);
return Response.ok().build();
}

In the Gymapp this REST API is call is done directly in JavaScript from the Gymuser client UI that I will cover in more detail the next part.

Like before the owner of the class is matched to the JWT principal to make sure Gymuser has the ownership to the Myclass being cancelled and unless matching the call fails. The LRA is passed as a header param along with the JWT as auth (LRA is one the of the Myclass fields in the database):

function cancelAttend(id, lraid) {
let auth = 'Bearer ' + data.access_token;
let axiosInstance = axios.create({
headers: {
'Long-Running-Action' : `${lraid}`,
'Authorization' : `${auth}`
}
});
axiosInstance
.delete('/classes/' + id)
.then(response =>
{
data.showCancelAttendMessage = 1;
getMyclasses(); // immediate refresh of refresh classes
getInstructorclasses(); // immediate refresh of instructor classes
}
).catch(error => {
console.log(error);
data.showSystemErrorMessage = 1;
}).finally(() => {
})
}

The following annotation in the MyclassResource.java DELETE REST API as previously shown

@LRA(value = LRA.Type.MANDATORY, end = true, cancelOn = Response.Status.OK)

makes the coordinator to call Myclass to compensate REST API i.e. refund the Gymuser’s payment involved:

@Compensate
@Transactional(Transactional.TxType.REQUIRED)
public Response compensate(URI lraId) {
TypedQuery<Myclass> query = entityManager.createNamedQuery("getMyclassByLra", Myclass.class);
Myclass myclass = query.setParameter("lra", lraId.toString()).getSingleResult();
if(myclass.getPayment() != null && myclass.getPayment().length() > 0)
{
// Calling here PayPal Refund REST API
// String refundResponseJson is instantiated

myclass.setPaymentRefund(refundResponseJson);
if(refundResponse.getStatus() != Response.Status.OK.getStatusCode())
{
myclass.setStatus(10); // Status payment cancelled
} else {
System.out.println("COMPENSATE ERROR: PayPal refund error is not allowed, lra " + lraId);
myclass.setStatus(11); // Status Error
entityManager.persist(myclass);
return Response.serverError().build();
}
} else {
System.out.println("No payment to compensate, lra:" + lraId);
myclass.setStatus(8); // Status cancelled
}
entityManager.persist(myclass);
return Response.ok(ParticipantStatus.Compensated.name()).build();
}

Coordinator also makes Gyminstructor to compensate the Myclass matching signup in ClassAttendanceResource.java to update the status:

@Compensate
@Transactional(Transactional.TxType.REQUIRED)
public Response compensate(URI lraId) {
TypedQuery<ClassAttendance> query = entityManager.createNamedQuery("getSignupByLra", ClassAttendance.class);
ClassAttendance classAttendance = query.setParameter("lra", lraId.toString()).getSingleResult();
if (classAttendance == null) {
return Response.serverError().build();
}
classAttendance.setStatus(8);
entityManager.persist(classAttendance);
return Response.ok(ParticipantStatus.Compensated.name()).build();
}

If no errors occur money is refunded to the Gymuser and coordinator ends the LRA.

The third use case of Gyminstructor cancelling a class is a combination of the two previously:

  • Find all signups for the Instructor class to compensate the LRA and update the status as “cancelled” for each. This will refund money to all class attending Gymusers
  • Update the Instructor class status by ID as “cancelled”

Here’s the REST API (http DELETE) for the Gyminstructor class in Instructorclass.java:

@Authenticated
@DELETE
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
@Transactional(Transactional.TxType.REQUIRED)
public Response cancelInstructorclass(@Context SecurityContext context,
@HeaderParam("Authorization") String auth,
@PathParam("id") Long id) {
Optional<Principal> userPrincipal = context.userPrincipal();
String userEmail = userPrincipal.get().getName();
if(!isAdmin(userEmail))
{
throw new BadRequestException("Unable to cancel instructor class");
}
Instructorclass instructorclass = entityManager.find(Instructorclass.class, id);
if (instructorclass == null) {
throw new NotFoundException("Unable to cancel gym class with id " + id);
}
if(instructorclass.getInstructorEmail().trim().compareTo(userEmail.trim()) != 0) {
System.out.println("User does not match to the owner of the class:" + instructorclass.getInstructorEmail() + ", " + userEmail);
return Response.serverError().build();
}
//Cancel all signups for this class
try {
Response signUpsResponse = ClientBuilder.newClient()
.target("http://gyminstructor:8081")
.path("signups/" + id)
.request()
.header(HttpHeaders.AUTHORIZATION, auth)
.get();
if(signUpsResponse.getStatus() == Response.Status.OK.getStatusCode())
{
String json = signUpsResponse.readEntity(String.class);
if(json.length() > 0)
{
ObjectMapper objectMapper = new ObjectMapper();
ClassAttendance[] signups = objectMapper.readValue(json, ClassAttendance[].class);
for (ClassAttendance signup : signups) {
Response cancelSignUpResponse = ClientBuilder.newClient()
.target("http://gyminstructor:8081")
.path("signups/" + signup.getId())
.request()
.header(HttpHeaders.AUTHORIZATION, auth)
.header(LRA.LRA_HTTP_CONTEXT_HEADER, new URI(signup.getLra()))
.delete();
if(cancelSignUpResponse.getStatus() == Response.Status.OK.getStatusCode())
{
System.out.println("Signup cancelled, ID:" + signup.getId() + ", LRA: " + signup.getLra());
} else {
System.out.println("Signup cancelling ERROR:" + cancelSignUpResponse.getStatusInfo());
return Response.serverError().build();
}
}
}
} else {
System.out.println("get signUps ERROR: " + signUpsResponse.getStatusInfo());
return Response.serverError().build();
}
} catch(Exception e)
{
e.printStackTrace();
return Response.serverError().build();
}
instructorclass.setStatus(8);
entityManager.persist(instructorclass);
return Response.ok().build();
}

This will make the coordinator to call Myclass to compensate signup LRA matching Myclasses that will then refund the payment to each of the class attending Gymusers as shown earlier in Myclass compensate REST API.

If no errors occur coordinator ends the LRA.

In the next Part 6 I will cover the programming of the Gymapp UI and adding security using Oracle IDCS.

--

--