Repository: eclipse/microprofile-lra (java)

Issue:
SPEC: Indicate effect LRAid Unknown response by participant on LRA status
A call to the @Complete and @Compensate methods/endpoints are allowed to return with a result of "LRAid Unknown" (HTTP status 404 - or 410 when we decide that that response code is better suited)

The result on the LRA status is depending on the existence of previous call (are we in 'retry' mode) and is as follows

- When the first call to @Complete and @Compensate methods/endpoints results in "LRAid Unknown" the status of the Participant becomes `FailedToComplete` or `FailedToCompensate` (depending on the action) and LRA status becomes FailedToClose or FailedToCancel. (according to the general rules)

- When another call (not the first) to @Complete and @Compensate methods/endpoints results in "LRAid Unknown" the status of the Participant becomes `Completed` or `Compensated` (depending on the action) and LRA status becomes is defined according to the general rules.

- When a call to the @Status method/endpoint results in "LRAid Unknown" the status of the Participant becomes `Completed` or `Compensated` (depending on the action) and LRA status becomes is defined according to the general rules.


Pull request description:
Clarify and test participant response status code 410 on LRA Status
Spec: Clarify participant response status code 410 on LRA Status
TCK: Additional tests to verify status code 410 replies by implementation
Fixes #195.

Patch:
diff --git a/api/src/main/java/org/eclipse/microprofile/lra/annotation/Compensate.java b/api/src/main/java/org/eclipse/microprofile/lra/annotation/Compensate.java
index e2b0671..b172feb 100644
--- a/api/src/main/java/org/eclipse/microprofile/lra/annotation/Compensate.java
+++ b/api/src/main/java/org/eclipse/microprofile/lra/annotation/Compensate.java
@@ -142,6 +142,15 @@ import java.time.temporal.ChronoUnit;
  * </table>
  *
  * <p>
+ * The implementation will handle the return code 410 in the same way
+ * as the return code 200. Specifically, when the implementation calls the Compensate method
+ * as a result of the LRA being cancelled, and the participant returns the code
+ * 410, the implementation assumes that the action is compensated and participant returns
+ * a 410 since participant is allowed to forget about an action which is completely
+ * handled by the participant.
+ * </p>
+ *
+ * <p>
  * If any other code is returned (or, in the 500 case, the body does not
  * correspond to a valid state) then the implementation SHOULD either keep
  * retrying or attempt to discover the status by calling the
diff --git a/api/src/main/java/org/eclipse/microprofile/lra/annotation/Complete.java b/api/src/main/java/org/eclipse/microprofile/lra/annotation/Complete.java
index 7eae535..186ad01 100644
--- a/api/src/main/java/org/eclipse/microprofile/lra/annotation/Complete.java
+++ b/api/src/main/java/org/eclipse/microprofile/lra/annotation/Complete.java
@@ -144,6 +144,15 @@ import java.time.temporal.ChronoUnit;
  * </table>
  *
  * <p>
+ * The implementation will handle the return code 410 in the same way
+ * as the return code 200. Specifically, when the implementation calls the Complete method
+ * as a result of the LRA being closed, and the participant returns the code
+ * 410, the implementation assumes that the action is completed and participant returns
+ * a 410 since participant is allowed to forget about an action which is completely
+ * handled by the participant.
+ * </p>
+ *
+ * <p>
  * If any other code is returned (or, in the 500 case, the body does not
  * correspond to a valid state)  then the implementation SHOULD either keep
  * retrying or attempt to discover the status by calling the
diff --git a/api/src/main/java/org/eclipse/microprofile/lra/annotation/Status.java b/api/src/main/java/org/eclipse/microprofile/lra/annotation/Status.java
index 704ea7d..a5f2cda 100644
--- a/api/src/main/java/org/eclipse/microprofile/lra/annotation/Status.java
+++ b/api/src/main/java/org/eclipse/microprofile/lra/annotation/Status.java
@@ -117,6 +117,15 @@ import java.lang.annotation.Target;
  *   <td scope="row">The method does not know about the LRA</td>
  * </tr>
  * </table>
