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();
+ }
}