RequestStateContext: Add method to abort cancelled requests

The new abortIfCancelled() method asks the registered
RequestStateProviders whether the request is cancelled. If yes, it
aborts the request by throwing a RequestCancelledException.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I0145f451c441ae665e89b42728eae895fa767084
diff --git a/java/com/google/gerrit/server/cancellation/RequestStateContext.java b/java/com/google/gerrit/server/cancellation/RequestStateContext.java
index 1fa2e03..183d779 100644
--- a/java/com/google/gerrit/server/cancellation/RequestStateContext.java
+++ b/java/com/google/gerrit/server/cancellation/RequestStateContext.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.cancellation;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
 import java.util.HashSet;
 import java.util.Set;
@@ -57,7 +58,25 @@
   private static final ThreadLocal<Set<RequestStateProvider>> threadLocalRequestStateProviders =
       new ThreadLocal<>();
 
+  /**
+   * Aborts the current request by throwing a {@link RequestCancelledException} if any of the
+   * registered {@link RequestStateProvider}s reports the request as cancelled.
+   *
+   * @throws RequestCancelledException thrown if the current request is cancelled and should be
+   *     aborted
+   */
+  public static void abortIfCancelled() throws RequestCancelledException {
+    getRequestStateProviders()
+        .forEach(
+            requestStateProvider ->
+                requestStateProvider.checkIfCancelled(
+                    (reason, message) -> {
+                      throw new RequestCancelledException(reason, message);
+                    }));
+  }
+
   /** Returns the {@link RequestStateProvider}s that have been registered for the thread. */
+  @VisibleForTesting
   static ImmutableSet<RequestStateProvider> getRequestStateProviders() {
     if (threadLocalRequestStateProviders.get() == null) {
       return ImmutableSet.of();
diff --git a/java/com/google/gerrit/server/cancellation/RequestStateProvider.java b/java/com/google/gerrit/server/cancellation/RequestStateProvider.java
index e1716eb..683ca1d 100644
--- a/java/com/google/gerrit/server/cancellation/RequestStateProvider.java
+++ b/java/com/google/gerrit/server/cancellation/RequestStateProvider.java
@@ -31,6 +31,7 @@
   void checkIfCancelled(OnCancelled onCancelled);
 
   /** Callback interface to be invoked if a request is cancelled. */
+  @FunctionalInterface
   interface OnCancelled {
     /**
      * Callback that is invoked if the request is cancelled.
diff --git a/javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java b/javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java
index 32903e6..e2c43eb 100644
--- a/javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java
+++ b/javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.cancellation;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableSet;
 import org.junit.Test;
@@ -89,6 +91,76 @@
     assertNoRequestStateProviders();
   }
 
+  @Test
+  public void abortIfCancelled_noRequestStateProvider() {
+    assertNoRequestStateProviders();
+
+    // Calling abortIfCancelled() shouldn't throw an exception.
+    RequestStateContext.abortIfCancelled();
+  }
+
+  @Test
+  public void abortIfCancelled_requestNotCancelled() {
+    try (RequestStateContext requestStateContext =
+        RequestStateContext.open()
+            .addRequestStateProvider(
+                new RequestStateProvider() {
+                  @Override
+                  public void checkIfCancelled(OnCancelled onCancelled) {}
+                })) {
+      // Calling abortIfCancelled() shouldn't throw an exception.
+      RequestStateContext.abortIfCancelled();
+    }
+  }
+
+  @Test
+  public void abortIfCancelled_requestCancelled() {
+    try (RequestStateContext requestStateContext =
+        RequestStateContext.open()
+            .addRequestStateProvider(
+                new RequestStateProvider() {
+                  @Override
+                  public void checkIfCancelled(OnCancelled onCancelled) {
+                    onCancelled.onCancel(
+                        RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST, /* message= */ null);
+                  }
+                })) {
+      RequestCancelledException requestCancelledException =
+          assertThrows(
+              RequestCancelledException.class, () -> RequestStateContext.abortIfCancelled());
+      assertThat(requestCancelledException)
+          .hasMessageThat()
+          .isEqualTo("Request cancelled: CLIENT_CLOSED_REQUEST");
+      assertThat(requestCancelledException.getCancellationReason())
+          .isEqualTo(RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST);
+      assertThat(requestCancelledException.getCancellationMessage()).isEmpty();
+    }
+  }
+
+  @Test
+  public void abortIfCancelled_requestCancelled_withMessage() {
+    try (RequestStateContext requestStateContext =
+        RequestStateContext.open()
+            .addRequestStateProvider(
+                new RequestStateProvider() {
+                  @Override
+                  public void checkIfCancelled(OnCancelled onCancelled) {
+                    onCancelled.onCancel(
+                        RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED, "deadline = 10m");
+                  }
+                })) {
+      RequestCancelledException requestCancelledException =
+          assertThrows(
+              RequestCancelledException.class, () -> RequestStateContext.abortIfCancelled());
+      assertThat(requestCancelledException)
+          .hasMessageThat()
+          .isEqualTo("Request cancelled: SERVER_DEADLINE_EXCEEDED (deadline = 10m)");
+      assertThat(requestCancelledException.getCancellationReason())
+          .isEqualTo(RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED);
+      assertThat(requestCancelledException.getCancellationMessage()).hasValue("deadline = 10m");
+    }
+  }
+
   private void assertNoRequestStateProviders() {
     assertRequestStateProviders(ImmutableSet.of());
   }