Merge branch 'stable-2.8'
* stable-2.8:
Fix JAR manifest claiming it is Commons IO project
Change-Id: Ie122fc40f10ad89271a9959f4bc599cedd9001ab
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 2f45466..17904c0 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -1,9 +1,9 @@
#Fri Jul 16 23:39:13 PDT 2010
eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
-org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.compliance=1.7
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
-org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.compiler.source=1.7
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
diff --git a/BUCK b/BUCK
index af236dc..db23cd9 100644
--- a/BUCK
+++ b/BUCK
@@ -19,6 +19,11 @@
srcs = glob(['src/test/java/**/*.java']),
deps = [
':replication__plugin__compile',
+ '//gerrit-common:server',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//lib:easymock',
+ '//lib:gwtorm',
'//lib:junit',
'//lib/jgit:jgit',
],
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index 73f1f39..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2012 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.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <groupId>com.googlesource.gerrit.plugins.replication</groupId>
- <artifactId>replication</artifactId>
- <name>replication</name>
- <packaging>jar</packaging>
- <version>2.8</version>
-
- <properties>
- <Gerrit-ApiType>plugin</Gerrit-ApiType>
- <Gerrit-ApiVersion>${project.version}</Gerrit-ApiVersion>
- </properties>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
- <version>2.4</version>
- <configuration>
- <archive>
- <manifestEntries>
- <Gerrit-PluginName>replication</Gerrit-PluginName>
-
- <Gerrit-Module>com.googlesource.gerrit.plugins.replication.ReplicationModule</Gerrit-Module>
- <Gerrit-SshModule>com.googlesource.gerrit.plugins.replication.SshModule</Gerrit-SshModule>
-
- <Implementation-Vendor>Gerrit Code Review</Implementation-Vendor>
- <Implementation-URL>http://code.google.com/p/gerrit/</Implementation-URL>
-
- <Implementation-Title>Plugin ${project.artifactId}</Implementation-Title>
- <Implementation-Version>${project.version}</Implementation-Version>
-
- <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
- <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
- </manifestEntries>
- </archive>
- </configuration>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <version>2.3.2</version>
- <configuration>
- <source>1.6</source>
- <target>1.6</target>
- <encoding>UTF-8</encoding>
- </configuration>
- </plugin>
- </plugins>
- </build>
-
- <dependencies>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId>
- <version>${Gerrit-ApiVersion}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <version>4.8.1</version>
- <scope>test</scope>
- </dependency>
-
- <dependency>
- <groupId>commons-io</groupId>
- <artifactId>commons-io</artifactId>
- <version>2.4</version>
- </dependency>
- </dependencies>
-
- <repositories>
- <repository>
- <id>gerrit-api-repository</id>
- <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
- </repository>
- </repositories>
-</project>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
new file mode 100644
index 0000000..a2b66d1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
@@ -0,0 +1,134 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.replication;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.replication.RemoteSiteUser;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+
+@Singleton
+public class AutoReloadConfigDecorator implements ReplicationConfig {
+ private static final Logger log = LoggerFactory
+ .getLogger(AutoReloadConfigDecorator.class);
+ private ReplicationFileBasedConfig currentConfig;
+ private long currentConfigTs;
+
+ private final Injector injector;
+ private final SitePaths site;
+ private final RemoteSiteUser.Factory remoteSiteUserFactory;
+ private final PluginUser pluginUser;
+ private final SchemaFactory<ReviewDb> db;
+ private final GitRepositoryManager gitRepositoryManager;
+ private final GroupBackend groupBackend;
+ private final WorkQueue workQueue;
+
+ @Inject
+ public AutoReloadConfigDecorator(Injector injector, SitePaths site,
+ RemoteSiteUser.Factory ruf, PluginUser pu, SchemaFactory<ReviewDb> db,
+ GitRepositoryManager grm, GroupBackend gb,
+ WorkQueue workQueue) throws ConfigInvalidException,
+ IOException {
+ this.injector = injector;
+ this.site = site;
+ this.remoteSiteUserFactory = ruf;
+ this.pluginUser = pu;
+ this.db = db;
+ this.gitRepositoryManager = grm;
+ this.groupBackend = gb;
+ this.currentConfig = loadConfig();
+ this.currentConfigTs = currentConfig.getCfgPath().lastModified();
+ this.workQueue = workQueue;
+ }
+
+ private ReplicationFileBasedConfig loadConfig()
+ throws ConfigInvalidException, IOException {
+ return new ReplicationFileBasedConfig(injector, site,
+ remoteSiteUserFactory, pluginUser, db, gitRepositoryManager,
+ groupBackend);
+ }
+
+ private synchronized boolean isAutoReload() {
+ return currentConfig.getConfig().getBoolean("gerrit", "autoReload", false);
+ }
+
+ @Override
+ public synchronized List<Destination> getDestinations() {
+ reloadIfNeeded();
+ return currentConfig.getDestinations();
+ }
+
+ private void reloadIfNeeded() {
+ if (isAutoReload()
+ && currentConfig.getCfgPath().lastModified() > currentConfigTs) {
+ try {
+ ReplicationFileBasedConfig newConfig = loadConfig();
+ newConfig.startup(workQueue);
+ int discarded = currentConfig.shutdown();
+
+ this.currentConfig = newConfig;
+ this.currentConfigTs = currentConfig.getCfgPath().lastModified();
+ log.info("Configuration reloaded: "
+ + currentConfig.getDestinations().size() + " destinations, "
+ + discarded + " replication events discarded");
+
+ } catch (Exception e) {
+ log.error(
+ "Cannot reload replication configuration: keeping existing settings",
+ e);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public synchronized boolean isReplicateAllOnPluginStart() {
+ return currentConfig.isReplicateAllOnPluginStart();
+ }
+
+ @Override
+ public synchronized boolean isDefaultForceUpdate() {
+ return currentConfig.isDefaultForceUpdate();
+ }
+
+ @Override
+ public synchronized boolean isEmpty() {
+ return currentConfig.isEmpty();
+ }
+
+ @Override
+ public synchronized int shutdown() {
+ return currentConfig.shutdown();
+ }
+
+ @Override
+ public synchronized void startup(WorkQueue workQueue) {
+ currentConfig.startup(workQueue);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
new file mode 100644
index 0000000..9d6ce69
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.replication;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class AutoReloadSecureCredentialsFactoryDecorator implements
+ CredentialsFactory {
+ private static final Logger log = LoggerFactory
+ .getLogger(AutoReloadSecureCredentialsFactoryDecorator.class);
+
+ private final AtomicReference<SecureCredentialsFactory> secureCredentialsFactory;
+ private volatile long secureCredentialsFactoryLoadTs;
+ private final SitePaths site;
+ private ReplicationFileBasedConfig config;
+
+ @Inject
+ public AutoReloadSecureCredentialsFactoryDecorator(SitePaths site,
+ ReplicationFileBasedConfig config) throws ConfigInvalidException,
+ IOException {
+ this.site = site;
+ this.config = config;
+ this.secureCredentialsFactory =
+ new AtomicReference<SecureCredentialsFactory>(
+ new SecureCredentialsFactory(site));
+ this.secureCredentialsFactoryLoadTs = getSecureConfigLastEditTs();
+ }
+
+ private long getSecureConfigLastEditTs() {
+ FileBasedConfig cfg = new FileBasedConfig(site.secure_config, FS.DETECTED);
+ if (cfg.getFile().exists()) {
+ return cfg.getFile().lastModified();
+ } else {
+ return 0L;
+ }
+ }
+
+ @Override
+ public SecureCredentialsProvider create(String remoteName) {
+ if (needsReload()) {
+ try {
+ secureCredentialsFactory.compareAndSet(secureCredentialsFactory.get(),
+ new SecureCredentialsFactory(site));
+ secureCredentialsFactoryLoadTs = getSecureConfigLastEditTs();
+ log.info("secure.config reloaded as it was updated on the file system");
+ } catch (Exception e) {
+ log.error("Unexpected error while trying to reload "
+ + "secure.config: keeping existing credentials", e);
+ }
+ }
+
+ return secureCredentialsFactory.get().create(remoteName);
+ }
+
+
+ private boolean needsReload() {
+ return config.getConfig().getBoolean("gerrit", "autoReload", false) &&
+ getSecureConfigLastEditTs() != secureCredentialsFactoryLoadTs;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java b/src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java
similarity index 69%
rename from src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java
rename to src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java
index ff88b87..9ce4a54 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2013 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.
@@ -11,16 +11,10 @@
// 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.googlesource.gerrit.plugins.replication;
-public enum ReplicationType {
- /** Replicate all after gerrit startup. */
- STARTUP,
+interface CredentialsFactory {
- /** Invoke ssh command to replicate. */
- COMMAND,
+ SecureCredentialsProvider create(String remoteName);
- /** After a git reference is updated, run the replicaton. */
- GIT_UPDATED;
}
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 19a9359..20b0a3f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -22,6 +22,7 @@
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PluginUser;
@@ -65,7 +66,7 @@
class Destination {
private static final Logger log = ReplicationQueue.log;
- private static final WrappedLogger wrappedLog = new WrappedLogger(log);
+ private static final ReplicationStateLogger stateLog = new ReplicationStateLogger(log);
private final int poolThreads;
private final String poolName;
@@ -76,6 +77,7 @@
private final int delay;
private final int retryDelay;
private final Object stateLock = new Object();
+ private final int lockErrorMaxRetries;
private final Map<URIish, PushOne> pending = new HashMap<URIish, PushOne>();
private final Map<URIish, PushOne> inFlight = new HashMap<URIish, PushOne>();
private final PushOne.Factory opFactory;
@@ -104,6 +106,7 @@
gitManager = gitRepositoryManager;
delay = Math.max(0, getInt(rc, cfg, "replicationdelay", 15));
retryDelay = Math.max(0, getInt(rc, cfg, "replicationretry", 1));
+ lockErrorMaxRetries = cfg.getInt("replication", "lockErrorMaxRetries", 0);
adminUrls = cfg.getStringList("remote", rc.getName(), "adminUrl");
poolThreads = Math.max(0, getInt(rc, cfg, "threads", 1));
@@ -210,7 +213,7 @@
return;
}
} catch (NoSuchProjectException err) {
- wrappedLog.error(String.format(
+ stateLog.error(String.format(
"source project %s not available", project), err, state);
return;
} catch (Exception e) {
@@ -227,7 +230,7 @@
try {
git = gitManager.openRepository(project);
} catch (IOException err) {
- wrappedLog.error(String.format(
+ stateLog.error(String.format(
"source project %s not available", project), err, state);
return;
}
@@ -235,11 +238,11 @@
Ref head = git.getRef(Constants.HEAD);
if (head != null
&& head.isSymbolic()
- && GitRepositoryManager.REF_CONFIG.equals(head.getLeaf().getName())) {
+ && RefNames.REFS_CONFIG.equals(head.getLeaf().getName())) {
return;
}
} catch (IOException err) {
- wrappedLog.error(String.format(
+ stateLog.error(String.format(
"cannot check type of project %s", project), err, state);
return;
} finally {
@@ -256,7 +259,7 @@
pending.put(uri, e);
}
e.addRef(ref);
- state.increasePushTaskCount();
+ state.increasePushTaskCount(project.get(), ref);
e.addState(ref, state);
}
}
@@ -433,7 +436,7 @@
}
boolean wouldPushRef(String ref) {
- if (!replicatePermissions && GitRepositoryManager.REF_CONFIG.equals(ref)) {
+ if (!replicatePermissions && RefNames.REFS_CONFIG.equals(ref)) {
return false;
}
for (RefSpec s : remote.getPushRefSpecs()) {
@@ -510,6 +513,10 @@
return adminUrls;
}
+ int getLockErrorMaxRetries() {
+ return lockErrorMaxRetries;
+ }
+
private static boolean matches(URIish uri, String urlMatch) {
if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) {
return true;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java b/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
index c752edc..37d47ca 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/OnStartStop.java
@@ -28,15 +28,18 @@
private final ServerInformation srvInfo;
private final PushAll.Factory pushAll;
private final ReplicationQueue queue;
+ private final ReplicationConfig config;
@Inject
OnStartStop(
ServerInformation srvInfo,
PushAll.Factory pushAll,
- ReplicationQueue queue) {
+ ReplicationQueue queue,
+ ReplicationConfig config) {
this.srvInfo = srvInfo;
this.pushAll = pushAll;
this.queue = queue;
+ this.config = config;
this.pushAllFuture = Atomics.newReference();
}
@@ -45,9 +48,8 @@
queue.start();
if (srvInfo.getState() == ServerInformation.State.STARTUP
- && queue.replicateAllOnPluginStart) {
- ReplicationState state =
- new ReplicationState(ReplicationType.STARTUP);
+ && config.isReplicateAllOnPluginStart()) {
+ ReplicationState state = new ReplicationState();
pushAllFuture.set(pushAll.create(null, state).schedule(30, TimeUnit.SECONDS));
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java b/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
index 25f4cde..165ef03 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushAll.java
@@ -29,7 +29,7 @@
class PushAll implements Runnable {
private static final Logger log = LoggerFactory.getLogger(PushAll.class);
- private static final WrappedLogger wrappedLog = new WrappedLogger(log);
+ private static final ReplicationStateLogger stateLog = new ReplicationStateLogger(log);
interface Factory {
PushAll create(String urlMatch, ReplicationState state);
@@ -62,7 +62,7 @@
replication.scheduleFullSync(nameKey, urlMatch, state);
}
} catch (Exception e) {
- wrappedLog.error("Cannot enumerate known projects", e, state);
+ stateLog.error("Cannot enumerate known projects", e, state);
}
state.markAllPushTasksScheduled();
}
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 afd7f91..876a4d5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -23,6 +23,7 @@
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.git.ChangeCache;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -38,7 +39,6 @@
import com.google.inject.assistedinject.Assisted;
import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
-
import com.jcraft.jsch.JSchException;
import org.eclipse.jgit.errors.NoRemoteRepositoryException;
@@ -77,7 +77,7 @@
*/
class PushOne implements ProjectRunnable {
private static final Logger log = ReplicationQueue.log;
- private static final WrappedLogger wrappedLog = new WrappedLogger(log);
+ private static final ReplicationStateLogger stateLog = new ReplicationStateLogger(log);
static final String ALL_REFS = "..all..";
interface Factory {
@@ -104,13 +104,15 @@
private boolean canceled;
private final Multimap<String,ReplicationState> stateMap =
LinkedListMultimap.create();
+ private final int maxLockRetries;
+ private int lockRetryCount;
@Inject
PushOne(final GitRepositoryManager grm,
final SchemaFactory<ReviewDb> s,
final Destination p,
final RemoteConfig c,
- final SecureCredentialsFactory cpFactory,
+ final CredentialsFactory cpFactory,
final TagCache tc,
final PerThreadRequestScope.Scoper ts,
final ChangeCache cc,
@@ -128,6 +130,8 @@
replicationQueue = rq;
projectName = d;
uri = u;
+ lockRetryCount = 0;
+ maxLockRetries = pool.getLockErrorMaxRetries();
}
@Override
@@ -266,7 +270,7 @@
git = gitManager.openRepository(projectName);
runImpl();
} catch (RepositoryNotFoundException e) {
- wrappedLog.error("Cannot replicate " + projectName
+ stateLog.error("Cannot replicate " + projectName
+ "; Local repository error: "
+ e.getMessage(), getStatesAsArray());
@@ -286,27 +290,38 @@
} catch (NoRemoteRepositoryException e) {
createRepository();
} catch (NotSupportedException e) {
- wrappedLog.error("Cannot replicate to " + uri, e, getStatesAsArray());
+ stateLog.error("Cannot replicate to " + uri, e, getStatesAsArray());
} catch (TransportException e) {
Throwable cause = e.getCause();
if (cause instanceof JSchException
&& cause.getMessage().startsWith("UnknownHostKey:")) {
log.error("Cannot replicate to " + uri + ": " + cause.getMessage());
+ } else if (e instanceof LockFailureException) {
+ lockRetryCount++;
+ // The LockFailureException message contains both URI and reason
+ // for this failure.
+ log.error("Cannot replicate to " + e.getMessage());
+
+ // The remote push operation should be retried.
+ if (lockRetryCount <= maxLockRetries) {
+ pool.reschedule(this, Destination.RetryReason.TRANSPORT_ERROR);
+ } else {
+ log.error("Giving up after " + lockRetryCount
+ + " of this error during replication to " + e.getMessage());
+ }
} else {
log.error("Cannot replicate to " + uri, e);
+ // The remote push operation should be retried.
+ pool.reschedule(this, Destination.RetryReason.TRANSPORT_ERROR);
}
-
- // The remote push operation should be retried.
- pool.reschedule(this, Destination.RetryReason.TRANSPORT_ERROR);
} catch (IOException e) {
- wrappedLog.error("Cannot replicate to " + uri, e, getStatesAsArray());
-
+ stateLog.error("Cannot replicate to " + uri, e, getStatesAsArray());
} catch (RuntimeException e) {
- wrappedLog.error("Unexpected error during replication to " + uri, e, getStatesAsArray());
+ stateLog.error("Unexpected error during replication to " + uri, e, getStatesAsArray());
} catch (Error e) {
- wrappedLog.error("Unexpected error during replication to " + uri, e, getStatesAsArray());
+ stateLog.error("Unexpected error during replication to " + uri, e, getStatesAsArray());
} finally {
if (git != null) {
@@ -336,11 +351,11 @@
log.warn("Missing repository created; retry replication to " + uri);
pool.reschedule(this, Destination.RetryReason.REPOSITORY_MISSING);
} catch (IOException ioe) {
- wrappedLog.error("Cannot replicate to " + uri + "; failed to create missing repository",
+ stateLog.error("Cannot replicate to " + uri + "; failed to create missing repository",
ioe, getStatesAsArray());
}
} else {
- wrappedLog.error("Cannot replicate to " + uri + "; repository not found", getStatesAsArray());
+ stateLog.error("Cannot replicate to " + uri + "; repository not found", getStatesAsArray());
}
}
@@ -374,6 +389,8 @@
return new PushResult();
}
+ log.info("Push to " + uri + " references: " + todo);
+
return tn.push(NullProgressMonitor.INSTANCE, todo);
}
@@ -406,7 +423,7 @@
try {
db = schema.open();
} catch (OrmException e) {
- wrappedLog.error("Cannot read database to replicate to " + projectName, e, getStatesAsArray());
+ stateLog.error("Cannot read database to replicate to " + projectName, e, getStatesAsArray());
return Collections.emptyList();
}
try {
@@ -474,8 +491,8 @@
}
private boolean canPushRef(String ref, boolean noPerms) {
- return !(noPerms && GitRepositoryManager.REF_CONFIG.equals(ref)) &&
- !ref.startsWith(GitRepositoryManager.REFS_CACHE_AUTOMERGE);
+ return !(noPerms && RefNames.REFS_CONFIG.equals(ref)) &&
+ !ref.startsWith(RefNames.REFS_CACHE_AUTOMERGE);
}
private Map<String, Ref> listRemote(Transport tn)
@@ -520,7 +537,8 @@
cmds.add(new RemoteRefUpdate(git, (Ref) null, dst, force, null, null));
}
- private void updateStates(Collection<RemoteRefUpdate> refUpdates) {
+ private void updateStates(Collection<RemoteRefUpdate> refUpdates)
+ throws LockFailureException {
Set<String> doneRefs = new HashSet<String>();
boolean anyRefFailed = false;
@@ -544,7 +562,7 @@
case REJECTED_NODELETE:
case REJECTED_NONFASTFORWARD:
case REJECTED_REMOTE_CHANGED:
- wrappedLog.error(String.format("Failed replicate of %s to %s: status %s",
+ stateLog.error(String.format("Failed replicate of %s to %s: status %s",
u.getRemoteName(), uri, u.getStatus()), logStatesArray);
pushStatus = RefPushResult.FAILED;
anyRefFailed = true;
@@ -552,12 +570,14 @@
case REJECTED_OTHER_REASON:
if ("non-fast-forward".equals(u.getMessage())) {
- wrappedLog.error(String.format("Failed replicate of %s to %s"
+ stateLog.error(String.format("Failed replicate of %s to %s"
+ ", remote rejected non-fast-forward push."
+ " Check receive.denyNonFastForwards variable in config file"
+ " of destination repository.", u.getRemoteName(), uri), logStatesArray);
+ } else if ("failed to lock".equals(u.getMessage())) {
+ throw new LockFailureException(uri, u.getMessage());
} else {
- wrappedLog.error(String.format(
+ stateLog.error(String.format(
"Failed replicate of %s to %s, reason: %s",
u.getRemoteName(), uri, u.getMessage()), logStatesArray);
}
@@ -585,4 +605,12 @@
}
stateMap.clear();
}
+
+ public class LockFailureException extends TransportException {
+ private static final long serialVersionUID = 1L;
+
+ public LockFailureException(URIish uri, String message) {
+ super(uri, message);
+ }
+ };
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java b/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java
index 0452b69..bc503ac 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushResultProcessing.java
@@ -14,17 +14,33 @@
package com.googlesource.gerrit.plugins.replication;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+
import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
import org.eclipse.jgit.transport.URIish;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class PushResultProcessing {
- abstract void onOneNodeReplicated(String project, String ref, URIish uri, RefPushResult status);
- abstract void onAllNodesReplicated(int totalPushTasksCount);
+ abstract void onRefReplicatedToOneNode(String project, String ref, URIish uri, RefPushResult status);
+
+ abstract void onRefReplicatedToAllNodes(String project, String ref, int nodesCount);
+
+ abstract void onAllRefsReplicatedToAllNodes(int totalPushTasksCount);
void writeStdOut(final String message) {
// Default doing nothing
@@ -57,11 +73,13 @@
}
@Override
- void onOneNodeReplicated(String project, String ref, URIish uri,
+ void onRefReplicatedToOneNode(String project, String ref, URIish uri,
RefPushResult status) {
StringBuilder sb = new StringBuilder();
sb.append("Replicate ");
sb.append(project);
+ sb.append(" ref ");
+ sb.append(ref);
sb.append(" to ");
sb.append(resolveNodeName(uri));
sb.append(", ");
@@ -84,7 +102,20 @@
}
@Override
- void onAllNodesReplicated(int totalPushTasksCount) {
+ void onRefReplicatedToAllNodes(String project, String ref, int nodesCount) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Replication of ");
+ sb.append(project);
+ sb.append(" ref ");
+ sb.append(ref);
+ sb.append(" completed to ");
+ sb.append(nodesCount);
+ sb.append(" nodes, ");
+ writeStdOut(sb.toString());
+ }
+
+ @Override
+ void onAllRefsReplicatedToAllNodes(int totalPushTasksCount) {
if (totalPushTasksCount == 0) {
return;
}
@@ -114,26 +145,76 @@
}
public static class GitUpdateProcessing extends PushResultProcessing {
- @Override
- void onOneNodeReplicated(String project, String ref, URIish uri,
- RefPushResult status) {
- //TODO: send stream events
+ static final Logger log = LoggerFactory.getLogger(GitUpdateProcessing.class);
+
+ private final ChangeHooks hooks;
+ private final SchemaFactory<ReviewDb> schema;
+
+ public GitUpdateProcessing(ChangeHooks hooks, SchemaFactory<ReviewDb> schema) {
+ this.hooks = hooks;
+ this.schema = schema;
}
@Override
- void onAllNodesReplicated(int totalPushTasksCount) {
- //TODO: send stream events
+ void onRefReplicatedToOneNode(String project, String ref, URIish uri,
+ RefPushResult status) {
+ RefReplicatedEvent event =
+ new RefReplicatedEvent(project, ref, resolveNodeName(uri), status);
+ postEvent(project, ref, event);
+ }
+
+ @Override
+ void onRefReplicatedToAllNodes(String project, String ref, int nodesCount) {
+ RefReplicationDoneEvent event =
+ new RefReplicationDoneEvent(project, ref, nodesCount);
+ postEvent(project, ref, event);
+ }
+
+ @Override
+ void onAllRefsReplicatedToAllNodes(int totalPushTasksCount) {
+ }
+
+ private void postEvent(String project, String ref, ChangeEvent event) {
+ if (PatchSet.isRef(ref)) {
+ try {
+ ReviewDb db = schema.open();
+ try {
+ hooks.postEvent(retrieveChange(ref, db), event, db);
+ } finally {
+ db.close();
+ }
+ } catch (Exception e) {
+ log.error("Cannot post event", e);
+ }
+ } else {
+ Branch.NameKey branch = new Branch.NameKey(Project.NameKey.parse(project), ref);
+ hooks.postEvent(branch, event);
+ }
+ }
+
+ private Change retrieveChange(String ref, ReviewDb db)
+ throws OrmException, NoSuchChangeException {
+ PatchSet.Id id = PatchSet.Id.fromRef(ref);
+ Change change = db.changes().get(id.getParentKey());
+ if (change == null) {
+ throw new NoSuchChangeException(id.getParentKey());
+ }
+ return change;
}
}
public static class NoopProcessing extends PushResultProcessing {
@Override
- void onOneNodeReplicated(String project, String ref, URIish uri,
+ void onRefReplicatedToOneNode(String project, String ref, URIish uri,
RefPushResult status) {
}
@Override
- void onAllNodesReplicated(int totalPushTasksCount) {
+ void onRefReplicatedToAllNodes(String project, String ref, int nodesCount) {
+ }
+
+ @Override
+ void onAllRefsReplicatedToAllNodes(int totalPushTasksCount) {
}
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java
new file mode 100644
index 0000000..98435ac
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.replication;
+
+import com.google.gerrit.server.events.ChangeEvent;
+
+import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
+
+public class RefReplicatedEvent extends ChangeEvent {
+ public final String type = "ref-replicated";
+ public final String project;
+ public final String ref;
+ public final String targetNode;
+ public final String status;
+
+ public RefReplicatedEvent(String project, String ref, String targetNode,
+ RefPushResult status) {
+ this.project = project;
+ this.ref = ref;
+ this.targetNode = targetNode;
+ this.status = toStatusString(status);
+ }
+
+ private String toStatusString(RefPushResult status) {
+ return status.name().toLowerCase().replace("_", "-");
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java
new file mode 100644
index 0000000..7eaed66
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.replication;
+
+import com.google.gerrit.server.events.ChangeEvent;
+
+public class RefReplicationDoneEvent extends ChangeEvent {
+ public final String type = "ref-replication-done";
+ public final String project;
+ public final String ref;
+ public final int nodesCount;
+
+ public RefReplicationDoneEvent(String project, String ref, int nodesCount) {
+ this.project = project;
+ this.ref = ref;
+ this.nodesCount = nodesCount;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
similarity index 63%
copy from src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java
copy to src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
index ff88b87..5c18f75 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2013 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.
@@ -11,16 +11,24 @@
// 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.googlesource.gerrit.plugins.replication;
-public enum ReplicationType {
- /** Replicate all after gerrit startup. */
- STARTUP,
+import com.google.gerrit.server.git.WorkQueue;
- /** Invoke ssh command to replicate. */
- COMMAND,
+import java.util.List;
- /** After a git reference is updated, run the replicaton. */
- GIT_UPDATED;
+public interface ReplicationConfig {
+
+ List<Destination> getDestinations();
+
+ boolean isReplicateAllOnPluginStart();
+
+ boolean isDefaultForceUpdate();
+
+ boolean isEmpty();
+
+ int shutdown();
+
+ void startup(WorkQueue workQueue);
+
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
new file mode 100644
index 0000000..dd8f4f5
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
@@ -0,0 +1,209 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.replication;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+@Singleton
+public class ReplicationFileBasedConfig implements ReplicationConfig {
+ static final Logger log = LoggerFactory.getLogger(ReplicationFileBasedConfig.class);
+ private List<Destination> destinations;
+ private File cfgPath;
+ private boolean replicateAllOnPluginStart;
+ private boolean defaultForceUpdate;
+ private Injector injector;
+ private final SchemaFactory<ReviewDb> database;
+ private final RemoteSiteUser.Factory replicationUserFactory;
+ private final PluginUser pluginUser;
+ private final GitRepositoryManager gitRepositoryManager;
+ private final GroupBackend groupBackend;
+ private final FileBasedConfig config;
+
+ @Inject
+ public ReplicationFileBasedConfig(final Injector injector, final SitePaths site,
+ final RemoteSiteUser.Factory ruf, final PluginUser pu,
+ final SchemaFactory<ReviewDb> db, final GitRepositoryManager grm,
+ final GroupBackend gb) throws ConfigInvalidException, IOException {
+ this.cfgPath = new File(site.etc_dir, "replication.config");
+ this.injector = injector;
+ this.replicationUserFactory = ruf;
+ this.pluginUser = pu;
+ this.database = db;
+ this.gitRepositoryManager = grm;
+ this.groupBackend = gb;
+ this.config = new FileBasedConfig(cfgPath, FS.DETECTED);
+ this.destinations = allDestinations();
+ }
+
+ /* (non-Javadoc)
+ * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#getDestinations()
+ */
+ @Override
+ public List<Destination> getDestinations() {
+ return destinations;
+ }
+
+ private List<Destination> allDestinations()
+ throws ConfigInvalidException, IOException {
+ if (!config.getFile().exists()) {
+ log.warn("Config file " + config.getFile() + "does not exist; not replicating");
+ return Collections.emptyList();
+ }
+ if (config.getFile().length() == 0) {
+ log.info("Config file " + config.getFile() + " is empty; not replicating");
+ return Collections.emptyList();
+ }
+
+ try {
+ config.load();
+ } catch (ConfigInvalidException e) {
+ throw new ConfigInvalidException(String.format(
+ "Config file %s is invalid: %s", config.getFile(), e.getMessage()), e);
+ } catch (IOException e) {
+ throw new IOException(String.format("Cannot read %s: %s", config.getFile(),
+ e.getMessage()), e);
+ }
+
+ replicateAllOnPluginStart =
+ config.getBoolean("gerrit", "replicateOnStartup", true);
+
+ defaultForceUpdate =
+ config.getBoolean("gerrit", "defaultForceUpdate", false);
+
+ ImmutableList.Builder<Destination> dest = ImmutableList.builder();
+ for (RemoteConfig c : allRemotes(config)) {
+ if (c.getURIs().isEmpty()) {
+ continue;
+ }
+
+ // If destination for push is not set assume equal to source.
+ for (RefSpec ref : c.getPushRefSpecs()) {
+ if (ref.getDestination() == null) {
+ ref.setDestination(ref.getSource());
+ }
+ }
+
+ if (c.getPushRefSpecs().isEmpty()) {
+ c.addPushRefSpec(new RefSpec().setSourceDestination("refs/*", "refs/*")
+ .setForceUpdate(defaultForceUpdate));
+ }
+
+ Destination destination =
+ new Destination(injector, c, config, database, replicationUserFactory,
+ pluginUser, gitRepositoryManager, groupBackend);
+
+ if (!destination.isSingleProjectMatch()) {
+ for (URIish u : c.getURIs()) {
+ if (u.getPath() == null || !u.getPath().contains("${name}")) {
+ throw new ConfigInvalidException(String.format(
+ "remote.%s.url \"%s\" lacks ${name} placeholder in %s",
+ c.getName(), u, config.getFile()));
+ }
+ }
+ }
+
+ dest.add(destination);
+ }
+ return dest.build();
+ }
+
+ /* (non-Javadoc)
+ * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#isReplicateAllOnPluginStart()
+ */
+ @Override
+ public boolean isReplicateAllOnPluginStart() {
+ return replicateAllOnPluginStart;
+ }
+
+ /* (non-Javadoc)
+ * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#isDefaultForceUpdate()
+ */
+ @Override
+ public boolean isDefaultForceUpdate() {
+ return defaultForceUpdate;
+ }
+
+ private static List<RemoteConfig> allRemotes(FileBasedConfig cfg)
+ throws ConfigInvalidException {
+ Set<String> names = cfg.getSubsections("remote");
+ List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size());
+ for (String name : names) {
+ try {
+ result.add(new RemoteConfig(cfg, name));
+ } catch (URISyntaxException e) {
+ throw new ConfigInvalidException(String.format(
+ "remote %s has invalid URL in %s", name, cfg.getFile()));
+ }
+ }
+ return result;
+ }
+
+ /* (non-Javadoc)
+ * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#isEmpty()
+ */
+ @Override
+ public boolean isEmpty() {
+ return destinations.isEmpty();
+ }
+
+ File getCfgPath() {
+ return cfgPath;
+ }
+
+ public int shutdown() {
+ int discarded = 0;
+ for (Destination cfg : destinations) {
+ discarded += cfg.shutdown();
+ }
+ return discarded;
+ }
+
+ FileBasedConfig getConfig() {
+ return config;
+ }
+
+ @Override
+ public void startup(WorkQueue workQueue) {
+ for (Destination cfg : destinations) {
+ cfg.start(workQueue);
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
index 77823f3..8aa3248 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
@@ -19,6 +19,7 @@
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.HeadUpdatedListener;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
import com.google.gerrit.extensions.events.ProjectDeletedListener;
@@ -35,23 +36,26 @@
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
.to(ReplicationQueue.class);
-
DynamicSet.bind(binder(), NewProjectCreatedListener.class)
.to(ReplicationQueue.class);
-
DynamicSet.bind(binder(), ProjectDeletedListener.class)
.to(ReplicationQueue.class);
+ DynamicSet.bind(binder(), HeadUpdatedListener.class)
+ .to(ReplicationQueue.class);
bind(OnStartStop.class).in(Scopes.SINGLETON);
bind(LifecycleListener.class)
.annotatedWith(UniqueAnnotations.create())
.to(OnStartStop.class);
- bind(SecureCredentialsFactory.class).in(Scopes.SINGLETON);
+ bind(CredentialsFactory.class).to(
+ AutoReloadSecureCredentialsFactoryDecorator.class).in(Scopes.SINGLETON);
bind(CapabilityDefinition.class)
.annotatedWith(Exports.named(START_REPLICATION))
.to(StartReplicationCapability.class);
install(new FactoryModuleBuilder().build(PushAll.Factory.class));
install(new FactoryModuleBuilder().build(RemoteSiteUser.Factory.class));
+
+ bind(ReplicationConfig.class).to(AutoReloadConfigDecorator.class);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
index cabb48b..223e597 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
@@ -15,23 +15,20 @@
package com.googlesource.gerrit.plugins.replication;
import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
+import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.HeadUpdatedListener;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
import com.google.gerrit.extensions.events.ProjectDeletedListener;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PluginUser;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
-import com.google.inject.Injector;
+
+import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.TransportException;
@@ -39,9 +36,6 @@
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.RemoteSession;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.URIish;
@@ -64,9 +58,10 @@
LifecycleListener,
GitReferenceUpdatedListener,
NewProjectCreatedListener,
- ProjectDeletedListener {
+ ProjectDeletedListener,
+ HeadUpdatedListener {
static final Logger log = LoggerFactory.getLogger(ReplicationQueue.class);
- private static final WrappedLogger wrappedLog = new WrappedLogger(log);
+ private static final ReplicationStateLogger stateLog = new ReplicationStateLogger(log);
static String replaceName(String in, String name, boolean keyIsOptional) {
String key = "${name}";
@@ -80,48 +75,32 @@
return null;
}
- private final Injector injector;
private final WorkQueue workQueue;
- private final List<Destination> configs;
private final SchemaFactory<ReviewDb> database;
- private final RemoteSiteUser.Factory replicationUserFactory;
- private final PluginUser pluginUser;
- private final GitRepositoryManager gitRepositoryManager;
- private final GroupBackend groupBackend;
+ private final ChangeHooks changeHooks;
+ private final ReplicationConfig config;
private volatile boolean running;
- boolean replicateAllOnPluginStart;
@Inject
- ReplicationQueue(final Injector i, final WorkQueue wq, final SitePaths site,
- final RemoteSiteUser.Factory ruf, final PluginUser pu,
- final SchemaFactory<ReviewDb> db,
- final GitRepositoryManager grm, final GroupBackend gb)
+ ReplicationQueue(final WorkQueue wq, final ReplicationConfig rc,
+ final SchemaFactory<ReviewDb> db, final ChangeHooks ch)
throws ConfigInvalidException, IOException {
- injector = i;
workQueue = wq;
database = db;
- replicationUserFactory = ruf;
- pluginUser = pu;
- gitRepositoryManager = grm;
- groupBackend = gb;
- configs = allDestinations(new File(site.etc_dir, "replication.config"));
+ changeHooks = ch;
+ config = rc;
}
@Override
public void start() {
- for (Destination cfg : configs) {
- cfg.start(workQueue);
- }
+ config.startup(workQueue);
running = true;
}
@Override
public void stop() {
running = false;
- int discarded = 0;
- for (Destination cfg : configs) {
- discarded += cfg.shutdown();
- }
+ int discarded = config.shutdown();
if (discarded > 0) {
log.warn(String.format(
"Cancelled %d replication events during shutdown",
@@ -132,11 +111,11 @@
void scheduleFullSync(final Project.NameKey project, final String urlMatch,
ReplicationState state) {
if (!running) {
- wrappedLog.warn("Replication plugin did not finish startup before event", state);
+ stateLog.warn("Replication plugin did not finish startup before event", state);
return;
}
- for (Destination cfg : configs) {
+ for (Destination cfg : config.getDestinations()) {
if (cfg.wouldPushProject(project)) {
for (URIish uri : cfg.getURIs(project, urlMatch)) {
cfg.schedule(project, PushOne.ALL_REFS, uri, state);
@@ -147,16 +126,14 @@
@Override
public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
- ReplicationState state =
- new ReplicationState(ReplicationType.GIT_UPDATED);
-
+ ReplicationState state = new ReplicationState(new GitUpdateProcessing(changeHooks, database));
if (!running) {
- wrappedLog.warn("Replication plugin did not finish startup before event", state);
+ stateLog.warn("Replication plugin did not finish startup before event", state);
return;
}
Project.NameKey project = new Project.NameKey(event.getProjectName());
- for (Destination cfg : configs) {
+ for (Destination cfg : config.getDestinations()) {
if (cfg.wouldPushProject(project) && cfg.wouldPushRef(event.getRefName())) {
for (URIish uri : cfg.getURIs(project, null)) {
cfg.schedule(project, event.getRefName(), uri, state);
@@ -166,85 +143,6 @@
state.markAllPushTasksScheduled();
}
- private List<Destination> allDestinations(File cfgPath)
- throws ConfigInvalidException, IOException {
- FileBasedConfig cfg = new FileBasedConfig(cfgPath, FS.DETECTED);
- if (!cfg.getFile().exists()) {
- log.warn("No " + cfg.getFile() + "; not replicating");
- return Collections.emptyList();
- }
- if (cfg.getFile().length() == 0) {
- log.info("Empty " + cfg.getFile() + "; not replicating");
- return Collections.emptyList();
- }
-
- try {
- cfg.load();
- } catch (ConfigInvalidException e) {
- throw new ConfigInvalidException(String.format(
- "Config file %s is invalid: %s",cfg.getFile(), e.getMessage()), e);
- } catch (IOException e) {
- throw new IOException(String.format(
- "Cannot read %s: %s", cfg.getFile(), e.getMessage()), e);
- }
-
- replicateAllOnPluginStart = cfg.getBoolean(
- "gerrit", "replicateOnStartup",
- true);
-
- ImmutableList.Builder<Destination> dest = ImmutableList.builder();
- for (RemoteConfig c : allRemotes(cfg)) {
- if (c.getURIs().isEmpty()) {
- continue;
- }
-
- // If destination for push is not set assume equal to source.
- for (RefSpec ref : c.getPushRefSpecs()) {
- if (ref.getDestination() == null) {
- ref.setDestination(ref.getSource());
- }
- }
-
- if (c.getPushRefSpecs().isEmpty()) {
- c.addPushRefSpec(new RefSpec()
- .setSourceDestination("refs/*", "refs/*")
- .setForceUpdate(true));
- }
-
- Destination destination = new Destination(injector, c, cfg, database,
- replicationUserFactory, pluginUser, gitRepositoryManager,
- groupBackend);
-
- if (!destination.isSingleProjectMatch()) {
- for (URIish u : c.getURIs()) {
- if (u.getPath() == null || !u.getPath().contains("${name}")) {
- throw new ConfigInvalidException(String.format(
- "remote.%s.url \"%s\" lacks ${name} placeholder in %s",
- c.getName(), u, cfg.getFile()));
- }
- }
- }
-
- dest.add(destination);
- }
- return dest.build();
- }
-
- private static List<RemoteConfig> allRemotes(FileBasedConfig cfg)
- throws ConfigInvalidException {
- Set<String> names = cfg.getSubsections("remote");
- List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size());
- for (String name : names) {
- try {
- result.add(new RemoteConfig(cfg, name));
- } catch (URISyntaxException e) {
- throw new ConfigInvalidException(String.format(
- "remote %s has invalid URL in %s", name, cfg.getFile()));
- }
- }
- return result;
- }
-
@Override
public void onNewProjectCreated(NewProjectCreatedListener.Event event) {
for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()), false)) {
@@ -259,9 +157,16 @@
}
}
+ @Override
+ public void onHeadUpdated(HeadUpdatedListener.Event event) {
+ for (URIish uri : getURIs(new Project.NameKey(event.getProjectName()), false)) {
+ updateHead(uri, event.getNewHeadName());
+ }
+ }
+
private Set<URIish> getURIs(Project.NameKey projectName,
boolean forProjectDeletion) {
- if (configs.isEmpty()) {
+ if (config.getDestinations().isEmpty()) {
return Collections.emptySet();
}
if (!running) {
@@ -270,7 +175,7 @@
}
Set<URIish> uris = Sets.newHashSet();
- for (Destination config : configs) {
+ for (Destination config : this.config.getDestinations()) {
if (!config.wouldPushProject(projectName)) {
continue;
}
@@ -365,7 +270,7 @@
}
OutputStream errStream = newErrorBufferStream();
try {
- executeRemotSsh(uri, cmd, errStream);
+ executeRemoteSsh(uri, cmd, errStream);
} catch (IOException e) {
log.error(String.format(
"Error creating remote repository at %s:\n"
@@ -423,7 +328,7 @@
String cmd = "rm -rf " + quotedPath;
OutputStream errStream = newErrorBufferStream();
try {
- executeRemotSsh(uri, cmd, errStream);
+ executeRemoteSsh(uri, cmd, errStream);
} catch (IOException e) {
log.error(String.format(
"Error deleting remote repository at %s:\n"
@@ -434,7 +339,52 @@
}
}
- private static void executeRemotSsh(URIish uri, String cmd,
+ private void updateHead(URIish replicateURI, String newHead) {
+ if (!replicateURI.isRemote()) {
+ updateHeadLocally(replicateURI, newHead);
+ } else if (isSSH(replicateURI)) {
+ updateHeadRemoteSsh(replicateURI, newHead);
+ } else {
+ log.warn(String.format("Cannot update HEAD of project on remote site %s."
+ + " Only local paths and SSH URLs are supported"
+ + " for remote HEAD update.", replicateURI));
+ }
+ }
+
+ private static void updateHeadRemoteSsh(URIish uri, String newHead) {
+ String quotedPath = QuotedString.BOURNE.quote(uri.getPath());
+ String cmd = "cd " + quotedPath
+ + " && git symbolic-ref HEAD " + QuotedString.BOURNE.quote(newHead);
+ OutputStream errStream = newErrorBufferStream();
+ try {
+ executeRemoteSsh(uri, cmd, errStream);
+ } catch (IOException e) {
+ log.error(String.format(
+ "Error updating HEAD of remote repository at %s to %s:\n"
+ + " Exception: %s\n"
+ + " Command: %s\n"
+ + " Output: %s",
+ uri, newHead, e, cmd, errStream), e);
+ }
+ }
+
+ private static void updateHeadLocally(URIish uri, String newHead) {
+ try {
+ Repository repo = new FileRepository(uri.getPath());
+ try {
+ if (newHead != null) {
+ RefUpdate u = repo.updateRef(Constants.HEAD);
+ u.link(newHead);
+ }
+ } finally {
+ repo.close();
+ }
+ } catch (IOException e) {
+ log.error(String.format("Failed to update HEAD of repository %s to %s", uri.getPath(), newHead), e);
+ }
+ }
+
+ private static void executeRemoteSsh(URIish uri, String cmd,
OutputStream errStream) throws IOException {
RemoteSession ssh = connect(uri);
Process proc = ssh.exec(cmd, 0);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java
index 35f086f..9632d5f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java
@@ -14,16 +14,17 @@
package com.googlesource.gerrit.plugins.replication;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import com.googlesource.gerrit.plugins.replication.PushResultProcessing.NoopProcessing;
+
import org.eclipse.jgit.transport.URIish;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
-import com.googlesource.gerrit.plugins.replication.PushResultProcessing.CommandProcessing;
-import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing;
-import com.googlesource.gerrit.plugins.replication.PushResultProcessing.NoopProcessing;
-
public class ReplicationState {
private boolean allScheduled;
private final PushResultProcessing pushResultProcessing;
@@ -31,31 +32,38 @@
private final Lock countingLock = new ReentrantLock();
private final CountDownLatch allPushTasksFinished = new CountDownLatch(1);
+ private static class RefReplicationStatus {
+ private final String project;
+ private final String ref;
+ private int nodesToReplicateCount;
+ private int replicatedNodesCount;
+
+ public RefReplicationStatus(String project, String ref) {
+ this.project = project;
+ this.ref = ref;
+ }
+
+ public boolean allDone() {
+ return replicatedNodesCount == nodesToReplicateCount;
+ }
+ }
+ private final Table<String, String, RefReplicationStatus> statusByProjectRef;
private int totalPushTasksCount;
private int finishedPushTasksCount;
- public ReplicationState(ReplicationType type) {
- this(type, null);
+ public ReplicationState() {
+ this(new NoopProcessing());
}
- public ReplicationState(ReplicationType type, StartCommand sshCommand) {
- switch(type) {
- case COMMAND:
- pushResultProcessing = new CommandProcessing(sshCommand);
- break;
- case GIT_UPDATED:
- pushResultProcessing = new GitUpdateProcessing();
- break;
- case STARTUP:
- default:
- pushResultProcessing = new NoopProcessing();
- break;
- }
+ public ReplicationState(PushResultProcessing processing) {
+ pushResultProcessing = processing;
+ statusByProjectRef = HashBasedTable.create();
}
- public void increasePushTaskCount() {
+ public void increasePushTaskCount(String project, String ref) {
countingLock.lock();
try {
+ getRefStatus(project, ref).nodesToReplicateCount++;
totalPushTasksCount++;
} finally {
countingLock.unlock();
@@ -68,22 +76,33 @@
public void notifyRefReplicated(String project, String ref, URIish uri,
RefPushResult status) {
- pushResultProcessing.onOneNodeReplicated(project, ref, uri, status);
+ pushResultProcessing.onRefReplicatedToOneNode(project, ref, uri, status);
+ RefReplicationStatus completedRefStatus = null;
+ boolean allPushTaksCompleted = false;
countingLock.lock();
try {
+ RefReplicationStatus refStatus = getRefStatus(project, ref);
+ refStatus.replicatedNodesCount++;
finishedPushTasksCount++;
- if (!allScheduled) {
- return;
- }
- if (finishedPushTasksCount < totalPushTasksCount) {
- return;
+
+ if (allScheduled) {
+ if (refStatus.allDone()) {
+ completedRefStatus = statusByProjectRef.remove(project, ref);
+ }
+ allPushTaksCompleted = finishedPushTasksCount == totalPushTasksCount;
}
} finally {
countingLock.unlock();
}
- doAllPushTasksCompleted();
+ if (completedRefStatus != null) {
+ doRefPushTasksCompleted(completedRefStatus);
+ }
+
+ if (allPushTaksCompleted) {
+ doAllPushTasksCompleted();
+ }
}
public void markAllPushTasksScheduled() {
@@ -101,10 +120,35 @@
}
private void doAllPushTasksCompleted() {
- pushResultProcessing.onAllNodesReplicated(totalPushTasksCount);
+ fireRemainingOnRefReplicatedToAllNodes();
+ pushResultProcessing.onAllRefsReplicatedToAllNodes(totalPushTasksCount);
allPushTasksFinished.countDown();
}
+ /**
+ * Some could be remaining if replication of a ref is completed before all
+ * tasks are scheduled.
+ */
+ private void fireRemainingOnRefReplicatedToAllNodes() {
+ for (RefReplicationStatus refStatus : statusByProjectRef.values()) {
+ doRefPushTasksCompleted(refStatus);
+ }
+ }
+
+ private void doRefPushTasksCompleted(RefReplicationStatus refStatus) {
+ pushResultProcessing.onRefReplicatedToAllNodes(refStatus.project,
+ refStatus.ref, refStatus.nodesToReplicateCount);
+ }
+
+ private RefReplicationStatus getRefStatus(String project, String ref) {
+ if (!statusByProjectRef.contains(project, ref)) {
+ RefReplicationStatus refStatus = new RefReplicationStatus(project, ref);
+ statusByProjectRef.put(project, ref, refStatus);
+ return refStatus;
+ }
+ return statusByProjectRef.get(project, ref);
+ }
+
public void waitForReplication() throws InterruptedException {
allPushTasksFinished.await();
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/WrappedLogger.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStateLogger.java
similarity index 78%
rename from src/main/java/com/googlesource/gerrit/plugins/replication/WrappedLogger.java
rename to src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStateLogger.java
index 7bef7e7..cb1d4ce 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/WrappedLogger.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStateLogger.java
@@ -16,11 +16,19 @@
import org.slf4j.Logger;
-public class WrappedLogger {
+/**
+ * Wrapper around a Logger that also logs out the replication state.
+ * <p>
+ * When logging replication errors it is useful to know the current
+ * replication state. This utility class wraps the methods from Logger
+ * and logs additional information about the replication state to the
+ * stderr console.
+ */
+public class ReplicationStateLogger {
private final Logger logger;
- public WrappedLogger(Logger logger) {
+ public ReplicationStateLogger(Logger logger) {
this.logger = logger;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
index 7bb9831..68433f1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
@@ -25,7 +25,7 @@
import java.io.IOException;
/** Looks up a remote's password in secure.config. */
-class SecureCredentialsFactory {
+class SecureCredentialsFactory implements CredentialsFactory {
private final Config config;
@Inject
@@ -51,7 +51,7 @@
return cfg;
}
- SecureCredentialsProvider create(String remoteName) {
+ public SecureCredentialsProvider create(String remoteName) {
String user = config.getString("remote", remoteName, "username");
String pass = config.getString("remote", remoteName, "password");
return new SecureCredentialsProvider(user, pass);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
index b80c462..44513c0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/StartCommand.java
@@ -21,6 +21,8 @@
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.replication.PushResultProcessing.CommandProcessing;
+
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
@@ -36,7 +38,7 @@
@CommandMetaData(name = "start", description = "Start replication for specific project or all projects")
final class StartCommand extends SshCommand {
private static final Logger log = LoggerFactory.getLogger(StartCommand.class);
- private static final WrappedLogger wrappedLog = new WrappedLogger(log);
+ private static final ReplicationStateLogger stateLog = new ReplicationStateLogger(log);
@Option(name = "--all", usage = "push all known projects")
private boolean all;
@@ -65,8 +67,7 @@
throw new UnloggedFailure(1, "error: cannot combine --all and PROJECT");
}
- ReplicationState state =
- new ReplicationState(ReplicationType.COMMAND, this);
+ ReplicationState state = new ReplicationState(new CommandProcessing(this));
Future<?> future = null;
if (all) {
future = pushAllFactory.create(urlMatch, state).schedule(0, TimeUnit.SECONDS);
@@ -87,10 +88,10 @@
try {
future.get();
} catch (InterruptedException e) {
- wrappedLog.error("Thread was interrupted while waiting for PushAll operation to finish", e, state);
+ stateLog.error("Thread was interrupted while waiting for PushAll operation to finish", e, state);
return;
} catch (ExecutionException e) {
- wrappedLog.error("An exception was thrown in PushAll operation", e, state);
+ stateLog.error("An exception was thrown in PushAll operation", e, state);
return;
}
}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 0737a94..94b8473 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -67,6 +67,43 @@
: If true, replicates to all remotes on startup to ensure they
are in-sync with this server. By default, true.
+gerrit.autoReload
+: If true, automatically reloads replication destinations and settings
+ after `replication.config` file is updated, without the need to restart
+ the replication plugin. When the reload takes place, pending replication
+ events based on old settings are discarded. By default, false.
+
+gerrit.defaultForceUpdate
+: If true, the default push refspec will be set to use forced
+ update to the remote when no refspec is given. By default, false.
+
+replication.lockErrorMaxRetries
+: Number of times to retry a replication operation if a lock
+ error is detected.
+
+ If two or more replication operations (to the same GIT and Ref)
+ are scheduled at approximately the same time (and end up on different
+ replication threads), there is a large probability that the last
+ push to complete will fail with a remote "failure to lock" error.
+ This option allows Gerrit to retry the replication push when the
+ "failure to lock" error is detected.
+
+ A good value would be 3 retries or less, depending on how often
+ you see lockError collisions in your server logs. A too highly set
+ value risks keeping around the replication operations in the queue
+ for a long time, and the number of items in the queue will increase
+ with time.
+
+ Normally Gerrit will succeed with the replication during its first
+ retry, but in certain edge cases (e.g. a mirror introduces a ref
+ namespace with the same name as a branch on the master) the retry
+ will never succeed.
+
+ The issue can also be mitigated somewhat by increasing the
+ replicationDelay.
+
+ Default: 0 (disabled, i.e. never retry)
+
remote.NAME.url
: Address of the remote server to push to. Multiple URLs may be
specified within a single remote block, listing different
@@ -122,13 +159,22 @@
the active branches, but not the change refs under
`refs/changes/`, or the tags under `refs/tags/`.
+ Note that prefixing a source refspec with `+` causes the replication
+ to be done with a `git push --force` command.
+ Be aware that when you are pushing to remote repositories that may
+ have read/write access (e.g. GitHub) you may want to omit the `+`
+ to prevent the risk of overwriting branches that have been modified
+ on the remote.
+
Multiple push keys can be supplied, to specify multiple
patterns to match against. In the [example above][2], remote
"pubmirror" uses two push keys to match both `refs/heads/*`
and `refs/tags/*`, but excludes all others, including
`refs/changes/*`.
- Defaults to `+refs/*:refs/*` (all refs) if not specified.
+ Defaults to `refs/*:refs/*` (push all refs) if not specified,
+ or `+refs/*:refs/*` (force push all refs) if not specified and
+ `gerrit.defaultForceUpdate` is true.
Note that the `refs/meta/config` branch is only replicated
when `replicatePermissions` is true, even if the push refspec
@@ -216,7 +262,7 @@
By default, false, do *not* replicate project deletions.
remote.NAME.mirror
-: If true, replication will remove remote branches that absent
+: If true, replication will remove remote branches that are absent
locally or invisible to the replication (for example read
access denied via `authGroup` option).
@@ -228,18 +274,20 @@
placeholder.
Github and Gitorious do not permit slashes "/" in repository
- names and changes this to dashes "-" at repository creation
- time. If set to "dash", this changes slashes to dashes in the
- repository name. If set to "underscore", this changes slashes
- to underscores in the repository name.
+ names and will change them to dashes "-" at repository creation
+ time.
+
+ If this setting is set to "dash", slashes will be replaced with
+ dashes in the remote repository name. If set to "underscore",
+ slashes will be replaced with underscores in the repository name.
Option "basenameOnly" makes `${name}` to be only the basename
(the part after the last slash) of the repository path on the
Gerrit server, e.g. `${name}` of `foo/bar/my-repo.git` would
be `my-repo`.
- By default, "slash" remote name will contain slashes as they
- do in Gerrit.
+ By default, "slash", i.e. remote names will contain slashes as
+ they do in Gerrit.
<a name="remote.NAME.projects">remote.NAME.projects</a>
: Specifies which repositories should be replicated to the
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java
new file mode 100644
index 0000000..93ea562
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.replication;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
+import static org.easymock.EasyMock.verify;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing;
+import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jgit.transport.URIish;
+
+import java.net.URISyntaxException;
+
+@SuppressWarnings("unchecked")
+public class GitUpdateProcessingTest extends TestCase {
+ static {
+ KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+ }
+
+ private ChangeHooks changeHooksMock;
+ private ChangeAccess changeAccessMock;
+ private GitUpdateProcessing gitUpdateProcessing;
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ changeHooksMock = createMock(ChangeHooks.class);
+ replay(changeHooksMock);
+ changeAccessMock = createNiceMock(ChangeAccess.class);
+ replay(changeAccessMock);
+ ReviewDb reviewDbMock = createNiceMock(ReviewDb.class);
+ expect(reviewDbMock.changes()).andReturn(changeAccessMock).anyTimes();
+ replay(reviewDbMock);
+ SchemaFactory<ReviewDb> schemaMock = createMock(SchemaFactory.class);
+ expect(schemaMock.open()).andReturn(reviewDbMock).anyTimes();
+ replay(schemaMock);
+ gitUpdateProcessing = new GitUpdateProcessing(changeHooksMock, schemaMock);
+ }
+
+ public void testHeadRefReplicated() throws URISyntaxException {
+ reset(changeHooksMock);
+ RefReplicatedEvent expectedEvent =
+ new RefReplicatedEvent("someProject", "refs/heads/master", "someHost",
+ RefPushResult.SUCCEEDED);
+ changeHooksMock.postEvent(anyObject(Branch.NameKey.class),
+ RefReplicatedEventEquals.eqEvent(expectedEvent));
+ expectLastCall().once();
+ replay(changeHooksMock);
+
+ gitUpdateProcessing.onRefReplicatedToOneNode("someProject", "refs/heads/master",
+ new URIish("git://someHost/someProject.git"), RefPushResult.SUCCEEDED);
+ verify(changeHooksMock);
+ }
+
+ public void testChangeRefReplicated() throws URISyntaxException, OrmException {
+ Change expectedChange = new Change(null, null, null, null, null);
+ reset(changeAccessMock);
+ expect(changeAccessMock.get(anyObject(Change.Id.class))).andReturn(expectedChange);
+ replay(changeAccessMock);
+
+ reset(changeHooksMock);
+ RefReplicatedEvent expectedEvent =
+ new RefReplicatedEvent("someProject", "refs/changes/1/1/1", "someHost",
+ RefPushResult.FAILED);
+ changeHooksMock.postEvent(eq(expectedChange),
+ RefReplicatedEventEquals.eqEvent(expectedEvent),
+ anyObject(ReviewDb.class));
+ expectLastCall().once();
+ replay(changeHooksMock);
+
+ gitUpdateProcessing.onRefReplicatedToOneNode("someProject",
+ "refs/changes/1/1/1", new URIish("git://someHost/someProject.git"),
+ RefPushResult.FAILED);
+ verify(changeHooksMock);
+ }
+
+ public void testOnAllNodesReplicated() throws URISyntaxException {
+ reset(changeHooksMock);
+ RefReplicationDoneEvent expectedDoneEvent =
+ new RefReplicationDoneEvent("someProject", "refs/heads/master", 5);
+ changeHooksMock.postEvent(anyObject(Branch.NameKey.class),
+ RefReplicationDoneEventEquals.eqEvent(expectedDoneEvent));
+ expectLastCall().once();
+ replay(changeHooksMock);
+
+ gitUpdateProcessing.onRefReplicatedToAllNodes("someProject", "refs/heads/master", 5);
+ verify(changeHooksMock);
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java
new file mode 100644
index 0000000..c68ba73
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.replication;
+
+import org.easymock.EasyMock;
+import org.easymock.IArgumentMatcher;
+
+public class RefReplicatedEventEquals implements IArgumentMatcher {
+
+ private RefReplicatedEvent expected;
+
+ public RefReplicatedEventEquals(RefReplicatedEvent expected) {
+ this.expected = expected;
+ }
+
+ public static final RefReplicatedEvent eqEvent(RefReplicatedEvent refReplicatedEvent) {
+ EasyMock.reportMatcher(new RefReplicatedEventEquals(refReplicatedEvent));
+ return null;
+ }
+
+ @Override
+ public boolean matches(Object actual) {
+ if (!(actual instanceof RefReplicatedEvent)) {
+ return false;
+ }
+ RefReplicatedEvent actualRefReplicatedEvent = (RefReplicatedEvent)actual;
+ if (!equals(expected.project, actualRefReplicatedEvent.project)) {
+ return false;
+ }
+ if (!equals(expected.ref, actualRefReplicatedEvent.ref)) {
+ return false;
+ }
+ if (!equals(expected.targetNode, actualRefReplicatedEvent.targetNode)) {
+ return false;
+ }
+ if (!equals(expected.status, actualRefReplicatedEvent.status)) {
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean equals(Object object1, Object object2) {
+ if (object1 == object2) {
+ return true;
+ }
+ if (object1 != null && !object1.equals(object2)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void appendTo(StringBuffer buffer) {
+ buffer.append("eqEvent(");
+ buffer.append(expected.getClass().getName());
+ buffer.append(" with project \"");
+ buffer.append(expected.project);
+ buffer.append(" and ref \"");
+ buffer.append(expected.ref);
+ buffer.append(" and targetNode \"");
+ buffer.append(expected.targetNode);
+ buffer.append(" and status \"");
+ buffer.append(expected.status);
+ buffer.append("\")");
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEventEquals.java b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEventEquals.java
new file mode 100644
index 0000000..42a25de
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEventEquals.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.replication;
+
+import org.easymock.EasyMock;
+import org.easymock.IArgumentMatcher;
+
+public class RefReplicationDoneEventEquals implements IArgumentMatcher {
+
+ private RefReplicationDoneEvent expected;
+
+ public RefReplicationDoneEventEquals(RefReplicationDoneEvent expected) {
+ this.expected = expected;
+ }
+
+ public static final RefReplicationDoneEvent eqEvent(RefReplicationDoneEvent refReplicatedEvent) {
+ EasyMock.reportMatcher(new RefReplicationDoneEventEquals(refReplicatedEvent));
+ return null;
+ }
+
+ @Override
+ public boolean matches(Object actual) {
+ if (!(actual instanceof RefReplicationDoneEvent)) {
+ return false;
+ }
+ RefReplicationDoneEvent actualRefReplicatedDoneEvent = (RefReplicationDoneEvent)actual;
+ if (!equals(expected.project, actualRefReplicatedDoneEvent.project)) {
+ return false;
+ }
+ if (!equals(expected.ref, actualRefReplicatedDoneEvent.ref)) {
+ return false;
+ }
+ if (expected.nodesCount != actualRefReplicatedDoneEvent.nodesCount) {
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean equals(Object object1, Object object2) {
+ if (object1 == object2) {
+ return true;
+ }
+ if (object1 != null && !object1.equals(object2)){
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void appendTo(StringBuffer buffer) {
+ buffer.append("eqEvent(");
+ buffer.append(expected.getClass().getName());
+ buffer.append(" with project \"");
+ buffer.append(expected.project);
+ buffer.append(" and ref \"");
+ buffer.append(expected.ref);
+ buffer.append(" and nodesCount \"");
+ buffer.append(expected.nodesCount);
+ buffer.append("\")");
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java
new file mode 100644
index 0000000..a0bf576
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java
@@ -0,0 +1,219 @@
+// Copyright (C) 2013 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.googlesource.gerrit.plugins.replication;
+
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.resetToDefault;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult;
+
+import org.eclipse.jgit.transport.URIish;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.URISyntaxException;
+
+public class ReplicationStateTest {
+
+ private ReplicationState replicationState;
+ private PushResultProcessing pushResultProcessingMock;
+
+ @Before
+ public void setUp() throws Exception {
+ pushResultProcessingMock = createNiceMock(PushResultProcessing.class);
+ replay(pushResultProcessingMock);
+ replicationState = new ReplicationState(pushResultProcessingMock);
+ }
+
+ @Test
+ public void shouldNotHavePushTask() {
+ assertFalse(replicationState.hasPushTask());
+ }
+
+ @Test
+ public void shouldHavePushTask() {
+ replicationState.increasePushTaskCount("someProject", "someRef");
+ assertTrue(replicationState.hasPushTask());
+ }
+
+ @Test
+ public void shouldFireOneReplicationEventWhenNothingToReplicate() {
+ resetToDefault(pushResultProcessingMock);
+
+ //expected event
+ pushResultProcessingMock.onAllRefsReplicatedToAllNodes(0);
+ replay(pushResultProcessingMock);
+
+ //actual test
+ replicationState.markAllPushTasksScheduled();
+ verify(pushResultProcessingMock);
+ }
+
+ @Test
+ public void shouldFireEventsForReplicationOfOneRefToOneNode()
+ throws URISyntaxException {
+ resetToDefault(pushResultProcessingMock);
+ URIish uri = new URIish("git://someHost/someRepo.git");
+
+ //expected events
+ pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "someRef",
+ uri, RefPushResult.SUCCEEDED);
+ pushResultProcessingMock.onRefReplicatedToAllNodes("someProject",
+ "someRef", 1);
+ pushResultProcessingMock.onAllRefsReplicatedToAllNodes(1);
+ replay(pushResultProcessingMock);
+
+ //actual test
+ replicationState.increasePushTaskCount("someProject", "someRef");
+ replicationState.markAllPushTasksScheduled();
+ replicationState.notifyRefReplicated("someProject", "someRef", uri,
+ RefPushResult.SUCCEEDED);
+ verify(pushResultProcessingMock);
+ }
+
+ @Test
+ public void shouldFireEventsForReplicationOfOneRefToMultipleNodes()
+ throws URISyntaxException {
+ resetToDefault(pushResultProcessingMock);
+ URIish uri1 = new URIish("git://someHost1/someRepo.git");
+ URIish uri2 = new URIish("git://someHost2/someRepo.git");
+
+ //expected events
+ pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "someRef",
+ uri1, RefPushResult.SUCCEEDED);
+ pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "someRef",
+ uri2, RefPushResult.FAILED);
+ pushResultProcessingMock.onRefReplicatedToAllNodes("someProject",
+ "someRef", 2);
+ pushResultProcessingMock.onAllRefsReplicatedToAllNodes(2);
+ replay(pushResultProcessingMock);
+
+ //actual test
+ replicationState.increasePushTaskCount("someProject", "someRef");
+ replicationState.increasePushTaskCount("someProject", "someRef");
+ replicationState.markAllPushTasksScheduled();
+ replicationState.notifyRefReplicated("someProject", "someRef", uri1,
+ RefPushResult.SUCCEEDED);
+ replicationState.notifyRefReplicated("someProject", "someRef", uri2,
+ RefPushResult.FAILED);
+ verify(pushResultProcessingMock);
+ }
+
+ @Test
+ public void shouldFireEventsForReplicationOfMultipleRefsToMultipleNodes()
+ throws URISyntaxException {
+ resetToDefault(pushResultProcessingMock);
+ URIish uri1 = new URIish("git://host1/someRepo.git");
+ URIish uri2 = new URIish("git://host2/someRepo.git");
+ URIish uri3 = new URIish("git://host3/someRepo.git");
+
+ //expected events
+ pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
+ uri1, RefPushResult.SUCCEEDED);
+ pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
+ uri2, RefPushResult.SUCCEEDED);
+ pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
+ uri3, RefPushResult.SUCCEEDED);
+ pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref2",
+ uri1, RefPushResult.SUCCEEDED);
+ pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref2",
+ uri2, RefPushResult.SUCCEEDED);
+ pushResultProcessingMock
+ .onRefReplicatedToAllNodes("someProject", "ref1", 3);
+ pushResultProcessingMock
+ .onRefReplicatedToAllNodes("someProject", "ref2", 2);
+ pushResultProcessingMock.onAllRefsReplicatedToAllNodes(5);
+ replay(pushResultProcessingMock);
+
+ //actual test
+ replicationState.increasePushTaskCount("someProject", "ref1");
+ replicationState.increasePushTaskCount("someProject", "ref1");
+ replicationState.increasePushTaskCount("someProject", "ref1");
+ replicationState.increasePushTaskCount("someProject", "ref2");
+ replicationState.increasePushTaskCount("someProject", "ref2");
+ replicationState.markAllPushTasksScheduled();
+ replicationState.notifyRefReplicated("someProject", "ref1", uri1,
+ RefPushResult.SUCCEEDED);
+ replicationState.notifyRefReplicated("someProject", "ref1", uri2,
+ RefPushResult.SUCCEEDED);
+ replicationState.notifyRefReplicated("someProject", "ref1", uri3,
+ RefPushResult.SUCCEEDED);
+ replicationState.notifyRefReplicated("someProject", "ref2", uri1,
+ RefPushResult.SUCCEEDED);
+ replicationState.notifyRefReplicated("someProject", "ref2", uri2,
+ RefPushResult.SUCCEEDED);
+ verify(pushResultProcessingMock);
+ }
+
+ @Test
+ public void shouldFireEventsForReplicationSameRefDifferentProjects()
+ throws URISyntaxException {
+ resetToDefault(pushResultProcessingMock);
+ URIish uri = new URIish("git://host1/someRepo.git");
+
+ //expected events
+ pushResultProcessingMock.onRefReplicatedToOneNode("project1", "ref1", uri,
+ RefPushResult.SUCCEEDED);
+ pushResultProcessingMock.onRefReplicatedToOneNode("project2", "ref2", uri,
+ RefPushResult.SUCCEEDED);
+ pushResultProcessingMock.onRefReplicatedToAllNodes("project1", "ref1", 1);
+ pushResultProcessingMock.onRefReplicatedToAllNodes("project2", "ref2", 1);
+ pushResultProcessingMock.onAllRefsReplicatedToAllNodes(2);
+ replay(pushResultProcessingMock);
+
+ //actual test
+ replicationState.increasePushTaskCount("project1", "ref1");
+ replicationState.increasePushTaskCount("project2", "ref2");
+ replicationState.markAllPushTasksScheduled();
+ replicationState.notifyRefReplicated("project1", "ref1", uri,
+ RefPushResult.SUCCEEDED);
+ replicationState.notifyRefReplicated("project2", "ref2", uri,
+ RefPushResult.SUCCEEDED);
+ verify(pushResultProcessingMock);
+ }
+
+ @Test
+ public void shouldFireEventsWhenSomeReplicationCompleteBeforeAllTasksAreScheduled()
+ throws URISyntaxException {
+ resetToDefault(pushResultProcessingMock);
+ URIish uri1 = new URIish("git://host1/someRepo.git");
+
+ //expected events
+ pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref1",
+ uri1, RefPushResult.SUCCEEDED);
+ pushResultProcessingMock.onRefReplicatedToOneNode("someProject", "ref2",
+ uri1, RefPushResult.SUCCEEDED);
+ pushResultProcessingMock
+ .onRefReplicatedToAllNodes("someProject", "ref1", 1);
+ pushResultProcessingMock
+ .onRefReplicatedToAllNodes("someProject", "ref2", 1);
+ pushResultProcessingMock.onAllRefsReplicatedToAllNodes(2);
+ replay(pushResultProcessingMock);
+
+ //actual test
+ replicationState.increasePushTaskCount("someProject", "ref1");
+ replicationState.increasePushTaskCount("someProject", "ref2");
+ replicationState.notifyRefReplicated("someProject", "ref1", uri1,
+ RefPushResult.SUCCEEDED);
+ replicationState.notifyRefReplicated("someProject", "ref2", uri1,
+ RefPushResult.SUCCEEDED);
+ replicationState.markAllPushTasksScheduled();
+ verify(pushResultProcessingMock);
+ }
+}