Merge "Auto-repair replication on missing necessary objects error"
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
index 867ffa4..f83e1df 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -139,6 +139,7 @@
boolean isRescheduled;
boolean isFailed;
RemoteRefUpdate.Status failedStatus;
+ URIish failoverTo;
}
private final ReplicationStateListener stateLog;
@@ -600,9 +601,7 @@
// second one fails, it will also be rescheduled and then,
// here, find out replication to its URI is already pending
// for retry (blocking).
- pendingPushOp.addRefBatches(pushOp.getRefs());
- pendingPushOp.addStates(pushOp.getStates());
- pushOp.removeStates();
+ consolidateOnto(pendingPushOp, pushOp);
} else {
// The one pending is one that is NOT retrying, it was just
@@ -619,9 +618,7 @@
pendingPushOp.canceledByReplication();
queue.pending.remove(uri);
- pushOp.addRefBatches(pendingPushOp.getRefs());
- pushOp.addStates(pendingPushOp.getStates());
- pendingPushOp.removeStates();
+ consolidateOnto(pushOp, pendingPushOp);
}
}
@@ -642,11 +639,21 @@
: REJECTED_OTHER_REASON;
status.isFailed = true;
if (pushOp.setToRetry()) {
- status.isRescheduled = true;
- replicationTasksStorage.get().reset(pushOp);
- @SuppressWarnings("unused")
- ScheduledFuture<?> ignored2 =
- pool.schedule(pushOp, config.getRetryDelay(), TimeUnit.MINUTES);
+ URIish nextUri =
+ urlDistributor.failover(
+ getURIs(pushOp.getProjectNameKey(), null), pushOp.getURI());
+ if (!nextUri.equals(pushOp.getURI())) {
+ // Defer actual failoverTo until after this write lock is released
+ pushOp.canceledByReplication();
+ queue.pending.remove(uri);
+ status.failoverTo = nextUri;
+ } else {
+ status.isRescheduled = true;
+ replicationTasksStorage.get().reset(pushOp);
+ @SuppressWarnings("unused")
+ ScheduledFuture<?> ignored2 =
+ pool.schedule(pushOp, config.getRetryDelay(), TimeUnit.MINUTES);
+ }
} else {
pushOp.canceledByReplication();
pushOp.retryDone();
@@ -667,6 +674,51 @@
if (status.isRescheduled) {
postReplicationScheduledEvent(pushOp);
}
+ if (status.failoverTo != null) {
+ failoverTo(pushOp, status.failoverTo);
+ }
+ }
+
+ private void failoverTo(PushOne pushOp, URIish newUri) {
+ PushOne replacement = opFactory.create(pushOp.getProjectNameKey(), newUri);
+ replacement.addRefBatches(pushOp.getRefs());
+ replacement.addStates(pushOp.getStates());
+ replacement.setToRetryWithCount(pushOp.getRetryCount());
+ pushOp.removeStates();
+
+ boolean installed =
+ stateLock.withWriteLock(
+ newUri,
+ () -> {
+ for (ReplicationTasksStorage.ReplicateRefUpdate u :
+ replacement.getReplicateRefUpdates()) {
+ replicationTasksStorage.get().create(u);
+ }
+ PushOne existing = getPendingPush(newUri);
+ if (existing != null) {
+ consolidateOnto(existing, replacement);
+ replicationTasksStorage.get().finish(pushOp);
+ return false;
+ }
+ replicationTasksStorage.get().finish(pushOp);
+ queue.pending.put(newUri, replacement);
+ @SuppressWarnings("unused")
+ ScheduledFuture<?> ignored =
+ pool.schedule(replacement, config.getRetryDelay(), TimeUnit.MINUTES);
+ return true;
+ });
+ repLog.atInfo().log(
+ "Failover: replication for %s rerouted from %s to %s (retry %d)",
+ pushOp.getProjectNameKey(), pushOp.getURI(), newUri, replacement.getRetryCount());
+ if (installed) {
+ postReplicationScheduledEvent(replacement);
+ }
+ }
+
+ private static void consolidateOnto(PushOne into, PushOne from) {
+ into.addRefBatches(from.getRefs());
+ into.addStates(from.getStates());
+ from.removeStates();
}
RunwayStatus requestRunway(PushOne op) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
index 232aa23..caf3236 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -266,11 +266,19 @@
}
boolean setToRetry() {
+ return setToRetryWithCount(retryCount + 1);
+ }
+
+ boolean setToRetryWithCount(int count) {
retrying = true;
- retryCount++;
+ retryCount = count;
return maxRetries == 0 || retryCount <= maxRetries;
}
+ int getRetryCount() {
+ return retryCount;
+ }
+
void retryDone() {
this.retrying = false;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationLogFile.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationLogFile.java
index 622034a..7e51f9f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationLogFile.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationLogFile.java
@@ -22,8 +22,11 @@
import com.google.gerrit.util.logging.JsonLogEntry;
import com.google.gson.annotations.SerializedName;
import com.google.inject.Inject;
+import java.util.HashMap;
+import java.util.Map;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.ThrowableInformation;
public class ReplicationLogFile extends PluginLogFile {
@@ -43,13 +46,18 @@
private class ReplicationJsonLogEntry extends JsonLogEntry {
public String timestamp;
public String message;
+ public Map<String, String> exception;
@SerializedName("@version")
- public final int version = 1;
+ public final int version = 2;
public ReplicationJsonLogEntry(LoggingEvent event) {
timestamp = timestampFormatter.format(event.getTimeStamp());
message = (String) event.getMessage();
+
+ if (event.getThrowableInformation() != null) {
+ this.exception = getException(event.getThrowableInformation());
+ }
}
}
@@ -57,5 +65,25 @@
public JsonLogEntry toJsonLogEntry(LoggingEvent event) {
return new ReplicationJsonLogEntry(event);
}
+
+ private Map<String, String> getException(ThrowableInformation throwable) {
+ HashMap<String, String> exceptionInformation = new HashMap<>();
+
+ String throwableName = throwable.getThrowable().getClass().getCanonicalName();
+ if (throwableName != null) {
+ exceptionInformation.put("exception_class", throwableName);
+ }
+
+ String throwableMessage = throwable.getThrowable().getMessage();
+ if (throwableMessage != null) {
+ exceptionInformation.put("exception_message", throwableMessage);
+ }
+
+ String[] stackTrace = throwable.getThrowableStrRep();
+ if (stackTrace != null) {
+ exceptionInformation.put("stacktrace", String.join("\n", stackTrace));
+ }
+ return exceptionInformation;
+ }
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/UrlDistributionStrategy.java b/src/main/java/com/googlesource/gerrit/plugins/replication/UrlDistributionStrategy.java
index 46183eb..1230e36 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/UrlDistributionStrategy.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/UrlDistributionStrategy.java
@@ -41,17 +41,36 @@
* Push to one URL at a time, rotating through the list on each push event. Particularly useful
* when multiple replica hosts share a single backend (likely via NFS): pushing to all URLs would
* cause redundant writes to the same underlying storage, while round-robin distributes load
- * evenly and ensures each push is executed exactly once.
+ * evenly and ensures each push is executed exactly once. On transport failure {@link
+ * Instance#failover} hands the push over to the next URL in the rotation.
*/
ROUND_ROBIN("roundRobin") {
@Override
public Instance newInstance() {
- final AtomicInteger index = new AtomicInteger();
- return candidates -> {
- if (candidates.isEmpty()) {
- return List.of();
+ return new Instance() {
+ private final AtomicInteger index = new AtomicInteger();
+
+ @Override
+ public List<URIish> select(List<URIish> candidates) {
+ if (candidates.isEmpty()) {
+ return List.of();
+ }
+ return List.of(candidates.get(Math.floorMod(index.getAndIncrement(), candidates.size())));
}
- return List.of(candidates.get(Math.floorMod(index.getAndIncrement(), candidates.size())));
+
+ @Override
+ public URIish failover(List<URIish> candidates, URIish failed) {
+ if (candidates.size() < 2) {
+ return failed;
+ }
+ for (int attempt = 0; attempt < candidates.size(); attempt++) {
+ URIish next = candidates.get(Math.floorMod(index.getAndIncrement(), candidates.size()));
+ if (!next.equals(failed)) {
+ return next;
+ }
+ }
+ return failed;
+ }
};
}
};
@@ -79,6 +98,15 @@
/** A stateful executor for a {@link UrlDistributionStrategy} strategy. */
@FunctionalInterface
public interface Instance {
+ /** Select the URLs to push to for this scheduling event. */
List<URIish> select(List<URIish> candidates);
+
+ /**
+ * If a push to any URI returned by {@link #select(List)} fails, {@link #failover(List,
+ * URIish)}} is invoked to select the next URI for retry.
+ */
+ default URIish failover(List<URIish> candidates, URIish failed) {
+ return failed;
+ }
}
}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index cf1b944..305bb28 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -741,7 +741,10 @@
hosts share a single NFS backend. Pushing to all URLs simultaneously
would cause redundant writes to the same underlying storage; round-robin
distributes load evenly and ensures each push is written exactly once.
- Has no effect if only one URL is configured.
+ On a transport error during retry, the next push attempt fails over to a
+ different URL in the rotation (bounded by `replicationRetry`), so a single
+ unreachable host does not block replication. Has no effect if only one URL
+ is configured.
Defaults to `all`.