+ *
+ * <p>
+ * The implementation will handle the return code 410 in the same way
+ * as the return code 200. Specifically, when the implementation calls the Status method
+ * after it has called the Complete or Compensated method and received a response which indicates
+ * that the process is in progress (with a return code 202, for example). The response code 410
+ * which is received when calling this Status annotated method, MUST be interpreted by the implementation
+ * that the process is successfully completed and the participant already forget about the LRA.
+ * </p>
  */
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.METHOD})
diff --git a/spec/src/main/asciidoc/microprofile-lra-spec.adoc b/spec/src/main/asciidoc/microprofile-lra-spec.adoc
index 5332097..9efe868 100644
--- a/spec/src/main/asciidoc/microprofile-lra-spec.adoc
+++ b/spec/src/main/asciidoc/microprofile-lra-spec.adoc
@@ -787,6 +787,12 @@ annotations when associated with JAX-RS resource methods:
 Please refer to the javadoc for each annotation for the description of the
 conditions under which the various JAX-RS response codes are returned.
 
+Returning the status code 410, when appropriate - see the above table, has the same effect
+as status 200. The participant indicates with this response that it is no longer aware of the
+LRA identification but the implementation MUST assume that all required actions are performed
+, which is equivalent with return status 200, and that the participant already forgot about it
+(participant is allowed to forget about a LRA identification when completely handled)
+
 If the method annotated with `@AfterLRA` returns an unexpected HTTP status
 or never reaches the caller then the implementation MUST invoke the same method again.
 
@@ -801,6 +807,12 @@ Users are allowed to reuse existing JAX-RS endpoints for participant method defi
 In this case the LRA implementation MUST ensure that invoking these methods outside of the 
 implementation of the LRA specification will not influence any running LRA.
 
+Specifically, developers should NEVER call any JAX-RS endpoint for participant callback methods
+(@Compensate, @Complete, @Status, @Forget, and @AfterLRA) where they
+add a header defined by the Java constant `LRA_HTTP_RECOVERY_HEADER`.
+This way, a developer can distinguish if the call is made by an end-user or the implementation and
+make sure that it will not influence the participant of the LRA when called directly.
+
 ===== Non-JAX-RS afterLRA method
 
 A method annotated with `@AfterLRA` that is not a JAX-RS resource method MUST accept
