Repository: eclipse/microprofile-lra
Pull request #150 (2019-05-01 20:22:40)

issue149 Examples of async usage
#149 

One of my "reactive" colleagues reported that the spec was not clear about whether or not reactive was supported in LRA. This PR adds a [new section to the spec](https://github.com/eclipse/microprofile-lra/pull/150/files#diff-06aca14982a5ee1e4547e9ec14b41bbbR735) and [3 TCK tests](https://github.com/eclipse/microprofile-lra/pull/150/files#diff-691a384040bd56b139d108d6d1a64cb9R213) that showcase reactive support.


Patch:
diff --git a/api/pom.xml b/api/pom.xml
index de6cc3d..329658d 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -29,7 +29,7 @@
     <description>Eclipse MicroProfile LRA Feature - API</description>
 
     <properties>
-        <version.jaxrs-api>2.0</version.jaxrs-api>
+        <version.jaxrs-api>2.1</version.jaxrs-api>
         <version.cdi-api>1.0-SP1</version.cdi-api>
     </properties>
 
diff --git a/spec/src/main/asciidoc/microprofile-lra-spec.adoc b/spec/src/main/asciidoc/microprofile-lra-spec.adoc
index a58ecd1..742af9e 100644
--- a/spec/src/main/asciidoc/microprofile-lra-spec.adoc
+++ b/spec/src/main/asciidoc/microprofile-lra-spec.adoc
@@ -560,6 +560,7 @@ The forget method MUST be annotated with the JAX-RS `@DELETE` annotation.
 If there is no `@Status` then the `@Compensate` or `@Complete` methods will continue
 to be invoked until the implementation knows it has the final status.
 
+[[async-compensators]]
 If the resource cannot perform a compensation activity
 immediately the `@Compensate` method SHOULD do one or more of the following:
 
@@ -731,6 +732,107 @@ Only when the implementation is certain that participant has finished
 will it tell it that it is okay to release any resources it associated
 with the LRA.
 
+[[reactive-support]]
+==== Reactive Support
+
+Implementations are expected to operate correctly when services use the
+asychronous and reactive features provided by JAX-RS.
+
+It has already been noted that
+<<async-compensators,participant completion and compensation callbacks>>
+can execute asynchronously but the same must also be true for the business
+methods that execute in the context of an LRA. It is the responsibility
+of the implementation to ensure that JAX-RS asynchronous features
+continue to behave in the presence of these LRAs. The following
+example shows a resource invocation that runs in the context of a
+long running action and uses a Java 8 completion stage to return
+an asynchronous response:
+
+[source,java]
+----
+    @LRA(value = LRA.Type.REQUIRED, // the method must run with an LRA
+         end = true) // the LRA must end when the method completes
+    @Path("completion-stage-lra")
+    public CompletionStage<Response> asyncInvocationWithLRA(
+        @HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) { // the id of LRA
+
+        ExecutorService es = Executors.newSingleThreadExecutor();
+        final CompletableFuture<Response> response = new CompletableFuture<>();
+
+        try {
+            es.submit(() -> {
+                // excecute a long running business activity and resume when done
+                ...
+                response.complete(Response.ok().entity(lraId).build());
+                // since the response is complete the implemention will
+                // close the LRA thereby invoking any registered participant
+                // completion handlers
+            });
+        } finally {
+            es.shutdown();
+        }
+
+        return response;
+    }
+----
+
+With completion stages it is also possible to complete exceptionally. The
+following example should run business logic asynchronously in the context
+of an LRA but the LRA should be cancelled: forcing any registered
+participant compensation handlers to run:
+
+[source,java]
+----
+    @LRA(value = LRA.Type.REQUIRED, // the method must run with an LRA
+         end = true, // the LRA must end when the method completes
+         cancelOn = {Response.Status.NOT_FOUND}) // cancel on a 404 code
+    @Path("completion-stage-exceptionally-lra")
+    public CompletionStage<Response> asyncInvocationWithException(
+        @HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) {
+
+        ExecutorService es = Executors.newSingleThreadExecutor();
+        final CompletableFuture<Response> response = new CompletableFuture<>();
+
+        try {
+            es.submit(() -> {
+                // excecute long running business activity finishing with a NOT_FOUND error
+                // which causes the LRA to cancel
+                response.completeExceptionally(
+                        new WebApplicationException(
+                            Response.status(Response.Status.NOT_FOUND).entity(lraId).build()));
+            });
+        } finally {
+            es.shutdown();
+        }
+
+        return response;
+    }
+----
+
+In addition to the use of completion stages, a resource method may also produce
+asynchronous responses by injecting a JAX-RS `AsyncResponse` parameter using
+the JAX-RS `@Suspended` annotation:
+
+[source,java]
+----
+    @LRA(value = LRA.Type.REQUIRED, // the method must run with an LRA
+         end = true) // the LRA must end when the method completes
+    @Path("asyncresponse-lra")
+    public void asyncResponseLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId,
+                         final @Suspended AsyncResponse ar) {
+        ExecutorService es = Executors.newSingleThreadExecutor();
+
+        try {
+            es.submit(() -> {
+                // excecute long running business activity and resume when done
+                ar.resume(Response.ok().entity(lraId).build());
+            });
+        } finally {
+            es.shutdown();
+        }
+    }
+----
+
 [[failure-resilience]]
 ==== Recovery Requirements
 
