Support configuring deadlines on server side

Allow to configure deadlines generically for requests by request type,
request URI pattern, caller and/or project pattern.

If multiple matching deadlines are configured the more strict deadline
(the one with the lower timeout) is applied.

Clients are able to override the configured deadlines by setting a
deadline on the request.

At the moment deadlines are only supported for REST, GIT_RECEIVE and SSH
requests but not for GIT_UPLOAD requests. Deadline support for
GIT_UPLOAD request may be implemented later.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: Idc2a4ee7a82e7324911e8e58468131b3fa007404
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index d9c2e4c..80bb0831 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -5307,6 +5307,68 @@
 +
 By default, unset (all projects are matched).
 
+[[deadline.id]]
+==== Subsection deadline.<id>
+
+There can be multiple `deadline.<id>` subsections to configure deadlines for
+request executions. For a deadline to apply all conditions of the
+`deadline.<id>` subsection must match. The subsection name is the ID of the
+deadline configuration and allows to track back an applied deadline to its
+configuration.
+
+Clients can override the deadlines that are configured here by setting a
+deadline on the request.
+
+Deadlines are only supported for `REST`, `SSH` and `GIT_RECEIVE` requests, but
+not for `GIT_UPLOAD` requests.
+
+[[deadline.id.timeout]]deadline.<id>.timeout::
++
+Timeout after which matching requests should be cancelled.
++
+Values must be specified using standard time unit abbreviations ('ms', 'sec',
+'min', etc.).
++
+For some requests additional timeout configurations may apply, e.g.
+link:#receive.timeout[receive.timeout] for git pushes.
++
+By default, unset.
+
+[[deadline.id.requestType]]deadline.<id>.requestType::
++
+Type of request to which the deadline applies (can be `GIT_RECEIVE`, `REST` and
+`SSH`).
++
+May be specified multiple times.
++
+By default, unset (all request types are matched).
+
+[[deadline.id.requestUriPattern]]deadline.<id>.requestUriPattern::
++
+Regular expression to match request URIs to which the deadline applies. Request
+URIs are only available for REST requests. Request URIs never include the '/a'
+prefix.
++
+May be specified multiple times.
++
+By default, unset (all request URIs are matched).
+
+[[deadline.id.account]]deadline.<id>.account::
++
+Account ID of an account to which the deadline applies.
++
+May be specified multiple times.
++
+By default, unset (all accounts are matched).
+
+[[deadline.id.projectPattern]]deadline.<id>.projectPattern::
++
+Regular expression to match project names to which the deadline applies.
++
+May be specified multiple times.
++
+By default, unset (all projects are matched).
+
 [[trackingid]]
 === Section trackingid
 
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index 7d2781c..469bee5 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -258,6 +258,11 @@
   X-Gerrit-Deadline: 5m
 ----
 
+
+Setting a deadline on the request overrides any
+link:config-gerrit.html#deadline.id[server-side deadline] that has been
+configured on the host.
+
 [[updated-refs]]
 === X-Gerrit-UpdatedRef
 This is only enabled when "X-Gerrit-UpdatedRef-Enabled" is set to "true" in the
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 3816dc4..2bfc62d 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -472,8 +472,10 @@
   git push -o deadline=10m ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master
 ----
 
-Setting a deadline doesn't override the server-side
-link:config.html#receive.timeout[receive timeout] after which a push is aborted.
+Setting a deadline for the push overrides any
+link:config-gerrit.html#deadline.id[server-side deadline] that has been
+configured on the host, but not the link:config.html#receive.timeout[receive
+timeout].
 
 [[push_replace]]
 === Replace Changes
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 7c2b354..7f90ed1 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -270,6 +270,7 @@
     final Injector injector;
     final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
     final ExperimentFeatures experimentFeatures;
