Merge changes Id316cffa,I14b8f005
* changes:
Add hook for exceptions
Add a dedicated exception that is thrown in case of Git update failures
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 77ef60d..74ed725 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2738,6 +2738,20 @@
}
----
+[[exception-hook]]
+== ExceptionHook
+
+An `ExceptionHook` allows implementors to control how certain
+exceptions should be handled.
+
+This interface is intended to be implemented for multi-master setups to
+control the behavior for handling exceptions that are thrown by a lower
+layer that handles the consensus and synchronization between different
+server nodes. E.g. if an operation fails because consensus for a Git
+update could not be achieved (e.g. due to slow responding server nodes)
+this interface can be used to retry the request instead of failing it
+immediately.
+
[[quota-enforcer]]
== Quota Enforcer
diff --git a/java/com/google/gerrit/git/BUILD b/java/com/google/gerrit/git/BUILD
index 5ece37a..1edba38 100644
--- a/java/com/google/gerrit/git/BUILD
+++ b/java/com/google/gerrit/git/BUILD
@@ -7,6 +7,8 @@
deps = [
"//java/com/google/gerrit/common:annotations",
"//lib:guava",
+ "//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/jgit/org.eclipse.jgit:jgit",
],
)
diff --git a/java/com/google/gerrit/git/GitUpdateFailureException.java b/java/com/google/gerrit/git/GitUpdateFailureException.java
new file mode 100644
index 0000000..76ef217
--- /dev/null
+++ b/java/com/google/gerrit/git/GitUpdateFailureException.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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 com.google.gerrit.git;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Thrown when updating a ref in Git fails. */
+public class GitUpdateFailureException extends IOException {
+ private static final long serialVersionUID = 1L;
+
+ private final ImmutableList<GitUpdateFailure> failures;
+
+ public GitUpdateFailureException(String message, RefUpdate refUpdate) {
+ super(message);
+ this.failures = ImmutableList.of(GitUpdateFailure.create(refUpdate));
+ }
+
+ public GitUpdateFailureException(String message, BatchRefUpdate batchRefUpdate) {
+ super(message);
+ this.failures =
+ batchRefUpdate.getCommands().stream()
+ .filter(c -> c.getResult() != ReceiveCommand.Result.OK)
+ .map(GitUpdateFailure::create)
+ .collect(toImmutableList());
+ }
+
+ /** @return the names of the refs for which the update failed. */
+ public ImmutableList<String> getFailedRefs() {
+ return failures.stream().map(GitUpdateFailure::ref).collect(toImmutableList());
+ }
+
+ /** @return the failures that caused this exception. */
+ @UsedAt(UsedAt.Project.GOOGLE)
+ public ImmutableList<GitUpdateFailure> getFailures() {
+ return failures;
+ }
+
+ @AutoValue
+ public abstract static class GitUpdateFailure {
+ private static GitUpdateFailure create(RefUpdate refUpdate) {
+ return builder().ref(refUpdate.getName()).result(refUpdate.getResult().name()).build();
+ }
+
+ private static GitUpdateFailure create(ReceiveCommand receiveCommand) {
+ return builder()
+ .ref(receiveCommand.getRefName())
+ .result(receiveCommand.getResult().name())
+ .message(receiveCommand.getMessage())
+ .build();
+ }
+
+ public abstract String ref();
+
+ public abstract String result();
+
+ public abstract Optional<String> message();
+
+ public static GitUpdateFailure.Builder builder() {
+ return new AutoValue_GitUpdateFailureException_GitUpdateFailure.Builder();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder ref(String ref);
+
+ abstract Builder result(String result);
+
+ abstract Builder message(@Nullable String message);
+
+ abstract GitUpdateFailure build();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/git/LockFailureException.java b/java/com/google/gerrit/git/LockFailureException.java
index 9e67d70..371488d 100644
--- a/java/com/google/gerrit/git/LockFailureException.java
+++ b/java/com/google/gerrit/git/LockFailureException.java
@@ -14,36 +14,18 @@
package com.google.gerrit.git;
-import static com.google.common.collect.ImmutableList.toImmutableList;
-
-import com.google.common.collect.ImmutableList;
-import java.io.IOException;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.transport.ReceiveCommand;
/** Thrown when updating a ref in Git fails with LOCK_FAILURE. */
-public class LockFailureException extends IOException {
+public class LockFailureException extends GitUpdateFailureException {
private static final long serialVersionUID = 1L;
- private final ImmutableList<String> refs;
-
public LockFailureException(String message, RefUpdate refUpdate) {
- super(message);
- refs = ImmutableList.of(refUpdate.getName());
+ super(message, refUpdate);
}
public LockFailureException(String message, BatchRefUpdate batchRefUpdate) {
- super(message);
- refs =
- batchRefUpdate.getCommands().stream()
- .filter(c -> c.getResult() == ReceiveCommand.Result.LOCK_FAILURE)
- .map(ReceiveCommand::getRefName)
- .collect(toImmutableList());
- }
-
- /** Subset of ref names that caused the lock failure. */
- public ImmutableList<String> getFailedRefs() {
- return refs;
+ super(message, batchRefUpdate);
}
}
diff --git a/java/com/google/gerrit/git/RefUpdateUtil.java b/java/com/google/gerrit/git/RefUpdateUtil.java
index 520d0f2..fa7b98f 100644
--- a/java/com/google/gerrit/git/RefUpdateUtil.java
+++ b/java/com/google/gerrit/git/RefUpdateUtil.java
@@ -99,7 +99,7 @@
if (lockFailure + aborted == bru.getCommands().size()) {
throw new LockFailureException("Update aborted with one or more lock failures: " + bru, bru);
} else if (failure > 0) {
- throw new IOException("Update failed: " + bru);
+ throw new GitUpdateFailureException("Update failed: " + bru, bru);
}
}
@@ -130,7 +130,8 @@
case REJECTED_CURRENT_BRANCH:
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
- throw new IOException("Failed to update " + ru.getName() + ": " + ru.getResult());
+ throw new GitUpdateFailureException(
+ "Failed to update " + ru.getName() + ": " + ru.getResult(), ru);
}
}
@@ -174,7 +175,8 @@
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
default:
- throw new IOException("Failed to delete " + refName + ": " + ru.getResult());
+ throw new GitUpdateFailureException(
+ "Failed to delete " + refName + ": " + ru.getResult(), ru);
}
}
diff --git a/java/com/google/gerrit/server/ExceptionHook.java b/java/com/google/gerrit/server/ExceptionHook.java
new file mode 100644
index 0000000..ea76330
--- /dev/null
+++ b/java/com/google/gerrit/server/ExceptionHook.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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 com.google.gerrit.server;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Allows implementors to control how certain exceptions should be handled.
+ *
+ * <p>This interface is intended to be implemented for multi-master setups to control the behavior
+ * for handling exceptions that are thrown by a lower layer that handles the consensus and
+ * synchronization between different server nodes. E.g. if an operation fails because consensus for
+ * a Git update could not be achieved (e.g. due to slow responding server nodes) this interface can
+ * be used to retry the request instead of failing it immediately.
+ */
+@ExtensionPoint
+public interface ExceptionHook {
+ /**
+ * Whether an operation should be retried if it failed with the given throwable.
+ *
+ * <p>Only affects operations that are executed with {@link
+ * com.google.gerrit.server.update.RetryHelper}.
+ *
+ * @param throwable throwable that was thrown while executing the operation
+ * @return whether the operation should be retried
+ */
+ default boolean shouldRetry(Throwable throwable) {
+ return false;
+ }
+}
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 6a23084..1b1fab6 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -32,6 +32,7 @@
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.git.GitUpdateFailureException;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
@@ -268,7 +269,7 @@
if (command.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
throw new LockFailureException(message, batchUpdate);
}
- throw new IOException(message);
+ throw new GitUpdateFailureException(message, batchUpdate);
}
}
}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 45b70b2..3794f04 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -78,6 +78,7 @@
import com.google.gerrit.server.CmdLineParserModule;
import com.google.gerrit.server.CreateGroupPermissionSyncer;
import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.TraceRequestListener;
@@ -390,6 +391,7 @@
DynamicSet.setOf(binder(), RequestListener.class);
DynamicSet.bind(binder(), RequestListener.class).to(TraceRequestListener.class);
DynamicSet.setOf(binder(), ChangeETagComputation.class);
+ DynamicSet.setOf(binder(), ExceptionHook.class);
DynamicMap.mapOf(binder(), MailFilter.class);
bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index d8829bf..aa735f9 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -18,6 +18,7 @@
import com.google.common.base.MoreObjects;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.git.GitUpdateFailureException;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.git.ObjectIds;
import com.google.gerrit.reviewdb.client.Change;
@@ -433,13 +434,14 @@
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
default:
- throw new IOException(
+ throw new GitUpdateFailureException(
"Cannot update "
+ ru.getName()
+ " in "
+ db.getDirectory()
+ ": "
- + ru.getResult());
+ + ru.getResult(),
+ ru);
}
}
};
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index b120379..bea3867 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -39,10 +39,12 @@
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Field;
import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.RequestId;
import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.time.Duration;
@@ -182,14 +184,19 @@
private final Metrics metrics;
private final BatchUpdate.Factory updateFactory;
+ private final PluginSetContext<ExceptionHook> exceptionHooks;
private final Map<ActionType, Duration> defaultTimeouts;
private final WaitStrategy waitStrategy;
@Nullable private final Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup;
private final boolean retryWithTraceOnFailure;
@Inject
- RetryHelper(@GerritServerConfig Config cfg, Metrics metrics, BatchUpdate.Factory updateFactory) {
- this(cfg, metrics, updateFactory, null);
+ RetryHelper(
+ @GerritServerConfig Config cfg,
+ Metrics metrics,
+ PluginSetContext<ExceptionHook> exceptionHooks,
+ BatchUpdate.Factory updateFactory) {
+ this(cfg, metrics, updateFactory, exceptionHooks, null);
}
@VisibleForTesting
@@ -197,9 +204,11 @@
@GerritServerConfig Config cfg,
Metrics metrics,
BatchUpdate.Factory updateFactory,
+ PluginSetContext<ExceptionHook> exceptionHooks,
@Nullable Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup) {
this.metrics = metrics;
this.updateFactory = updateFactory;
+ this.exceptionHooks = exceptionHooks;
Duration defaultTimeout =
Duration.ofMillis(
@@ -308,6 +317,11 @@
return true;
}
+ // Exception hooks may identify additional exceptions for retry.
+ if (exceptionHooks.stream().anyMatch(h -> h.shouldRetry(t))) {
+ return true;
+ }
+
// A non-recoverable failure occurred. Check if we should retry to capture a trace
// of the failure. If a trace was already done there is no need to retry.
if (retryWithTraceOnFailure
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 405c463..eb59669 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -134,6 +134,8 @@
import com.google.gerrit.server.notedb.Sequences;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
+import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.RefPattern;
import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -2589,7 +2591,11 @@
externalIds,
metaDataUpdateInternalFactory,
new RetryHelper(
- cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
+ cfg,
+ retryMetrics,
+ null,
+ new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
+ r -> r.withBlockStrategy(noSleepBlockStrategy)),
extIdNotesFactory,
ident,
ident,
@@ -2642,6 +2648,7 @@
cfg,
retryMetrics,
null,
+ new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
r ->
r.withStopStrategy(StopStrategies.stopAfterAttempt(status.size()))
.withBlockStrategy(noSleepBlockStrategy)),
@@ -2696,7 +2703,11 @@
externalIds,
metaDataUpdateInternalFactory,
new RetryHelper(
- cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
+ cfg,
+ retryMetrics,
+ null,
+ new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
+ r -> r.withBlockStrategy(noSleepBlockStrategy)),
extIdNotesFactory,
ident,
ident,
@@ -2765,7 +2776,11 @@
externalIds,
metaDataUpdateInternalFactory,
new RetryHelper(
- cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
+ cfg,
+ retryMetrics,
+ null,
+ new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
+ r -> r.withBlockStrategy(noSleepBlockStrategy)),
extIdNotesFactory,
ident,
ident,
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index 7b743f0..0e51208 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -36,6 +36,7 @@
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.httpd.restapi.ParameterParser;
import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.validators.CommitValidationException;
@@ -85,6 +86,7 @@
@Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
@Inject private DynamicSet<PerformanceLogger> performanceLoggers;
@Inject private DynamicSet<SubmitRule> submitRules;
+ @Inject private DynamicSet<ExceptionHook> exceptionHooks;
@Inject private WorkQueue workQueue;
private TraceValidatingProjectCreationValidationListener projectCreationListener;
@@ -606,6 +608,36 @@
}
@Test
+ @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
+ public void noAutoRetryIfExceptionCausesNormalRetrying() throws Exception {
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+
+ TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+ traceSubmitRule.failAlways = true;
+ RegistrationHandle submitRuleRegistrationHandle = submitRules.add("gerrit", traceSubmitRule);
+ RegistrationHandle exceptionHookRegistrationHandle =
+ exceptionHooks.add(
+ "gerrit",
+ new ExceptionHook() {
+ @Override
+ public boolean shouldRetry(Throwable t) {
+ return true;
+ }
+ });
+ try {
+ RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+ assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(traceSubmitRule.traceId).isNull();
+ assertThat(traceSubmitRule.isLoggingForced).isFalse();
+ } finally {
+ submitRuleRegistrationHandle.remove();
+ exceptionHookRegistrationHandle.remove();
+ }
+ }
+
+ @Test
public void noAutoRetryWithTraceIfDisabled() throws Exception {
String changeId = createChange().getChangeId();
approve(changeId);