diff --git a/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/ContextTckResource.java b/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/ContextTckResource.java
index 3924e53..3ea40dc 100644
--- a/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/ContextTckResource.java
+++ b/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/ContextTckResource.java
@@ -39,12 +39,18 @@ import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.client.ClientBuilder;
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.client.Invocation;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import java.util.AbstractMap;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -67,6 +73,10 @@ public class ContextTckResource {
     // resource path for testing that context on outbound and inbound calls made from an
     // method annotated with @LRA are spec compliant
     public static final String CONTEXT_CHECK_LRA_PATH = "/context-check-lra";
+    public static final String ASYNC_LRA_PATH1 = "async-response-lra";
+    public static final String ASYNC_LRA_PATH2 = "completion-stage-lra";
+    public static final String ASYNC_LRA_PATH3 = "completion-stage-exceptionally-lra";
+
     public static final String LEAVE_PATH = "/leave";
     // resource path for reading and writing the participant status
     public static final String STATUS_PATH = "/status";
@@ -243,6 +253,63 @@ public class ContextTckResource {
         return Response.ok().entity(lraId).build();
     }
 
+    @LRA(value = LRA.Type.REQUIRED)
+    @PUT
+    @Path(ASYNC_LRA_PATH1)
+    public void async1LRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId,
+                         final @Suspended AsyncResponse ar) {
+        ExecutorService es = Executors.newSingleThreadExecutor();
+
+        try {
+            es.submit(() -> {
+                // excecute long running business activity and resume when done
+                ar.resume(Response.ok().entity(lraId).build());
+            });
+        } finally {
+            es.shutdown();
+        }
+    }
+
+    @LRA(value = LRA.Type.REQUIRED)
+    @PUT
+    @Path(ASYNC_LRA_PATH2)
+    public CompletionStage<Response> async2LRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) {
+        ExecutorService es = Executors.newSingleThreadExecutor();
+        final CompletableFuture<Response> response = new CompletableFuture<>();
+
+        try {
+            es.submit(() -> {
+                // excecute long running business activity and resume when done
+                response.complete(Response.ok().entity(lraId).build());
+            });
+        } finally {
+            es.shutdown();
+        }
+
+        return response;
+    }
+
+    @LRA(value = LRA.Type.REQUIRED)
+    @PUT
+    @Path(ASYNC_LRA_PATH3)
+    public CompletionStage<Response> async3LRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) {
+        ExecutorService es = Executors.newSingleThreadExecutor();
+        final CompletableFuture<Response> response = new CompletableFuture<>();
+
+        try {
+            es.submit(() -> {
+                // excecute long running business activity finishing with an error
+                // code of NOT_FOUND which causes the LRA to cancel
+                response.completeExceptionally(
+                        new WebApplicationException(Response.status(Response.Status.NOT_FOUND).entity(lraId).build()));
+            });
+        } finally {
+            es.shutdown();
+        }
+
+        return response;
+    }
+
     /**
      * Query the current value of a metric in the context of an LRA
      * @param lraId the LRA that owns the metric


Test patch:
diff --git a/tck/src/main/java/org/eclipse/microprofile/lra/tck/TckContextTests.java b/tck/src/main/java/org/eclipse/microprofile/lra/tck/TckContextTests.java
index 98d8525..9fa6c83 100644
--- a/tck/src/main/java/org/eclipse/microprofile/lra/tck/TckContextTests.java
+++ b/tck/src/main/java/org/eclipse/microprofile/lra/tck/TckContextTests.java
@@ -19,6 +19,7 @@
  *******************************************************************************/
 package org.eclipse.microprofile.lra.tck;
 