+    final DeadlineChecker.Factory deadlineCheckerFactory;
 
     @Inject
     Globals(
@@ -288,7 +289,8 @@
         PluginSetContext<ExceptionHook> exceptionHooks,
         Injector injector,
         DynamicMap<DynamicOptions.DynamicBean> dynamicBeans,
-        ExperimentFeatures experimentFeatures) {
+        ExperimentFeatures experimentFeatures,
+        DeadlineChecker.Factory deadlineCheckerFactory) {
       this.currentUser = currentUser;
       this.webSession = webSession;
       this.paramParser = paramParser;
@@ -306,6 +308,7 @@
       this.injector = injector;
       this.dynamicBeans = dynamicBeans;
       this.experimentFeatures = experimentFeatures;
+      this.deadlineCheckerFactory = deadlineCheckerFactory;
     }
 
     private static Pattern makeAllowOrigin(Config cfg) {
@@ -353,12 +356,14 @@
 
     try (TraceContext traceContext = enableTracing(req, res)) {
       List<IdString> path = splitPath(req);
+      RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
 
       try (RequestStateContext requestStateContext =
               RequestStateContext.open()
-                  .addRequestStateProvider(new DeadlineChecker(req.getHeader(X_GERRIT_DEADLINE)));
+                  .addRequestStateProvider(
+                      globals.deadlineCheckerFactory.create(
+                          requestInfo, req.getHeader(X_GERRIT_DEADLINE)));
           PerThreadCache ignored = PerThreadCache.create()) {
-        RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
         globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
 
         // It's important that the PerformanceLogContext is closed before the response is sent to
diff --git a/java/com/google/gerrit/server/DeadlineChecker.java b/java/com/google/gerrit/server/DeadlineChecker.java
index 03504d4..8418782 100644
--- a/java/com/google/gerrit/server/DeadlineChecker.java
+++ b/java/com/google/gerrit/server/DeadlineChecker.java
@@ -14,21 +14,32 @@
 
 package com.google.gerrit.server;
 
+import static java.util.Comparator.comparing;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.base.Function;
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Longs;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.cancellation.RequestStateProvider;
 import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
 
 /** {@link RequestStateProvider} that checks whether a client provided deadline is exceeded. */
 public class DeadlineChecker implements RequestStateProvider {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private static String SECTION_DEADLINE = "deadline";
+
   /**
    * Formatter to format a timeout as {@code timeout=<TIMEOUT><TIME_UNIT>}.
    *
@@ -45,6 +56,17 @@
         return String.format("timeout=%s", formattedTimeout);
       };
 
+  public interface Factory {
+    DeadlineChecker create(RequestInfo requestInfo, @Nullable String clientProvidedTimeoutValue)
+        throws InvalidDeadlineException;
+
+    DeadlineChecker create(
+        long start, RequestInfo requestInfo, @Nullable String clientProvidedTimeoutValue)
+        throws InvalidDeadlineException;
+  }
+
+  private final RequestStateProvider.Reason cancellationReason;
+
   /**
    * Timeout in nanoseconds after which the request should be aborted.
    *
@@ -66,15 +88,20 @@
    *
    * <p>No deadline is enforced if the client provided deadline value is {@code null} or {@code 0}.
    *
+   * @param requestInfo the request that was received from a user
    * @param clientProvidedTimeoutValue the timeout value that the client provided, must represent a
    *     numerical time unit (e.g. "5m"), if no time unit is specified milliseconds are assumed, may
    *     be {@code null}
    * @throws InvalidDeadlineException thrown if the client provided deadline value cannot be parsed,
    *     e.g. because it uses a bad time unit
    */
-  public DeadlineChecker(@Nullable String clientProvidedTimeoutValue)
+  @AssistedInject
+  DeadlineChecker(
+      @GerritServerConfig Config serverConfig,
+      @Assisted RequestInfo requestInfo,
+      @Assisted @Nullable String clientProvidedTimeoutValue)
       throws InvalidDeadlineException {
-    this(System.nanoTime(), clientProvidedTimeoutValue);
+    this(serverConfig, System.nanoTime(), requestInfo, clientProvidedTimeoutValue);
   }
 
   /**
@@ -83,24 +110,63 @@
    * <p>No deadline is enforced if the client provided deadline value is {@code null} or {@code 0}.
    *
    * @param start the start time of the request in nanoseconds
+   * @param requestInfo the request that was received from a user
    * @param clientProvidedTimeoutValue the timeout value that the client provided, must represent a
    *     numerical time unit (e.g. "5m"), if no time unit is specified milliseconds are assumed, may
    *     be {@code null}
    * @throws InvalidDeadlineException thrown if the client provided deadline value cannot be parsed,
    *     e.g. because it uses a bad time unit
    */
-  public DeadlineChecker(long start, @Nullable String clientProvidedTimeoutValue)
+  @AssistedInject
+  DeadlineChecker(
+      @GerritServerConfig Config serverConfig,
+      @Assisted long start,
+      @Assisted RequestInfo requestInfo,
+      @Assisted @Nullable String clientProvidedTimeoutValue)
       throws InvalidDeadlineException {
-    this.timeout = parseTimeout(clientProvidedTimeoutValue);
+    Optional<ServerDeadline> serverSideDeadline = getServerSideDeadline(serverConfig, requestInfo);
+    Optional<Long> clientedProvidedTimeout = parseTimeout(clientProvidedTimeoutValue);
+    if (serverSideDeadline.isPresent()) {
+      if (clientedProvidedTimeout.isPresent()) {
+        logger.atFine().log(
+            "client provided deadline (timeout=%sms) overrides server deadline %s (timeout=%sms)",
+            TimeUnit.MILLISECONDS.convert(clientedProvidedTimeout.get(), TimeUnit.NANOSECONDS),
+            serverSideDeadline.get().id(),
+            TimeUnit.MILLISECONDS.convert(
+                serverSideDeadline.get().timeout(), TimeUnit.NANOSECONDS));
+      } else {
+        logger.atFine().log(
+            "applying server deadline %s (timeout = %sms)",
+            serverSideDeadline.get().id(),
+            TimeUnit.MILLISECONDS.convert(
+                serverSideDeadline.get().timeout(), TimeUnit.NANOSECONDS));
+      }
+    }
+    this.cancellationReason =
+        clientedProvidedTimeout.isPresent()
+            ? RequestStateProvider.Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED
+            : RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED;
+    this.timeout =
+        clientedProvidedTimeout.orElse(serverSideDeadline.map(ServerDeadline::timeout).orElse(0L));
     this.deadline = timeout > 0 ? Optional.of(start + timeout) : Optional.empty();
   }
 
+  private Optional<ServerDeadline> getServerSideDeadline(
+      Config serverConfig, RequestInfo requestInfo) {
+    return RequestConfig.parseConfigs(serverConfig, SECTION_DEADLINE).stream()
+        .filter(deadlineConfig -> deadlineConfig.matches(requestInfo))
+        .map(ServerDeadline::readFrom)
+        .filter(ServerDeadline::hasTimeout)
+        // let the stricter deadline (the lower deadline) take precedence
+        .sorted(comparing(ServerDeadline::timeout))
+        .findFirst();
+  }
+
   @Override
   public void checkIfCancelled(OnCancelled onCancelled) {
     long now = System.nanoTime();
     if (deadline.isPresent() && now > deadline.get()) {
-      onCancelled.onCancel(
-          Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED, TIMEOUT_FORMATTER.apply(timeout));
+      onCancelled.onCancel(cancellationReason, TIMEOUT_FORMATTER.apply(timeout));
     }
   }
 
@@ -113,9 +179,14 @@
    * @throws InvalidDeadlineException thrown if the provided deadline value cannot be parsed, e.g.
    *     because it uses a bad time unit
    */
-  private static long parseTimeout(@Nullable String timeoutValue) throws InvalidDeadlineException {
-    if (Strings.isNullOrEmpty(timeoutValue) || "0".equals(timeoutValue)) {
-      return 0;
+  private static Optional<Long> parseTimeout(@Nullable String timeoutValue)
+      throws InvalidDeadlineException {
+    if (Strings.isNullOrEmpty(timeoutValue)) {
+      return Optional.empty();
+    }
+
+    if ("0".equals(timeoutValue)) {
+      return Optional.of(0L);
     }
 
     // If no time unit was specified, assume milliseconds.
@@ -129,9 +200,34 @@
       if (parsedTimeout == -1) {
         throw new InvalidDeadlineException(String.format("Invalid value: %s", timeoutValue));
       }
-      return parsedTimeout;
+      return Optional.of(parsedTimeout);
     } catch (IllegalArgumentException e) {
       throw new InvalidDeadlineException(e.getMessage(), e);
     }
   }
+
+  @AutoValue
+  abstract static class ServerDeadline {
+    abstract String id();
+
+    abstract long timeout();
+
+    boolean hasTimeout() {
+      return timeout() > 0;
+    }
+
+    static ServerDeadline readFrom(RequestConfig requestConfig) {
+      String timeoutValue =
+          requestConfig.cfg().getString(requestConfig.section(), requestConfig.id(), "timeout");
+      try {
+        Optional<Long> timeout = parseTimeout(timeoutValue);
+        return new AutoValue_DeadlineChecker_ServerDeadline(requestConfig.id(), timeout.orElse(0L));
+      } catch (InvalidDeadlineException e) {
+        logger.atWarning().log(
+            "Ignoring invalid deadline configuration %s.%s.timeout: %s",
+            requestConfig.section(), requestConfig.id(), e.getMessage());
+        return new AutoValue_DeadlineChecker_ServerDeadline(requestConfig.id(), 0);
+      }
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/RequestConfig.java b/java/com/google/gerrit/server/RequestConfig.java
index bbefd7a..960907d 100644
--- a/java/com/google/gerrit/server/RequestConfig.java
+++ b/java/com/google/gerrit/server/RequestConfig.java
@@ -38,8 +38,7 @@
 
     for (String id : cfg.getSubsections(section)) {
       try {
-        RequestConfig.Builder requestConfig = RequestConfig.builder();
-        requestConfig.id(id);
+        RequestConfig.Builder requestConfig = RequestConfig.builder(cfg, section, id);
         requestConfig.requestTypes(parseRequestTypes(cfg, section, id));
         requestConfig.requestUriPatterns(parseRequestUriPatterns(cfg, section, id));
         requestConfig.accountIds(parseAccounts(cfg, section, id));
@@ -101,7 +100,13 @@
     return patterns.build();
   }
 
-  /** ID of the config */
+  /** the config from which this request config was read */
+  abstract Config cfg();
+
+  /** the section from which this request config was read */
+  abstract String section();
+
+  /** ID of the config, also the subsection from which this request config was read */
   abstract String id();
 
   /** request types that should be matched */
@@ -116,8 +121,8 @@
   /** pattern matching projects names */
   abstract ImmutableSet<Pattern> projectPatterns();
 
-  private static Builder builder() {
-    return new AutoValue_RequestConfig.Builder();
+  private static Builder builder(Config cfg, String section, String id) {
+    return new AutoValue_RequestConfig.Builder().cfg(cfg).section(section).id(id);
   }
 
   /**
@@ -185,6 +190,10 @@
 
   @AutoValue.Builder
   abstract static class Builder {
+    abstract Builder cfg(Config cfg);
+
+    abstract Builder section(String section);
+
     abstract Builder id(String id);
 
     abstract Builder requestTypes(ImmutableSet<String> requestTypes);
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 076ba46..30e1a54 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -79,6 +79,7 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.CreateGroupPermissionSyncer;
+import com.google.gerrit.server.DeadlineChecker;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.ExceptionHookImpl;
@@ -279,6 +280,7 @@
     factory(ChangeData.AssistedFactory.class);
     factory(ChangeJson.AssistedFactory.class);
     factory(ChangeIsVisibleToPredicate.Factory.class);
+    factory(DeadlineChecker.Factory.class);
     factory(MergeUtil.Factory.class);
     factory(PatchScriptFactory.Factory.class);
     factory(PatchScriptFactoryForAutoFix.Factory.class);
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index fd9e6b9..1803aab 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -352,6 +352,7 @@
   private final Config config;
   private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
   private final CreateRefControl createRefControl;
+  private final DeadlineChecker.Factory deadlineCheckerFactory;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final DynamicSet<PluginPushOption> pluginPushOptions;
   private final PluginSetContext<ReceivePackInitializer> initializers;
@@ -437,6 +438,7 @@
       BranchCommitValidator.Factory commitValidatorFactory,
       CreateGroupPermissionSyncer createGroupPermissionSyncer,
       CreateRefControl createRefControl,
+      DeadlineChecker.Factory deadlineCheckerFactory,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       DynamicSet<PluginPushOption> pluginPushOptions,
       PluginSetContext<ReceivePackInitializer> initializers,
@@ -487,6 +489,7 @@
     this.config = config;
     this.createRefControl = createRefControl;
     this.createGroupPermissionSyncer = createGroupPermissionSyncer;
+    this.deadlineCheckerFactory = deadlineCheckerFactory;
     this.editUtil = editUtil;
     this.hashtagsFactory = hashtagsFactory;
     this.setTopicFactory = setTopicFactory;
@@ -652,7 +655,8 @@
       try (RequestStateContext requestStateContext =
           RequestStateContext.open()
               .addRequestStateProvider(progress)
-              .addRequestStateProvider(new DeadlineChecker(start, clientProvidedDeadlineValue))) {
+              .addRequestStateProvider(
+                  deadlineCheckerFactory.create(start, requestInfo, clientProvidedDeadlineValue))) {
         processCommandsUnsafe(commands, progress);
         rejectRemaining(commands, INTERNAL_SERVER_ERROR);
       } catch (InvalidDeadlineException e) {
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index f95e72e..d79cd71 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -42,6 +42,7 @@
   @Inject private DynamicSet<PerformanceLogger> performanceLoggers;
   @Inject private PluginSetContext<RequestListener> requestListeners;
   @Inject @GerritServerConfig private Config config;
+  @Inject private DeadlineChecker.Factory deadlineCheckerFactory;
 
   @Option(name = "--trace", usage = "enable request tracing")
   private boolean trace;
@@ -65,30 +66,32 @@
             stderr = toPrintWriter(err);
             try (TraceContext traceContext = enableTracing();
                 PerformanceLogContext performanceLogContext =
-                    new PerformanceLogContext(config, performanceLoggers);
-                RequestStateContext requestStateContext =
-                    RequestStateContext.open()
-                        .addRequestStateProvider(new DeadlineChecker(deadline))) {
+                    new PerformanceLogContext(config, performanceLoggers)) {
               RequestInfo requestInfo =
                   RequestInfo.builder(RequestInfo.RequestType.SSH, user, traceContext).build();
-              requestListeners.runEach(l -> l.onRequest(requestInfo));
-              SshCommand.this.run();
-            } catch (InvalidDeadlineException e) {
-              stderr.println(e.getMessage());
-            } catch (RuntimeException e) {
-              Optional<RequestCancelledException> requestCancelledException =
-                  RequestCancelledException.getFromCausalChain(e);
-              if (!requestCancelledException.isPresent()) {
-                Throwables.throwIfUnchecked(e);
+              try (RequestStateContext requestStateContext =
+                  RequestStateContext.open()
+                      .addRequestStateProvider(
+                          deadlineCheckerFactory.create(requestInfo, deadline))) {
+                requestListeners.runEach(l -> l.onRequest(requestInfo));
+                SshCommand.this.run();
+              } catch (InvalidDeadlineException e) {
+                stderr.println(e.getMessage());
+              } catch (RuntimeException e) {
+                Optional<RequestCancelledException> requestCancelledException =
+                    RequestCancelledException.getFromCausalChain(e);
+                if (!requestCancelledException.isPresent()) {
+                  Throwables.throwIfUnchecked(e);
+                }
+                StringBuilder msg =
+                    new StringBuilder(requestCancelledException.get().formatCancellationReason());
+                if (requestCancelledException.get().getCancellationMessage().isPresent()) {
+                  msg.append(
+                      String.format(
+                          " (%s)", requestCancelledException.get().getCancellationMessage().get()));
+                }
+                stderr.println(msg.toString());
               }
-              StringBuilder msg =
-                  new StringBuilder(requestCancelledException.get().formatCancellationReason());
-              if (requestCancelledException.get().getCancellationMessage().isPresent()) {
-                msg.append(
-                    String.format(
-                        " (%s)", requestCancelledException.get().getCancellationMessage().get()));
-              }
-              stderr.println(msg.toString());
             } finally {
               stdout.flush();
               stderr.flush();
diff --git a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
index d94bae6..fe9124d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
@@ -193,6 +193,204 @@
     response.assertCreated();
   }
 
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  public void abortIfServerDeadlineExceeded() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.foo.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.bar.timeout", value = "100ms")
+  public void stricterDeadlineTakesPrecedence() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.requestType", value = "REST")
+  public void abortIfServerDeadlineExceeded_requestType() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.requestUriPattern", value = "/projects/.*")
+  public void abortIfServerDeadlineExceeded_requestUriPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.projectPattern", value = ".*new.*")
+  public void abortIfServerDeadlineExceeded_projectPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.account", value = "1000000")
+  public void abortIfServerDeadlineExceeded_account() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.requestType", value = "SSH")
+  public void nonMatchingServerDeadlineIsIgnored_requestType() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.requestUriPattern", value = "/changes/.*")
+  public void nonMatchingServerDeadlineIsIgnored_requestUriPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.projectPattern", value = ".*foo.*")
+  public void nonMatchingServerDeadlineIsIgnored_projectPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.account", value = "999")
+  public void nonMatchingServerDeadlineIsIgnored_account() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1")
+  public void invalidServerDeadlineIsIgnored_missingTimeUnit() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1x")
+  public void invalidServerDeadlineIsIgnored_invalidTimeUnit() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "invalid")
+  public void invalidServerDeadlineIsIgnored_invalidValue() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.requestType", value = "INVALID")
+  public void invalidServerDeadlineIsIgnored_invalidRequestType() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.requestUriPattern", value = "][")
+  public void invalidServerDeadlineIsIgnored_invalidRequestUriPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.projectPattern", value = "][")
+  public void invalidServerDeadlineIsIgnored_invalidProjectPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.account", value = "invalid")
+  public void invalidServerDeadlineIsIgnored_invalidAccount() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.requestType", value = "REST")
+  public void deadlineConfigWithoutTimeoutIsIgnored() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "0ms")
+  @GerritConfig(name = "deadline.default.requestType", value = "REST")
+  public void deadlineConfigWithZeroTimeoutIsIgnored() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "500ms")
+  public void exceededDeadlineForOneRequestDoesntAbortFollowUpRequest() throws Exception {
+    ProjectCreationValidationListener projectCreationValidationListener =
+        new ProjectCreationValidationListener() {
+          @Override
+          public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+            try {
+              Thread.sleep(1000);
+            } catch (InterruptedException e) {
+              throw new RuntimeException("interrupted during sleep");
+            }
+          }
+        };
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationValidationListener)) {
+      RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+      assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+      assertThat(response.getEntityContent())
+          .isEqualTo("Server Deadline Exceeded\n\ntimeout=500ms");
+    }
+    // verify that the exceeded deadline for the previous request, isn't applied to a new request
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new2"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  public void clientProvidedDeadlineOverridesServerDeadline() throws Exception {
+    RestResponse response =
+        adminRestSession.putWithHeaders(
+            "/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "2ms"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+    assertThat(response.getEntityContent())
+        .isEqualTo("Client Provided Deadline Exceeded\n\ntimeout=2ms");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  public void clientCanDisableDeadlineBySettingZeroAsDeadline() throws Exception {
+    RestResponse response =
+        adminRestSession.putWithHeaders(
+            "/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "0"));
+    response.assertCreated();
+  }
+
   @Test
   public void handleClientDisconnectedForPush() throws Exception {
     CommitValidationListener commitValidationListener =
@@ -299,6 +497,14 @@
   }
 
   @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  public void abortPushIfServerDeadlineExceeded() throws Exception {
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertErrorStatus("Server Deadline Exceeded (timeout=1ms)");
+  }
+
+  @Test
   @GerritConfig(name = "receive.timeout", value = "1ms")
   @GerritConfig(
       name = "experiments.enabled",
@@ -318,6 +524,18 @@
   }
 
   @Test
+  @GerritConfig(name = "receive.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.timeout", value = "10s")
+  @GerritConfig(
+      name = "experiments.enabled",
+      value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_PUSH_CANCELLATION)
+  public void receiveTimeoutTakesPrecedence() throws Exception {
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertErrorStatus("Server Deadline Exceeded (timeout=1ms)");
+  }
+
+  @Test
   public void abortPushIfClientProvidedDeadlineExceeded() throws Exception {
     List<String> pushOptions = new ArrayList<>();
     pushOptions.add("deadline=1ms");
@@ -368,6 +586,17 @@
   }
 
   @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  public void clientProvidedDeadlineOnPushOverridesServerDeadline() throws Exception {
+    List<String> pushOptions = new ArrayList<>();
+    pushOptions.add("deadline=2ms");
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    push.setPushOptions(pushOptions);
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertErrorStatus("Client Provided Deadline Exceeded (timeout=2ms)");
+  }
+
+  @Test
   @GerritConfig(name = "receive.timeout", value = "1ms")
   @GerritConfig(
       name = "experiments.enabled",
@@ -380,4 +609,15 @@
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertErrorStatus("Server Deadline Exceeded (timeout=1ms)");
   }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  public void clientCanDisableDeadlineOnPushBySettingZeroAsDeadline() throws Exception {
+    List<String> pushOptions = new ArrayList<>();
+    pushOptions.add("deadline=0");
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    push.setPushOptions(pushOptions);
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
index 113382e..d08e219 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.acceptance.ExtensionRegistry;
 import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.server.cancellation.RequestCancelledException;
 import com.google.gerrit.server.cancellation.RequestStateProvider;
 import com.google.gerrit.server.project.CreateProjectArgs;
@@ -147,4 +148,24 @@
     adminSshSession.exec("gerrit create-project --deadline 10m " + name("new"));
     adminSshSession.assertSuccess();
   }
+
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  public void abortIfServerDeadlineExceeded() throws Exception {
+    adminSshSession.exec("gerrit create-project " + name("new"));
+    adminSshSession.assertFailure("Server Deadline Exceeded (timeout=1ms)");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  public void clientProvidedDeadlineOverridesServerDeadline() throws Exception {
+    adminSshSession.exec("gerrit create-project --deadline 2ms " + name("new"));
+    adminSshSession.assertFailure("Client Provided Deadline Exceeded (timeout=2ms)");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  public void clientCanDisableDeadlineBySettingZeroAsDeadline() throws Exception {
+    adminSshSession.exec("gerrit create-project --deadline 0 " + name("new"));
+    adminSshSession.assertSuccess();
+  }
 }