diff --git a/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/LRAUnknownResource.java b/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/LRAUnknownResource.java
new file mode 100644
index 0000000..672e6f7
--- /dev/null
+++ b/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/LRAUnknownResource.java
@@ -0,0 +1,154 @@
+/*
+ *******************************************************************************
+ * Copyright (c) 2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.eclipse.microprofile.lra.tck.participant.api;
+
+import org.eclipse.microprofile.lra.annotation.AfterLRA;
+import org.eclipse.microprofile.lra.annotation.Compensate;
+import org.eclipse.microprofile.lra.annotation.Complete;
+import org.eclipse.microprofile.lra.annotation.LRAStatus;
+import org.eclipse.microprofile.lra.annotation.ws.rs.LRA;
+import org.eclipse.microprofile.lra.tck.service.LRAMetricService;
+import org.eclipse.microprofile.lra.tck.service.LRAMetricType;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER;
+import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_ENDED_CONTEXT_HEADER;
+
+/**
+ * TCK Tests related to the 410 status code handling. Version without a Status method.
+ */
+@ApplicationScoped
+@Path(LRAUnknownResource.LRA_CONTROLLER_PATH)
+@LRA
+public class LRAUnknownResource {
+    public static final String LRA_CONTROLLER_PATH = "lraUnknownController";
+    public static final String TRANSACTIONAL_WORK_PATH = "work";
+
+    private static final Logger LOGGER = Logger.getLogger(LRAUnknownResource.class.getName());
+    private static final String AFTER_LRA = "/after";
+
+    private Map<String, Scenario> scenarioMap = new HashMap<>();
+
+    @Inject
+    private LRAMetricService lraMetricService;
+
+    @PUT
+    @Path(TRANSACTIONAL_WORK_PATH)
+    public Response activityWithLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId
+            , @QueryParam("scenario") Scenario scenario) {
+
+        scenarioMap.put(lraId.toASCIIString(), scenario);
+        // scenario.pathResponseCode determines if /complete or /compensate will be called.
+        return Response.status(scenario.getPathResponseCode()).entity(lraId).build();
+    }
+
+    @PUT
+    @Path("/complete")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Complete
+    public Response completeWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId)
+            throws NotFoundException {
+        lraMetricService.incrementMetric(LRAMetricType.Completed, lraId);
+
+        // flow for the following cases
+        // Scenario.COMPLETE_RETRY
+        // -> /complete -> 202
+        // -> /complete -> 410 (recalled to find final status by implementation)
+
+        // Scenario.COMPLETE_IMMEDIATE
+        // -> /complete -> 410
+
+        int responseCode = 410;
+        Scenario scenario = scenarioMap.get(lraId.toASCIIString());
+        if (scenario == Scenario.COMPLETE_RETRY) {
+            responseCode = 202; // The 'action' is in progress
+            scenarioMap.remove(lraId.toASCIIString()); // so that by next call the return status is 410.
+        }
+
+        LOGGER.info(String.format("LRA id '%s' was completed", lraId.toASCIIString()));
+        return Response.status(responseCode).build();
+    }
+
+    @PUT
+    @Path("/compensate")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Compensate()
+    public Response compensateWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId)
+            throws NotFoundException {
+
+        lraMetricService.incrementMetric(LRAMetricType.Compensated, lraId);
+
+        // flow for the following cases
+        // Scenario.COMPENSATE_RETRY
+        // -> /compensate -> 202
+        // -> /compensate -> 410 (recalled to find final status by implementation)
+
+        // Scenario.COMPENSATE_IMMEDIATE
+        // -> /compensate -> 410
+
+        int responseCode = 410;
+        Scenario scenario = scenarioMap.get(lraId.toASCIIString());
+        if (scenario == Scenario.COMPENSATE_RETRY) {
+            responseCode = 202; // The 'action' is in progress
+            scenarioMap.remove(lraId.toASCIIString()); // so that by next call the return status is 410.
+        }
+
+        LOGGER.info(String.format("LRA id '%s' was compensated", lraId));
+        return Response.status(responseCode).build();
+    }
+
+    @PUT
+    @Path(AFTER_LRA)
+    @AfterLRA // this method will be called when the LRA associated with the method activityWithLRA finishes
+    public Response afterEnd(@HeaderParam(LRA_HTTP_ENDED_CONTEXT_HEADER) URI lraId, LRAStatus status) {
+        lraMetricService.incrementMetric(LRAMetricType.AfterLRA, lraId);
+        switch (status) {
+            case Closed:
+                // FALLTHRU
+            case Cancelled:
+                // FALLTHRU
+            case FailedToCancel:
+                // FALLTHRU
+            case FailedToClose:
+                lraMetricService.incrementMetric(
+                        LRAMetricType.valueOf(status.name()),
+                        lraId,
+                        LRAUnknownResource.class.getName());
+                return Response.ok().build();
+            default:
+                return Response.status(Response.Status.BAD_REQUEST).build();
+        }
+    }
+
+}
diff --git a/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/LRAUnknownStatusResource.java b/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/LRAUnknownStatusResource.java
new file mode 100644
index 0000000..e13f9b6
--- /dev/null
+++ b/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/LRAUnknownStatusResource.java
@@ -0,0 +1,164 @@
+/*
+ *******************************************************************************
+ * Copyright (c) 2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.eclipse.microprofile.lra.tck.participant.api;
+
+import org.eclipse.microprofile.lra.annotation.AfterLRA;
+import org.eclipse.microprofile.lra.annotation.Compensate;
+import org.eclipse.microprofile.lra.annotation.Complete;
+import org.eclipse.microprofile.lra.annotation.Status;
+import org.eclipse.microprofile.lra.annotation.LRAStatus;
+import org.eclipse.microprofile.lra.annotation.ws.rs.LRA;
+import org.eclipse.microprofile.lra.tck.service.LRAMetricService;
+import org.eclipse.microprofile.lra.tck.service.LRAMetricType;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.PUT;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER;
+import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_ENDED_CONTEXT_HEADER;
+
+/**
+ * TCK Tests related to the 410 status code handling. Version with a Status method.
+ */
+@ApplicationScoped
+@Path(LRAUnknownStatusResource.LRA_CONTROLLER_PATH)
+@LRA
+public class LRAUnknownStatusResource {
+    public static final String LRA_CONTROLLER_PATH = "lraUnknownStatusController";
+    public static final String TRANSACTIONAL_WORK_PATH = "work";
+
+    private static final Logger LOGGER = Logger.getLogger(LRAUnknownStatusResource.class.getName());
+    private static final String AFTER_LRA = "/after";
+
+    private Map<String, Scenario> scenarioMap = new HashMap<>();
+
+    @Inject
+    private LRAMetricService lraMetricService;
+
+    @PUT
+    @Path(TRANSACTIONAL_WORK_PATH)
+    public Response activityWithLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId
+            , @QueryParam("scenario") Scenario scenario) {
+
+        scenarioMap.put(lraId.toASCIIString(), scenario);
+        // scenario.pathResponseCode determines if /complete or /compensate will be called.
+        return Response.status(scenario.getPathResponseCode()).entity(lraId).build();
+    }
+
+    @PUT
+    @Path("/complete")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Complete
+    public Response completeWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId)
+            throws NotFoundException {
+        lraMetricService.incrementMetric(LRAMetricType.Completed, lraId);
+
+        // flow for the following cases
+        // Scenario.COMPLETE_RETRY
+        // -> /complete -> 202
+        // -> /complete -> 410 (recalled to find final status by implementation)
+
+        // Scenario.COMPLETE_IMMEDIATE
+        // -> /complete -> 410
+
+        int responseCode = 410;
+        Scenario scenario = scenarioMap.get(lraId.toASCIIString());
+        if (scenario == Scenario.COMPLETE_RETRY) {
+            responseCode = 202; // The 'action' is in progress
+            scenarioMap.remove(lraId.toASCIIString()); // so that by next call the return status is 410.
+        }
+
+        LOGGER.info(String.format("LRA id '%s' was completed", lraId.toASCIIString()));
+        return Response.status(responseCode).build();
+    }
+
+    @PUT
+    @Path("/compensate")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Compensate()
+    public Response compensateWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId)
+            throws NotFoundException {
+
+        lraMetricService.incrementMetric(LRAMetricType.Compensated, lraId);
+
+        // flow for the following cases
+        // Scenario.COMPENSATE_RETRY
+        // -> /compensate -> 202
+        // -> /compensate -> 410 (recalled to find final status by implementation)
+
+        // Scenario.COMPENSATE_IMMEDIATE
+        // -> /compensate -> 410
+
+        int responseCode = 410;
+        Scenario scenario = scenarioMap.get(lraId.toASCIIString());
+        if (scenario == Scenario.COMPENSATE_RETRY) {
+            responseCode = 202; // The 'action' is in progress
+            scenarioMap.remove(lraId.toASCIIString()); // so that by next call the return status is 410.
+        }
+
+        LOGGER.info(String.format("LRA id '%s' was compensated", lraId));
+        return Response.status(responseCode).build();
+    }
+
+    @GET
+    @Path("/status")
+    @Status
+    public Response status(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
+        lraMetricService.incrementMetric(LRAMetricType.Status, lraId);
+
+        return Response.status(410).build();
+    }
+
+    @PUT
+    @Path(AFTER_LRA)
+    @AfterLRA // this method will be called when the LRA associated with the method activityWithLRA finishes
+    public Response afterEnd(@HeaderParam(LRA_HTTP_ENDED_CONTEXT_HEADER) URI lraId, LRAStatus status) {
+        lraMetricService.incrementMetric(LRAMetricType.AfterLRA, lraId);
+        switch (status) {
+            case Closed:
+                // FALLTHRU
+            case Cancelled:
+                // FALLTHRU
+            case FailedToCancel:
+                // FALLTHRU
+            case FailedToClose:
+                lraMetricService.incrementMetric(
+                        LRAMetricType.valueOf(status.name()),
+                        lraId,
+                        LRAUnknownStatusResource.class.getName());
+                return Response.ok().build();
+            default:
+                return Response.status(Response.Status.BAD_REQUEST).build();
+        }
+    }
+}
diff --git a/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/Scenario.java b/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/Scenario.java
new file mode 100644
index 0000000..8a2ee13
--- /dev/null
+++ b/tck/src/main/java/org/eclipse/microprofile/lra/tck/participant/api/Scenario.java
@@ -0,0 +1,34 @@
+/*
+ *******************************************************************************
+ * Copyright (c) 2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.eclipse.microprofile.lra.tck.participant.api;
+
+public enum Scenario {
+    COMPENSATE_IMMEDIATE(500), COMPENSATE_RETRY(500), COMPLETE_IMMEDIATE(200), COMPLETE_RETRY(200);
+
+    private int pathResponseCode;
+
+    Scenario(int pathResponseCode) {
+        this.pathResponseCode = pathResponseCode;
+    }
+
+    public int getPathResponseCode() {
+        return pathResponseCode;
+    }
+}


Test patch:
diff --git a/tck/src/main/java/org/eclipse/microprofile/lra/tck/TckUnknownStatusTests.java b/tck/src/main/java/org/eclipse/microprofile/lra/tck/TckUnknownStatusTests.java
new file mode 100644
index 0000000..1e587b4
--- /dev/null
+++ b/tck/src/main/java/org/eclipse/microprofile/lra/tck/TckUnknownStatusTests.java
@@ -0,0 +1,103 @@
+/*
+ *******************************************************************************
+ * Copyright (c) 2018 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.eclipse.microprofile.lra.tck;
+
+import org.eclipse.microprofile.lra.tck.participant.api.LRAUnknownStatusResource;
+import org.eclipse.microprofile.lra.tck.participant.api.Scenario;
+import org.eclipse.microprofile.lra.tck.service.LRAMetricService;
+import org.eclipse.microprofile.lra.tck.service.LRAMetricType;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import javax.inject.Inject;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * TCK Tests related to the 410 status code handling. Version with a Status method.
+ */
+@RunWith(Arquillian.class)
+public class TckUnknownStatusTests extends TckTestBase {
+
+    @Inject
+    private LRAMetricService lraMetricService;
+
+    @Deployment(name = "tckunkownstatus")
+    public static WebArchive deploy() {
+        return TckUnknownStatusTests.deploy(TckTests.class.getSimpleName().toLowerCase());
+    }
+
+    @Before
+    public void before() {
+        super.before();
+    }
+
+    @Test
+    public void compensate_retry() throws WebApplicationException {
+        String lraIdString = invoke(Scenario.COMPENSATE_RETRY);
+        URI lraId = URI.create(lraIdString);
+
+        applyConsistencyDelay();
+        int compensated = lraMetricService.getMetric(LRAMetricType.Compensated, lraId);
+        int status = lraMetricService.getMetric(LRAMetricType.Status, lraId);
+        int afterLRA = lraMetricService.getMetric(LRAMetricType.AfterLRA, lraId);
+        int cancelled = lraMetricService.getMetric(LRAMetricType.Cancelled, lraId);
+
+        assertEquals(1, compensated);
+        assertEquals(1, status);
+        assertEquals(1, afterLRA);
+        assertEquals(1, cancelled);
+    }
+
+    @Test
+    public void complete_retry() throws WebApplicationException {
+        String lraIdString = invoke(Scenario.COMPLETE_RETRY);
+        URI lraId = URI.create(lraIdString);
+
+        applyConsistencyDelay();
+        int completed = lraMetricService.getMetric(LRAMetricType.Completed, lraId);
+        int status = lraMetricService.getMetric(LRAMetricType.Status, lraId);
+        int afterLRA = lraMetricService.getMetric(LRAMetricType.AfterLRA, lraId);
+        int closed = lraMetricService.getMetric(LRAMetricType.Closed, lraId);
+
+        assertEquals(1, completed);
+        assertEquals(1, status);
+        assertEquals(1, afterLRA);
+        assertEquals(1, closed);
+    }
+
+    private String invoke(Scenario scenario) {
+        WebTarget resourcePath = tckSuiteTarget.path(LRAUnknownStatusResource.LRA_CONTROLLER_PATH)
+                .path(LRAUnknownStatusResource.TRANSACTIONAL_WORK_PATH)
+                .queryParam("scenario", scenario.name());
+        Response response = resourcePath.request().put(Entity.text(""));
+
+        return checkStatusReadAndCloseResponse(Response.Status.fromStatusCode(scenario.getPathResponseCode()), response, resourcePath);
+    }
+}
diff --git a/tck/src/main/java/org/eclipse/microprofile/lra/tck/TckUnknownTests.java b/tck/src/main/java/org/eclipse/microprofile/lra/tck/TckUnknownTests.java
new file mode 100644
index 0000000..9af1f46
--- /dev/null
+++ b/tck/src/main/java/org/eclipse/microprofile/lra/tck/TckUnknownTests.java
@@ -0,0 +1,129 @@
+/*
+ *******************************************************************************
+ * Copyright (c) 2018 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+package org.eclipse.microprofile.lra.tck;
+
+import org.eclipse.microprofile.lra.tck.participant.api.LRAUnknownResource;
+import org.eclipse.microprofile.lra.tck.participant.api.Scenario;
+import org.eclipse.microprofile.lra.tck.service.LRAMetricService;
+import org.eclipse.microprofile.lra.tck.service.LRAMetricType;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import javax.inject.Inject;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * TCK Tests related to the 410 status code handling. Version without a Status method.
+ */
+@RunWith(Arquillian.class)
+public class TckUnknownTests extends TckTestBase {
+
+    @Inject
+    private LRAMetricService lraMetricService;
+
+    @Deployment(name = "tckunkown")
+    public static WebArchive deploy() {
+        return TckTestBase.deploy(TckUnknownTests.class.getSimpleName().toLowerCase());
+    }
+
+    @Before
+    public void before() {
+        super.before();
+    }
+
+    @Test
+    public void compensate_immediate() throws WebApplicationException {
+        String lraIdString = invoke(Scenario.COMPENSATE_IMMEDIATE);
+        URI lraId = URI.create(lraIdString);
+
+        applyConsistencyDelay();
+        int compensated = lraMetricService.getMetric(LRAMetricType.Compensated, lraId);
+        int afterLRA = lraMetricService.getMetric(LRAMetricType.AfterLRA, lraId);
+        int cancelled = lraMetricService.getMetric(LRAMetricType.Cancelled, lraId);
+
+        assertEquals(1, compensated);
+        assertEquals(1, afterLRA);
+        assertEquals(1, cancelled);
+    }
+
+    @Test
+    public void compensate_retry() throws WebApplicationException {
+        String lraIdString = invoke(Scenario.COMPENSATE_RETRY);
+        URI lraId = URI.create(lraIdString);
+
+        applyConsistencyDelay();
+        int compensated = lraMetricService.getMetric(LRAMetricType.Compensated, lraId);
+        int afterLRA = lraMetricService.getMetric(LRAMetricType.AfterLRA, lraId);
+        int cancelled = lraMetricService.getMetric(LRAMetricType.Cancelled, lraId);
+
+        assertEquals(2, compensated);
+        assertEquals(1, afterLRA);
+        assertEquals(1, cancelled);
+    }
+
+    @Test
+    public void complete_immediate() throws WebApplicationException {
+        String lraIdString = invoke(Scenario.COMPLETE_IMMEDIATE);
+        URI lraId = URI.create(lraIdString);
+
+        applyConsistencyDelay();
+        int completed = lraMetricService.getMetric(LRAMetricType.Completed, lraId);
+        int afterLRA = lraMetricService.getMetric(LRAMetricType.AfterLRA, lraId);
+        int closed = lraMetricService.getMetric(LRAMetricType.Closed, lraId);
+
+        assertEquals(1, completed);
+        assertEquals(1, afterLRA);
+        assertEquals(1, closed);
+    }
+
+    @Test
+    public void complete_retry() throws WebApplicationException {
+        String lraIdString = invoke(Scenario.COMPLETE_RETRY);
+        URI lraId = URI.create(lraIdString);
+
+        applyConsistencyDelay();
+        int completed = lraMetricService.getMetric(LRAMetricType.Completed, lraId);
+        int afterLRA = lraMetricService.getMetric(LRAMetricType.AfterLRA, lraId);
+        int closed = lraMetricService.getMetric(LRAMetricType.Closed, lraId);
+
+        assertEquals(2, completed);
+        assertEquals(1, afterLRA);
+        assertEquals(1, closed);
+    }
+
+    private String invoke(Scenario scenario) {
+        WebTarget resourcePath = tckSuiteTarget.path(LRAUnknownResource.LRA_CONTROLLER_PATH).path(LRAUnknownResource.TRANSACTIONAL_WORK_PATH)
+                .queryParam("scenario", scenario.name());
+        Response response = resourcePath.request().put(Entity.text(""));
+
+        return checkStatusReadAndCloseResponse(Response.Status.fromStatusCode(scenario.getPathResponseCode()), response, resourcePath);
+    }
+}