+import org.eclipse.microprofile.lra.annotation.Compensate;
 import org.eclipse.microprofile.lra.annotation.Complete;
 import org.eclipse.microprofile.lra.annotation.Forget;
 import org.eclipse.microprofile.lra.annotation.ParticipantStatus;
@@ -40,6 +41,9 @@ import javax.ws.rs.core.Response;
 import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER;
 import static org.eclipse.microprofile.lra.tck.TckContextTests.HttpMethod.GET;
 import static org.eclipse.microprofile.lra.tck.TckContextTests.HttpMethod.PUT;
+import static org.eclipse.microprofile.lra.tck.participant.api.ContextTckResource.ASYNC_LRA_PATH1;
+import static org.eclipse.microprofile.lra.tck.participant.api.ContextTckResource.ASYNC_LRA_PATH2;
+import static org.eclipse.microprofile.lra.tck.participant.api.ContextTckResource.ASYNC_LRA_PATH3;
 import static org.eclipse.microprofile.lra.tck.participant.api.ContextTckResource.CONTEXT_CHECK_LRA_PATH;
 import static org.eclipse.microprofile.lra.tck.participant.api.ContextTckResource.LEAVE_PATH;
 import static org.eclipse.microprofile.lra.tck.participant.api.ContextTckResource.LRA_TCK_FAULT_CODE_HEADER;
@@ -206,6 +210,43 @@ public class TckContextTests extends TckTestBase {
         invoke(false, CONTEXT_CHECK_LRA_PATH, PUT, null);
     }
 
+    @Test
+    public void testAsync1Support() {
+        String lra = invoke(false, ASYNC_LRA_PATH1, PUT, null);
+
+        // verify that the resource was asked to complete
+        String completions = invoke(true, METRIC_PATH + "/" + Complete.class.getName(), GET, lra);
+
+        assertEquals(testName.getMethodName() + ": Resource was not asked to complete",
+                Integer.toString(1), completions);
+    }
+
+    @Test
+    public void testAsync2Support() {
+        String lra = invoke(false, ASYNC_LRA_PATH2, PUT, null);
+
+        // verify that the resource was asked to complete
+        String completions = invoke(true, METRIC_PATH + "/" + Complete.class.getName(), GET, lra);
+
+        assertEquals(testName.getMethodName() + ": Resource was not asked to complete",
+                Integer.toString(1), completions);
+    }
+
+    @Test
+    public void testAsync3Support() {
+        // invoke an async resource that throws an exception which cancels the LRA
+        String lra = invoke(true, ASYNC_LRA_PATH3, PUT, null, 404, ContextTckResource.EndPhase.SUCCESS, 200);
+
+        // verify that the resource was asked to compensate
+        String completions = invoke(true, METRIC_PATH + "/" + Complete.class.getName(), GET, lra);
+        String compensations = invoke(false, METRIC_PATH + "/" + Compensate.class.getName(), GET, lra);
+
+        assertEquals(testName.getMethodName() + ": Resource was asked to complete",
+                Integer.toString(0), completions);
+        assertEquals(testName.getMethodName() + ": Resource was not asked to compensate",
+                Integer.toString(1), compensations);
+    }
+
     private String invoke(boolean delay, String where, HttpMethod method, String lraContext) {
         return invoke(delay, where, method, lraContext, 200, ContextTckResource.EndPhase.SUCCESS, 200);
     }