Merge branch 'stable-2.8' * stable-2.8: Update Gerrit API version to 2.8 in pom.xml Conflicts: pom.xml Change-Id: I0af06ff7633e8512f19f81ac013c2642c14d96e9
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 d8a9358..0018323 100644 --- a/BUCK +++ b/BUCK
@@ -17,6 +17,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 index 73f1f39..b09964f 100644 --- a/pom.xml +++ b/pom.xml
@@ -23,7 +23,7 @@ <artifactId>replication</artifactId> <name>replication</name> <packaging>jar</packaging> - <version>2.8</version> + <version>2.9-SNAPSHOT</version> <properties> <Gerrit-ApiType>plugin</Gerrit-ApiType> @@ -62,8 +62,8 @@ <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> - <source>1.6</source> - <target>1.6</target> + <source>1.7</source> + <target>1.7</target> <encoding>UTF-8</encoding> </configuration> </plugin> @@ -81,7 +81,14 @@ <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> - <version>4.8.1</version> + <version>4.11</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.easymock</groupId> + <artifactId>easymock</artifactId> + <version>3.2</version> <scope>test</scope> </dependency>
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..9edf501 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -256,7 +256,7 @@ pending.put(uri, e); } e.addRef(ref); - state.increasePushTaskCount(); + state.increasePushTaskCount(project.get(), ref); e.addState(ref, state); } }
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/PushOne.java b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java index 897d120..e246cd1 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -363,6 +363,8 @@ return new PushResult(); } + log.info("Push to " + uri + " references: " + todo); + return tn.push(NullProgressMonitor.INSTANCE, todo); }
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/ReplicationConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java new file mode 100644 index 0000000..1d79750 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
@@ -0,0 +1,160 @@ +// 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.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 ReplicationConfig { + static final Logger log = LoggerFactory.getLogger(ReplicationConfig.class); + private List<Destination> destinations; + private File cfgPath; + private boolean replicateAllOnPluginStart; + 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; + + @Inject + public ReplicationConfig(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.destinations = allDestinations(cfgPath); + } + + public List<Destination> getDestinations() { + return destinations; + } + + 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(); + } + + public boolean isReplicateAllOnPluginStart() { + return replicateAllOnPluginStart; + } + + 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; + } + + public boolean isEmpty() { + return destinations.isEmpty(); + } +}
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..4b3b730 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; @@ -31,16 +32,17 @@ class ReplicationModule extends AbstractModule { @Override protected void configure() { + bind(ReplicationConfig.class); bind(ReplicationQueue.class).in(Scopes.SINGLETON); 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)
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 5b6c1e2..78725a3 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,7 +58,8 @@ LifecycleListener, GitReferenceUpdatedListener, NewProjectCreatedListener, - ProjectDeletedListener { + ProjectDeletedListener, + HeadUpdatedListener { static final Logger log = LoggerFactory.getLogger(ReplicationQueue.class); private static final WrappedLogger wrappedLog = new WrappedLogger(log); @@ -80,36 +75,25 @@ 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) { + for (Destination cfg : config.getDestinations()) { cfg.start(workQueue); } running = true; @@ -119,7 +103,7 @@ public void stop() { running = false; int discarded = 0; - for (Destination cfg : configs) { + for (Destination cfg : config.getDestinations()) { discarded += cfg.shutdown(); } if (discarded > 0) { @@ -136,7 +120,7 @@ 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 +131,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); 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 +148,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 +162,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 +180,7 @@ } Set<URIish> uris = Sets.newHashSet(); - for (Destination config : configs) { + for (Destination config : this.config.getDestinations()) { if (!config.wouldPushProject(projectName)) { continue; } @@ -426,6 +336,51 @@ } } + 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 { + executeRemotSsh(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 executeRemotSsh(URIish uri, String cmd, OutputStream errStream) throws IOException { RemoteSession ssh = connect(uri);
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/ReplicationType.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java deleted file mode 100644 index ff88b87..0000000 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationType.java +++ /dev/null
@@ -1,26 +0,0 @@ -// 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. - -package com.googlesource.gerrit.plugins.replication; - -public enum ReplicationType { - /** Replicate all after gerrit startup. */ - STARTUP, - - /** Invoke ssh command to replicate. */ - COMMAND, - - /** After a git reference is updated, run the replicaton. */ - GIT_UPDATED; -}
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..9db0ac5 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; @@ -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);
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); + } +}