Merge "Propagate RuntimeExceptions in DefaultQuotaBackend"
diff --git a/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
index baa7677..c6e67ca 100644
--- a/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
+++ b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
@@ -74,8 +74,12 @@
           responses.add(enforcer.call(p -> p.dryRun(quotaGroup, requestContext, numTokens)));
         }
       } catch (RuntimeException e) {
-        logger.atSevere().withCause(e).log("exception while enforcing quota");
-        responses.add(QuotaResponse.error("failed to request quota tokens"));
+        // Roll back the quota request for all enforcers that deducted the quota. Rethrow the
+        // exception to adhere to the API contract.
+        if (deduct) {
+          refillAfterErrorOrException(enforcers, responses, quotaGroup, requestContext, numTokens);
+        }
+        throw e;
       }
     }
 
@@ -83,11 +87,7 @@
       // Roll back the quota request for all enforcers that deducted the quota (= the request
       // succeeded). Don't touch failed enforcers as the interface contract said that failed
       // requests should not be deducted.
-      for (int i = 0; i < responses.size(); i++) {
-        if (responses.get(i).status().isOk()) {
-          enforcers.get(i).run(p -> p.refill(quotaGroup, requestContext, numTokens));
-        }
-      }
+      refillAfterErrorOrException(enforcers, responses, quotaGroup, requestContext, numTokens);
     }
 
     logger.atFine().log(
@@ -100,6 +100,19 @@
     return QuotaResponse.Aggregated.create(ImmutableList.copyOf(responses));
   }
 
+  private static void refillAfterErrorOrException(
+      List<PluginSetEntryContext<QuotaEnforcer>> enforcers,
+      List<QuotaResponse> collectedResponses,
+      String quotaGroup,
+      QuotaRequestContext requestContext,
+      long numTokens) {
+    for (int i = 0; i < collectedResponses.size(); i++) {
+      if (collectedResponses.get(i).status().isOk()) {
+        enforcers.get(i).run(p -> p.refill(quotaGroup, requestContext, numTokens));
+      }
+    }
+  }
+
   static class WithUser extends WithResource implements QuotaBackend.WithUser {
     WithUser(PluginSetContext<QuotaEnforcer> quotaEnforcers, CurrentUser user) {
       super(quotaEnforcers, QuotaRequestContext.builder().user(user).build());
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java b/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
index f60d301..dea83ca 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
@@ -139,17 +139,13 @@
   }
 
   @Test
-  public void requestTokenPluginThrowsResultsInError() throws Exception {
+  public void requestTokenPluginThrowsAndRethrows() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
     expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andThrow(new NullPointerException());
     replay(quotaEnforcer);
 
-    QuotaResponse.Aggregated result = quotaBackend.user(identifiedAdmin).requestToken("testGroup");
-    assertThat(result)
-        .isEqualTo(singletonAggregation(QuotaResponse.error("failed to request quota tokens")));
-    exception.expect(QuotaException.class);
-    exception.expectMessage("failed to request quota tokens");
-    result.throwOnError();
+    exception.expect(NullPointerException.class);
+    quotaBackend.user(identifiedAdmin).requestToken("testGroup");
   }
 
   private static QuotaResponse.Aggregated singletonAggregation(QuotaResponse response) {
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
index ba69d7c..31a8808 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
@@ -19,6 +19,7 @@
 import static org.easymock.EasyMock.expectLastCall;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.resetToStrict;
+import static org.easymock.EasyMock.verify;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -87,20 +88,24 @@
 
   @Test
   public void refillsOnException() {
+    NullPointerException exception = new NullPointerException();
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcerA.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
-    expect(quotaEnforcerB.requestTokens("testGroup", ctx, 1)).andThrow(new NullPointerException());
-    quotaEnforcerA.refill("testGroup", ctx, 1);
+    expect(quotaEnforcerA.requestTokens("testGroup", ctx, 1)).andThrow(exception);
+    expect(quotaEnforcerB.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
+    quotaEnforcerB.refill("testGroup", ctx, 1);
     expectLastCall();
 
     replay(quotaEnforcerA);
     replay(quotaEnforcerB);
 
-    assertThat(quotaBackend.user(identifiedAdmin).requestToken("testGroup"))
-        .isEqualTo(
-            QuotaResponse.Aggregated.create(
-                ImmutableList.of(
-                    QuotaResponse.ok(), QuotaResponse.error("failed to request quota tokens"))));
+    try {
+      quotaBackend.user(identifiedAdmin).requestToken("testGroup");
+      fail("expected a NullPointerException");
+    } catch (NullPointerException e) {
+      assertThat(exception).isEqualTo(e);
+    }
+
+    verify(quotaEnforcerA);
   }
 
   @Test