Merge branch 'stable-3.0' * stable-3.0: Clean up ReplicationQueue tests Convert ReferenceUpdateEvent class to AutoValue Fix issue with dropping events on start Change-Id: I5a4bc84a829600865fdd551152219cd1bbe5aa91
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java index 945f869..4c07f40 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
@@ -14,127 +14,45 @@ package com.googlesource.gerrit.plugins.replication; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Multimap; -import com.google.common.flogger.FluentLogger; -import com.google.gerrit.common.FileUtil; -import com.google.gerrit.extensions.annotations.PluginData; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; import com.google.gerrit.extensions.annotations.PluginName; -import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; -import java.io.IOException; import java.nio.file.Path; -import java.util.List; -import java.util.Optional; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.transport.URIish; @Singleton -public class AutoReloadConfigDecorator implements ReplicationConfig { - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); +public class AutoReloadConfigDecorator implements ReplicationConfig, LifecycleListener { private static final long RELOAD_DELAY = 120; private static final long RELOAD_INTERVAL = 60; private volatile ReplicationFileBasedConfig currentConfig; - private long currentConfigTs; - private long lastFailedConfigTs; - private final SitePaths site; - private final Destination.Factory destinationFactory; - private final Path pluginDataDir; - // Use Provider<> instead of injecting the ReplicationQueue because of circular dependency with - // ReplicationConfig - private final Provider<ReplicationQueue> replicationQueue; private final ScheduledExecutorService autoReloadExecutor; private ScheduledFuture<?> autoReloadRunnable; - - private volatile boolean shuttingDown; + private final AutoReloadRunnable reloadRunner; @Inject public AutoReloadConfigDecorator( - SitePaths site, - Destination.Factory destinationFactory, - Provider<ReplicationQueue> replicationQueue, - @PluginData Path pluginDataDir, @PluginName String pluginName, - WorkQueue workQueue) - throws ConfigInvalidException, IOException { - this.site = site; - this.destinationFactory = destinationFactory; - this.pluginDataDir = pluginDataDir; - this.currentConfig = loadConfig(); - this.currentConfigTs = getLastModified(currentConfig); - this.replicationQueue = replicationQueue; + WorkQueue workQueue, + ReplicationFileBasedConfig replicationConfig, + AutoReloadRunnable reloadRunner, + EventBus eventBus) { + this.currentConfig = replicationConfig; this.autoReloadExecutor = workQueue.createQueue(1, pluginName + "_auto-reload-config"); - } - - private static long getLastModified(ReplicationFileBasedConfig cfg) { - return FileUtil.lastModified(cfg.getCfgPath()); - } - - private ReplicationFileBasedConfig loadConfig() throws ConfigInvalidException, IOException { - return new ReplicationFileBasedConfig(site, destinationFactory, pluginDataDir); - } - - private synchronized boolean isAutoReload() { - return currentConfig.getConfig().getBoolean("gerrit", "autoReload", false); - } - - @Override - public synchronized List<Destination> getDestinations(FilterType filterType) { - return currentConfig.getDestinations(filterType); - } - - @Override - public synchronized Multimap<Destination, URIish> getURIs( - Optional<String> remoteName, Project.NameKey projectName, FilterType filterType) { - return currentConfig.getURIs(remoteName, projectName, filterType); - } - - private synchronized void reloadIfNeeded() { - reload(false); + this.reloadRunner = reloadRunner; + eventBus.register(this); } @VisibleForTesting - public void forceReload() { - reload(true); - } - - private void reload(boolean force) { - if (force || isAutoReload()) { - ReplicationQueue queue = replicationQueue.get(); - - long lastModified = getLastModified(currentConfig); - try { - if (force - || (!shuttingDown - && lastModified > currentConfigTs - && lastModified > lastFailedConfigTs - && queue.isRunning() - && !queue.isReplaying())) { - queue.stop(); - currentConfig = loadConfig(); - currentConfigTs = lastModified; - lastFailedConfigTs = 0; - logger.atInfo().log( - "Configuration reloaded: %d destinations", - currentConfig.getDestinations(FilterType.ALL).size()); - } - } catch (Exception e) { - logger.atSevere().withCause(e).log( - "Cannot reload replication configuration: keeping existing settings"); - lastFailedConfigTs = lastModified; - return; - } finally { - queue.start(); - } - } + public void reload() { + reloadRunner.reload(); } @Override @@ -153,45 +71,36 @@ } @Override - public synchronized boolean isEmpty() { - return currentConfig.isEmpty(); - } - - @Override public Path getEventsDirectory() { return currentConfig.getEventsDirectory(); } - /* shutdown() cannot be set as a synchronized method because - * it may need to wait for pending events to complete; - * e.g. when enabling the drain of replication events before - * shutdown. - * - * As a rule of thumb for synchronized methods, because they - * implicitly define a critical section and associated lock, - * they should never hold waiting for another resource, otherwise - * the risk of deadlock is very high. - * - * See more background about deadlocks, what they are and how to - * prevent them at: https://en.wikipedia.org/wiki/Deadlock - */ @Override - public int shutdown() { - this.shuttingDown = true; - if (autoReloadRunnable != null) { - autoReloadRunnable.cancel(false); - autoReloadRunnable = null; - } - return currentConfig.shutdown(); + public synchronized void start() { + autoReloadRunnable = + autoReloadExecutor.scheduleAtFixedRate( + reloadRunner, RELOAD_DELAY, RELOAD_INTERVAL, TimeUnit.SECONDS); } @Override - public synchronized void startup(WorkQueue workQueue) { - shuttingDown = false; - currentConfig.startup(workQueue); - autoReloadRunnable = - autoReloadExecutor.scheduleAtFixedRate( - this::reloadIfNeeded, RELOAD_DELAY, RELOAD_INTERVAL, TimeUnit.SECONDS); + public synchronized void stop() { + if (autoReloadRunnable != null) { + if (!autoReloadRunnable.cancel(true)) { + throw new IllegalStateException( + "Unable to cancel replication reload task: cannot guarantee orderly shutdown"); + } + autoReloadRunnable = null; + } + } + + @Override + public String getVersion() { + return currentConfig.getVersion(); + } + + @Subscribe + public void onReload(ReplicationFileBasedConfig newConfig) { + currentConfig = newConfig; } @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadRunnable.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadRunnable.java new file mode 100644 index 0000000..c5009d3 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadRunnable.java
@@ -0,0 +1,80 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication; + +import com.google.common.eventbus.EventBus; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.extensions.annotations.PluginData; +import com.google.gerrit.server.config.SitePaths; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.nio.file.Path; + +public class AutoReloadRunnable implements Runnable { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final SitePaths site; + private final Path pluginDataDir; + private final EventBus eventBus; + private final Provider<ReplicationQueue> replicationQueue; + + private ReplicationFileBasedConfig loadedConfig; + private String loadedConfigVersion; + private String lastFailedConfigVersion; + + @Inject + public AutoReloadRunnable( + ReplicationFileBasedConfig config, + SitePaths site, + @PluginData Path pluginDataDir, + EventBus eventBus, + Provider<ReplicationQueue> replicationQueue) { + this.loadedConfig = config; + this.loadedConfigVersion = config.getVersion(); + this.lastFailedConfigVersion = ""; + this.site = site; + this.pluginDataDir = pluginDataDir; + this.eventBus = eventBus; + this.replicationQueue = replicationQueue; + } + + @Override + public synchronized void run() { + String pendingConfigVersion = loadedConfig.getVersion(); + ReplicationQueue queue = replicationQueue.get(); + if (pendingConfigVersion.equals(loadedConfigVersion) + || pendingConfigVersion.equals(lastFailedConfigVersion) + || !queue.isRunning() + || queue.isReplaying()) { + return; + } + + reload(); + } + + synchronized void reload() { + String pendingConfigVersion = loadedConfig.getVersion(); + try { + loadedConfig = new ReplicationFileBasedConfig(site, pluginDataDir); + loadedConfigVersion = loadedConfig.getVersion(); + lastFailedConfigVersion = ""; + eventBus.post(loadedConfig); + } catch (Exception e) { + logger.atSevere().withCause(e).log( + "Cannot reload replication configuration: keeping existing settings"); + lastFailedConfigVersion = pendingConfigVersion; + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java b/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java index a8dede3..9f07d76 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java
@@ -31,7 +31,7 @@ } private final RemoteConfig config; - private final ReplicationConfig replicationConfig; + private final DestinationsCollection destinations; private final DynamicItem<AdminApiFactory> adminApiFactory; private final Project.NameKey project; private final String head; @@ -39,21 +39,20 @@ @Inject CreateProjectTask( RemoteConfig config, - ReplicationConfig replicationConfig, + DestinationsCollection destinations, DynamicItem<AdminApiFactory> adminApiFactory, @Assisted Project.NameKey project, @Assisted String head) { this.config = config; - this.replicationConfig = replicationConfig; + this.destinations = destinations; this.adminApiFactory = adminApiFactory; this.project = project; this.head = head; } public boolean create() { - return replicationConfig - .getURIs(Optional.of(config.getName()), project, FilterType.PROJECT_CREATION).values() - .stream() + return destinations.getURIs(Optional.of(config.getName()), project, FilterType.PROJECT_CREATION) + .values().stream() .map(u -> createProject(u, project, head)) .reduce(true, (a, b) -> a && b); }
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 679776f..996cb2a 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -30,7 +30,7 @@ import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.AccountGroup; -import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.BranchNameKey; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.CurrentUser; @@ -731,6 +731,10 @@ return config.getDelay() * 1000; } + int getSlowLatencyThreshold() { + return config.getSlowLatencyThreshold(); + } + private static boolean matches(URIish uri, String urlMatch) { if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) { return true; @@ -750,7 +754,7 @@ ReplicationScheduledEvent event = new ReplicationScheduledEvent(project.get(), ref, targetNode); try { - eventDispatcher.get().postEvent(new Branch.NameKey(project, ref), event); + eventDispatcher.get().postEvent(BranchNameKey.create(project, ref), event); } catch (PermissionBackendException e) { repLog.error("error posting event", e); } @@ -764,7 +768,7 @@ RefReplicatedEvent event = new RefReplicatedEvent(project.get(), ref, targetNode, RefPushResult.FAILED, status); try { - eventDispatcher.get().postEvent(new Branch.NameKey(project, ref), event); + eventDispatcher.get().postEvent(BranchNameKey.create(project, ref), event); } catch (PermissionBackendException e) { repLog.error("error posting event", e); }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java index f688cfc..815b1b7 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java
@@ -16,6 +16,8 @@ import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; +import com.google.gerrit.server.config.ConfigUtil; +import java.util.concurrent.TimeUnit; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.transport.RemoteConfig; @@ -23,6 +25,7 @@ static final int DEFAULT_REPLICATION_DELAY = 15; static final int DEFAULT_RESCHEDULE_DELAY = 3; static final int DEFAULT_DRAIN_QUEUE_ATTEMPTS = 0; + private static final int DEFAULT_SLOW_LATENCY_THRESHOLD_SECS = 900; private final int delay; private final int rescheduleDelay; @@ -41,6 +44,7 @@ private final ImmutableList<String> authGroupNames; private final RemoteConfig remoteConfig; private final int maxRetries; + private final int slowLatencyThreshold; protected DestinationConfiguration(RemoteConfig remoteConfig, Config cfg) { this.remoteConfig = remoteConfig; @@ -67,6 +71,16 @@ maxRetries = getInt( remoteConfig, cfg, "replicationMaxRetries", cfg.getInt("replication", "maxRetries", 0)); + + slowLatencyThreshold = + (int) + ConfigUtil.getTimeUnit( + cfg, + "remote", + remoteConfig.getName(), + "slowLatencyThreshold", + DEFAULT_SLOW_LATENCY_THRESHOLD_SECS, + TimeUnit.SECONDS); } public int getDelay() { @@ -140,4 +154,8 @@ private static int getInt(RemoteConfig rc, Config cfg, String name, int defValue) { return cfg.getInt("remote", rc.getName(), name, defValue); } + + public int getSlowLatencyThreshold() { + return slowLatencyThreshold; + } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationsCollection.java b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationsCollection.java new file mode 100644 index 0000000..a1f0095 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationsCollection.java
@@ -0,0 +1,349 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication; + +import static com.googlesource.gerrit.plugins.replication.AdminApiFactory.isGerrit; +import static com.googlesource.gerrit.plugins.replication.AdminApiFactory.isGerritHttp; +import static com.googlesource.gerrit.plugins.replication.AdminApiFactory.isSSH; +import static com.googlesource.gerrit.plugins.replication.ReplicationQueue.repLog; +import static java.util.stream.Collectors.toList; + +import com.google.common.base.Strings; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.collect.SetMultimap; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.git.WorkQueue; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.googlesource.gerrit.plugins.replication.Destination.Factory; +import com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +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; + +@Singleton +public class DestinationsCollection implements ReplicationDestinations { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final Factory destinationFactory; + private final Provider<ReplicationQueue> replicationQueue; + private volatile ReplicationFileBasedConfig replicationConfig; + private volatile List<Destination> destinations; + private boolean shuttingDown; + + public static class EventQueueNotEmptyException extends Exception { + private static final long serialVersionUID = 1L; + + public EventQueueNotEmptyException(String errorMessage) { + super(errorMessage); + } + } + + @Inject + public DestinationsCollection( + Destination.Factory destinationFactory, + Provider<ReplicationQueue> replicationQueue, + ReplicationFileBasedConfig replicationConfig, + EventBus eventBus) + throws ConfigInvalidException { + this.destinationFactory = destinationFactory; + this.replicationQueue = replicationQueue; + this.replicationConfig = replicationConfig; + this.destinations = allDestinations(destinationFactory); + eventBus.register(this); + } + + @Override + public Multimap<Destination, URIish> getURIs( + Optional<String> remoteName, Project.NameKey projectName, FilterType filterType) { + if (getAll(filterType).isEmpty()) { + return ImmutableMultimap.of(); + } + + SetMultimap<Destination, URIish> uris = HashMultimap.create(); + for (Destination config : getAll(filterType)) { + if (!config.wouldPushProject(projectName)) { + continue; + } + + if (remoteName.isPresent() && !config.getRemoteConfigName().equals(remoteName.get())) { + continue; + } + + boolean adminURLUsed = false; + + for (String url : config.getAdminUrls()) { + if (Strings.isNullOrEmpty(url)) { + continue; + } + + URIish uri; + try { + uri = new URIish(url); + } catch (URISyntaxException e) { + repLog.warn("adminURL '{}' is invalid: {}", url, e.getMessage()); + continue; + } + + if (!isGerrit(uri) && !isGerritHttp(uri)) { + String path = + replaceName(uri.getPath(), projectName.get(), config.isSingleProjectMatch()); + if (path == null) { + repLog.warn("adminURL {} does not contain ${name}", uri); + continue; + } + + uri = uri.setPath(path); + if (!isSSH(uri)) { + repLog.warn("adminURL '{}' is invalid: only SSH and HTTP are supported", uri); + continue; + } + } + uris.put(config, uri); + adminURLUsed = true; + } + + if (!adminURLUsed) { + for (URIish uri : config.getURIs(projectName, "*")) { + uris.put(config, uri); + } + } + } + return uris; + } + + @Override + public synchronized List<Destination> getAll(FilterType filterType) { + Predicate<? super Destination> filter; + switch (filterType) { + case PROJECT_CREATION: + filter = dest -> dest.isCreateMissingRepos(); + break; + case PROJECT_DELETION: + filter = dest -> dest.isReplicateProjectDeletions(); + break; + case ALL: + default: + filter = dest -> true; + break; + } + return destinations.stream().filter(Objects::nonNull).filter(filter).collect(toList()); + } + + @Override + public synchronized boolean isEmpty() { + return destinations.isEmpty(); + } + + @Override + public synchronized void startup(WorkQueue workQueue) { + shuttingDown = false; + for (Destination cfg : destinations) { + cfg.start(workQueue); + } + } + + /* shutdown() cannot be set as a synchronized method because + * it may need to wait for pending events to complete; + * e.g. when enabling the drain of replication events before + * shutdown. + * + * As a rule of thumb for synchronized methods, because they + * implicitly define a critical section and associated lock, + * they should never hold waiting for another resource, otherwise + * the risk of deadlock is very high. + * + * See more background about deadlocks, what they are and how to + * prevent them at: https://en.wikipedia.org/wiki/Deadlock + */ + @Override + public int shutdown() { + synchronized (this) { + shuttingDown = true; + } + + int discarded = 0; + for (Destination cfg : destinations) { + try { + drainReplicationEvents(cfg); + } catch (EventQueueNotEmptyException e) { + logger.atWarning().log("Event queue not empty: %s", e.getMessage()); + } finally { + discarded += cfg.shutdown(); + } + } + return discarded; + } + + void drainReplicationEvents(Destination destination) throws EventQueueNotEmptyException { + int drainQueueAttempts = destination.getDrainQueueAttempts(); + if (drainQueueAttempts == 0) { + return; + } + int pending = destination.getQueueInfo().pending.size(); + int inFlight = destination.getQueueInfo().inFlight.size(); + while ((inFlight > 0 || pending > 0) && drainQueueAttempts > 0) { + try { + logger.atInfo().log( + "Draining replication events, postpone shutdown. Events left: inFlight %d, pending %d", + inFlight, pending); + Thread.sleep(destination.getReplicationDelaySeconds()); + } catch (InterruptedException ie) { + logger.atWarning().withCause(ie).log( + "Wait for replication events to drain has been interrupted"); + } + pending = destination.getQueueInfo().pending.size(); + inFlight = destination.getQueueInfo().inFlight.size(); + drainQueueAttempts--; + } + if (pending > 0 || inFlight > 0) { + throw new EventQueueNotEmptyException( + String.format("Pending: %d - InFlight: %d", pending, inFlight)); + } + } + + @Subscribe + public synchronized void onReload(ReplicationFileBasedConfig newConfig) + throws ConfigInvalidException { + if (shuttingDown) { + logger.atWarning().log("Shutting down: configuration reload ignored"); + return; + } + + try { + replicationQueue.get().stop(); + replicationConfig = newConfig; + destinations = allDestinations(destinationFactory); + logger.atInfo().log("Configuration reloaded: %d destinations", getAll(FilterType.ALL).size()); + } finally { + replicationQueue.get().start(); + } + } + + private List<Destination> allDestinations(Destination.Factory destinationFactory) + throws ConfigInvalidException { + if (!replicationConfig.getConfig().getFile().exists()) { + logger.atWarning().log( + "Config file %s does not exist; not replicating", + replicationConfig.getConfig().getFile()); + return Collections.emptyList(); + } + if (replicationConfig.getConfig().getFile().length() == 0) { + logger.atInfo().log( + "Config file %s is empty; not replicating", replicationConfig.getConfig().getFile()); + return Collections.emptyList(); + } + + try { + replicationConfig.getConfig().load(); + } catch (ConfigInvalidException e) { + throw new ConfigInvalidException( + String.format( + "Config file %s is invalid: %s", + replicationConfig.getConfig().getFile(), e.getMessage()), + e); + } catch (IOException e) { + throw new ConfigInvalidException( + String.format( + "Cannot read %s: %s", replicationConfig.getConfig().getFile(), e.getMessage()), + e); + } + + boolean defaultForceUpdate = + replicationConfig.getConfig().getBoolean("gerrit", "defaultForceUpdate", false); + + ImmutableList.Builder<Destination> dest = ImmutableList.builder(); + for (RemoteConfig c : allRemotes(replicationConfig.getConfig())) { + 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 = + destinationFactory.create(new DestinationConfiguration(c, replicationConfig.getConfig())); + + 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, replicationConfig.getConfig().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; + } + + static String replaceName(String in, String name, boolean keyIsOptional) { + String key = "${name}"; + int n = in.indexOf(key); + if (0 <= n) { + return in.substring(0, n) + name + in.substring(n + key.length()); + } + if (keyIsOptional) { + return in; + } + return null; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ListCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ListCommand.java index fa17dce..d337883 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ListCommand.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ListCommand.java
@@ -40,11 +40,11 @@ @Option(name = "--json", usage = "output in json format") private boolean json; - @Inject private ReplicationConfig config; + @Inject private ReplicationDestinations destinations; @Override protected void run() { - for (Destination d : config.getDestinations(FilterType.ALL)) { + for (Destination d : destinations.getAll(FilterType.ALL)) { if (matches(d.getRemoteConfigName())) { printRemote(d); }
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 56cecfe..af34555 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -16,6 +16,7 @@ import static com.googlesource.gerrit.plugins.replication.ReplicationQueue.repLog; import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.stream.Collectors.toMap; import com.google.common.base.Throwables; @@ -329,14 +330,19 @@ } repLog.info("Replication to {} started...", uri); - Timer1.Context context = metrics.start(config.getName()); + Timer1.Context<String> destinationContext = metrics.start(config.getName()); try { - long startedAt = context.getStartTime(); + long startedAt = destinationContext.getStartTime(); long delay = NANOSECONDS.toMillis(startedAt - createdAt); metrics.record(config.getName(), delay, retryCount); git = gitManager.openRepository(projectName); runImpl(); - long elapsed = NANOSECONDS.toMillis(context.stop()); + long elapsed = NANOSECONDS.toMillis(destinationContext.stop()); + + if (elapsed > SECONDS.toMillis(pool.getSlowLatencyThreshold())) { + metrics.recordSlowProjectReplication( + config.getName(), projectName.get(), pool.getSlowLatencyThreshold(), elapsed); + } repLog.info( "Replication to {} completed in {}ms, {}ms delay, {} retries", uri,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java index fccdb7b..511a8fc 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEvent.java
@@ -17,6 +17,7 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.events.RefEvent; import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult; +import java.util.Objects; import org.eclipse.jgit.transport.RemoteRefUpdate; import org.eclipse.jgit.transport.RemoteRefUpdate.Status; @@ -45,11 +46,40 @@ @Override public Project.NameKey getProjectNameKey() { - return new Project.NameKey(project); + return Project.nameKey(project); } @Override public String getRefName() { return ref; } + + @Override + public boolean equals(Object other) { + if (!(other instanceof RefReplicatedEvent)) { + return false; + } + RefReplicatedEvent event = (RefReplicatedEvent) other; + if (!Objects.equals(event.project, this.project)) { + return false; + } + if (!Objects.equals(event.ref, this.ref)) { + return false; + } + if (!Objects.equals(event.targetNode, this.targetNode)) { + return false; + } + if (!Objects.equals(event.status, this.status)) { + return false; + } + if (!Objects.equals(event.refStatus, this.refStatus)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return super.hashCode(); + } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java index 4789a96..7acd3d1 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEvent.java
@@ -16,6 +16,7 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.events.RefEvent; +import java.util.Objects; public class RefReplicationDoneEvent extends RefEvent { public static final String TYPE = "ref-replication-done"; @@ -33,11 +34,35 @@ @Override public Project.NameKey getProjectNameKey() { - return new Project.NameKey(project); + return Project.nameKey(project); } @Override public String getRefName() { return ref; } + + @Override + public boolean equals(Object other) { + if (!(other instanceof RefReplicationDoneEvent)) { + return false; + } + + RefReplicationDoneEvent event = (RefReplicationDoneEvent) other; + if (!Objects.equals(event.project, this.project)) { + return false; + } + if (!Objects.equals(event.ref, this.ref)) { + return false; + } + if (event.nodesCount != this.nodesCount) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return super.hashCode(); + } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java index 929c538..b981bc8 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
@@ -11,44 +11,63 @@ // 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.Multimap; -import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.server.git.WorkQueue; import java.nio.file.Path; -import java.util.List; -import java.util.Optional; -import org.eclipse.jgit.transport.URIish; +/** Configuration of all the replication end points. */ public interface ReplicationConfig { + /** Filter for accessing replication projects. */ enum FilterType { PROJECT_CREATION, PROJECT_DELETION, ALL } - List<Destination> getDestinations(FilterType filterType); - - Multimap<Destination, URIish> getURIs( - Optional<String> remoteName, Project.NameKey projectName, FilterType filterType); - + /** + * Returns current replication configuration of whether to replicate or not all the projects when + * the plugin starts. + * + * @return true if replication at plugin start, false otherwise. + */ boolean isReplicateAllOnPluginStart(); + /** + * Returns the default behaviour of the replication plugin when pushing to remote replication + * ends. Even though the property name has the 'update' suffix, it actually refers to Git push + * operation and not to a Git update. + * + * @return true if forced push is the default, false otherwise. + */ boolean isDefaultForceUpdate(); + /** + * Returns the maximum number of ref-specs to log into the replication_log whenever a push + * operation is completed against a replication end. + * + * @return maximum number of refs to log, zero if unlimited. + */ int getMaxRefsToLog(); - boolean isEmpty(); - + /** + * Configured location where the replication events are stored on the filesystem for being resumed + * and kept across restarts. + * + * @return path to store persisted events. + */ Path getEventsDirectory(); - int shutdown(); - - void startup(WorkQueue workQueue); - int getSshConnectionTimeout(); int getSshCommandTimeout(); + + /** + * Current logical version string of the current configuration loaded in memory, depending on the + * actual implementation of the configuration on the persistent storage. + * + * @return current logical version number. + */ + String getVersion(); }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationDestinations.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationDestinations.java new file mode 100644 index 0000000..1a6b77f --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationDestinations.java
@@ -0,0 +1,63 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication; + +import com.google.common.collect.Multimap; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.git.WorkQueue; +import com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType; +import java.util.List; +import java.util.Optional; +import org.eclipse.jgit.transport.URIish; + +/** Git destinations currently active for replication. */ +public interface ReplicationDestinations { + + /** + * Return all the URIs associated to a project and a filter criteria. + * + * @param remoteName name of the replication end or empty if selecting all ends. + * @param projectName name of the project + * @param filterType type of filter criteria for selecting projects + * @return the multi-map of destinations and the associated replication URIs + */ + Multimap<Destination, URIish> getURIs( + Optional<String> remoteName, Project.NameKey projectName, FilterType filterType); + + /** + * List of currently active replication destinations. + * + * @param filterType type project filtering + * @return the list of active destinations + */ + List<Destination> getAll(FilterType filterType); + + /** @return true if there are no destinations, false otherwise. */ + boolean isEmpty(); + + /** + * Start replicating to all destinations. + * + * @param workQueue execution queue for scheduling the replication events. + */ + void startup(WorkQueue workQueue); + + /** + * Stop the replication to all destinations. + * + * @return number of events cancelled during shutdown. + */ + int shutdown(); +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java index 6476412..83f941c 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
@@ -13,51 +13,19 @@ // limitations under the License. package com.googlesource.gerrit.plugins.replication; -import static com.googlesource.gerrit.plugins.replication.AdminApiFactory.isGerrit; -import static com.googlesource.gerrit.plugins.replication.AdminApiFactory.isGerritHttp; -import static com.googlesource.gerrit.plugins.replication.AdminApiFactory.isSSH; -import static com.googlesource.gerrit.plugins.replication.ReplicationQueue.repLog; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; -import static java.util.stream.Collectors.toList; - import com.google.common.base.Strings; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; -import com.google.common.collect.SetMultimap; -import com.google.common.flogger.FluentLogger; import com.google.gerrit.extensions.annotations.PluginData; -import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.SitePaths; -import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Inject; import com.google.inject.Singleton; -import java.io.IOException; -import java.net.URISyntaxException; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; -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; @Singleton public class ReplicationFileBasedConfig implements ReplicationConfig { - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final int DEFAULT_SSH_CONNECTION_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes - private List<Destination> destinations; private final SitePaths site; private Path cfgPath; private boolean replicateAllOnPluginStart; @@ -69,176 +37,16 @@ private final Path pluginDataDir; @Inject - public ReplicationFileBasedConfig( - SitePaths site, Destination.Factory destinationFactory, @PluginData Path pluginDataDir) - throws ConfigInvalidException, IOException { + public ReplicationFileBasedConfig(SitePaths site, @PluginData Path pluginDataDir) { this.site = site; this.cfgPath = site.etc_dir.resolve("replication.config"); this.config = new FileBasedConfig(cfgPath.toFile(), FS.DETECTED); - this.destinations = allDestinations(destinationFactory); + this.replicateAllOnPluginStart = config.getBoolean("gerrit", "replicateOnStartup", false); + this.defaultForceUpdate = config.getBoolean("gerrit", "defaultForceUpdate", false); + this.maxRefsToLog = config.getInt("gerrit", "maxRefsToLog", 0); this.pluginDataDir = pluginDataDir; } - /* - * (non-Javadoc) - * @see - * com.googlesource.gerrit.plugins.replication.ReplicationConfig#getDestinations - * (com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType) - */ - @Override - public List<Destination> getDestinations(FilterType filterType) { - Predicate<? super Destination> filter; - switch (filterType) { - case PROJECT_CREATION: - filter = dest -> dest.isCreateMissingRepos(); - break; - case PROJECT_DELETION: - filter = dest -> dest.isReplicateProjectDeletions(); - break; - case ALL: - default: - filter = dest -> true; - break; - } - return destinations.stream().filter(Objects::nonNull).filter(filter).collect(toList()); - } - - private List<Destination> allDestinations(Destination.Factory destinationFactory) - throws ConfigInvalidException, IOException { - if (!config.getFile().exists()) { - logger.atWarning().log("Config file %s does not exist; not replicating", config.getFile()); - return Collections.emptyList(); - } - if (config.getFile().length() == 0) { - logger.atInfo().log("Config file %s is empty; not replicating", config.getFile()); - 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", false); - - defaultForceUpdate = config.getBoolean("gerrit", "defaultForceUpdate", false); - - maxRefsToLog = config.getInt("gerrit", "maxRefsToLog", 0); - - sshCommandTimeout = - (int) ConfigUtil.getTimeUnit(config, "gerrit", null, "sshCommandTimeout", 0, SECONDS); - sshConnectionTimeout = - (int) - ConfigUtil.getTimeUnit( - config, - "gerrit", - null, - "sshConnectionTimeout", - DEFAULT_SSH_CONNECTION_TIMEOUT_MS, - MILLISECONDS); - - 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 = destinationFactory.create(new DestinationConfiguration(c, config)); - - 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(); - } - - @Override - public Multimap<Destination, URIish> getURIs( - Optional<String> remoteName, Project.NameKey projectName, FilterType filterType) { - if (getDestinations(filterType).isEmpty()) { - return ImmutableMultimap.of(); - } - - SetMultimap<Destination, URIish> uris = HashMultimap.create(); - for (Destination config : getDestinations(filterType)) { - if (!config.wouldPushProject(projectName)) { - continue; - } - - if (remoteName.isPresent() && !config.getRemoteConfigName().equals(remoteName.get())) { - continue; - } - - boolean adminURLUsed = false; - - for (String url : config.getAdminUrls()) { - if (Strings.isNullOrEmpty(url)) { - continue; - } - - URIish uri; - try { - uri = new URIish(url); - } catch (URISyntaxException e) { - repLog.warn("adminURL '{}' is invalid: {}", url, e.getMessage()); - continue; - } - - if (!isGerrit(uri) && !isGerritHttp(uri)) { - String path = - replaceName(uri.getPath(), projectName.get(), config.isSingleProjectMatch()); - if (path == null) { - repLog.warn("adminURL {} does not contain ${name}", uri); - continue; - } - - uri = uri.setPath(path); - if (!isSSH(uri)) { - repLog.warn("adminURL '{}' is invalid: only SSH and HTTP are supported", uri); - continue; - } - } - uris.put(config, uri); - adminURLUsed = true; - } - - if (!adminURLUsed) { - for (URIish uri : config.getURIs(projectName, "*")) { - uris.put(config, uri); - } - } - } - return uris; - } - static String replaceName(String in, String name, boolean keyIsOptional) { String key = "${name}"; int n = in.indexOf(key); @@ -272,28 +80,6 @@ return maxRefsToLog; } - 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(); - } - @Override public Path getEventsDirectory() { String eventsDirectory = config.getString("replication", null, "eventsDirectory"); @@ -307,67 +93,13 @@ return cfgPath; } - @Override - public int shutdown() { - int discarded = 0; - for (Destination cfg : destinations) { - try { - drainReplicationEvents(cfg); - } catch (EventQueueNotEmptyException e) { - logger.atWarning().log("Event queue not empty: %s", e.getMessage()); - } finally { - discarded += cfg.shutdown(); - } - } - return discarded; - } - - void drainReplicationEvents(Destination destination) throws EventQueueNotEmptyException { - int drainQueueAttempts = destination.getDrainQueueAttempts(); - if (drainQueueAttempts == 0) { - return; - } - int pending = destination.getQueueInfo().pending.size(); - int inFlight = destination.getQueueInfo().inFlight.size(); - - while ((inFlight > 0 || pending > 0) && drainQueueAttempts > 0) { - try { - logger.atInfo().log( - "Draining replication events, postpone shutdown. Events left: inFlight %d, pending %d", - inFlight, pending); - Thread.sleep(destination.getReplicationDelaySeconds()); - } catch (InterruptedException ie) { - logger.atWarning().withCause(ie).log( - "Wait for replication events to drain has been interrupted"); - } - pending = destination.getQueueInfo().pending.size(); - inFlight = destination.getQueueInfo().inFlight.size(); - drainQueueAttempts--; - } - - if (pending > 0 || inFlight > 0) { - throw new EventQueueNotEmptyException( - String.format("Pending: %d - InFlight: %d", pending, inFlight)); - } - } - - public static class EventQueueNotEmptyException extends Exception { - private static final long serialVersionUID = 1L; - - public EventQueueNotEmptyException(String errorMessage) { - super(errorMessage); - } - } - FileBasedConfig getConfig() { return config; } @Override - public void startup(WorkQueue workQueue) { - for (Destination cfg : destinations) { - cfg.start(workQueue); - } + public String getVersion() { + return Long.toString(config.getFile().lastModified()); } @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationMetrics.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationMetrics.java index afc7926..1bc17ec 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationMetrics.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationMetrics.java
@@ -14,11 +14,14 @@ package com.googlesource.gerrit.plugins.replication; +import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.metrics.Description; import com.google.gerrit.metrics.Field; import com.google.gerrit.metrics.Histogram1; +import com.google.gerrit.metrics.Histogram3; import com.google.gerrit.metrics.MetricMaker; import com.google.gerrit.metrics.Timer1; +import com.google.gerrit.server.logging.PluginMetadata; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -27,10 +30,37 @@ private final Timer1<String> executionTime; private final Histogram1<String> executionDelay; private final Histogram1<String> executionRetries; + private final Histogram3<Integer, String, String> slowProjectReplicationLatency; @Inject - ReplicationMetrics(MetricMaker metricMaker) { - Field<String> DEST_FIELD = Field.ofString("destination"); + ReplicationMetrics(@PluginName String pluginName, MetricMaker metricMaker) { + Field<String> DEST_FIELD = + Field.ofString( + "destination", + (metadataBuilder, fieldValue) -> + metadataBuilder + .pluginName(pluginName) + .addPluginMetadata(PluginMetadata.create("destination", fieldValue))) + .build(); + + Field<String> PROJECT_FIELD = + Field.ofString( + "project", + (metadataBuilder, fieldValue) -> + metadataBuilder + .pluginName(pluginName) + .addPluginMetadata(PluginMetadata.create("project", fieldValue))) + .build(); + + Field<Integer> SLOW_THRESHOLD_FIELD = + Field.ofInteger( + "slow_threshold", + (metadataBuilder, fieldValue) -> + metadataBuilder + .pluginName(pluginName) + .addPluginMetadata( + PluginMetadata.create("slow_threshold", fieldValue.toString()))) + .build(); executionTime = metricMaker.newTimer( @@ -55,6 +85,17 @@ .setCumulative() .setUnit("retries"), DEST_FIELD); + + slowProjectReplicationLatency = + metricMaker.newHistogram( + "latency_slower_than_threshold" + "", + new Description( + "latency for project to destination, where latency was slower than threshold") + .setCumulative() + .setUnit(Description.Units.MILLISECONDS), + SLOW_THRESHOLD_FIELD, + PROJECT_FIELD, + DEST_FIELD); } /** @@ -63,7 +104,7 @@ * @param name the destination name. * @return the timer context. */ - Timer1.Context start(String name) { + Timer1.Context<String> start(String name) { return executionTime.start(name); } @@ -78,4 +119,17 @@ executionDelay.record(name, delay); executionRetries.record(name, retries); } + + /** + * Record replication latency for project to destination, where latency was slower than threshold + * + * @param destinationName the destination name. + * @param projectName the project name. + * @param slowThreshold replication initialDelay in milliseconds. + * @param latency number of retries. + */ + void recordSlowProjectReplication( + String destinationName, String projectName, Integer slowThreshold, long latency) { + slowProjectReplicationLatency.record(slowThreshold, destinationName, projectName, latency); + } }
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 835d068..be6e7f6 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
@@ -16,6 +16,7 @@ import static com.googlesource.gerrit.plugins.replication.StartReplicationCapability.START_REPLICATION; +import com.google.common.eventbus.EventBus; import com.google.gerrit.extensions.annotations.Exports; import com.google.gerrit.extensions.config.CapabilityDefinition; import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; @@ -23,14 +24,30 @@ import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.extensions.events.ProjectDeletedListener; import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.events.EventTypes; import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.ProvisionException; import com.google.inject.Scopes; import com.google.inject.assistedinject.FactoryModuleBuilder; import com.google.inject.internal.UniqueAnnotations; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.util.FS; class ReplicationModule extends AbstractModule { + private final Path cfgPath; + + @Inject + public ReplicationModule(SitePaths site) { + cfgPath = site.etc_dir.resolve("replication.config"); + } + @Override protected void configure() { install(new FactoryModuleBuilder().build(Destination.Factory.class)); @@ -57,7 +74,18 @@ install(new FactoryModuleBuilder().build(PushAll.Factory.class)); - bind(ReplicationConfig.class).to(AutoReloadConfigDecorator.class); + bind(EventBus.class).in(Scopes.SINGLETON); + bind(ReplicationDestinations.class).to(DestinationsCollection.class); + + if (getReplicationConfig().getBoolean("gerrit", "autoReload", false)) { + bind(ReplicationConfig.class).to(AutoReloadConfigDecorator.class); + bind(LifecycleListener.class) + .annotatedWith(UniqueAnnotations.create()) + .to(AutoReloadConfigDecorator.class); + } else { + bind(ReplicationConfig.class).to(ReplicationFileBasedConfig.class); + } + DynamicSet.setOf(binder(), ReplicationStateListener.class); DynamicSet.bind(binder(), ReplicationStateListener.class).to(ReplicationStateLogger.class); @@ -68,4 +96,15 @@ bind(TransportFactory.class).to(TransportFactoryImpl.class).in(Scopes.SINGLETON); } + + private FileBasedConfig getReplicationConfig() { + File replicationConfigFile = cfgPath.toFile(); + FileBasedConfig config = new FileBasedConfig(replicationConfigFile, FS.DETECTED); + try { + config.load(); + } catch (IOException | ConfigInvalidException e) { + throw new ProvisionException("Unable to load " + replicationConfigFile.getAbsolutePath(), e); + } + return config; + } }
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 8030d28..3e17167 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
@@ -26,6 +26,7 @@ import com.google.gerrit.server.events.EventDispatcher; import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Inject; +import com.google.inject.Provider; import com.googlesource.gerrit.plugins.replication.PushResultProcessing.GitUpdateProcessing; import com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType; import com.googlesource.gerrit.plugins.replication.ReplicationTasksStorage.ReplicateRefUpdate; @@ -50,7 +51,7 @@ private final WorkQueue workQueue; private final DynamicItem<EventDispatcher> dispatcher; - private final ReplicationConfig config; + private final Provider<ReplicationDestinations> destinations; // For Guice circular dependency private final ReplicationTasksStorage replicationTasksStorage; private volatile boolean running; private volatile boolean replaying; @@ -59,13 +60,13 @@ @Inject ReplicationQueue( WorkQueue wq, - ReplicationConfig rc, + Provider<ReplicationDestinations> rd, DynamicItem<EventDispatcher> dis, ReplicationStateListeners sl, ReplicationTasksStorage rts) { workQueue = wq; dispatcher = dis; - config = rc; + destinations = rd; stateLog = sl; replicationTasksStorage = rts; beforeStartupEventsQueue = Queues.newConcurrentLinkedQueue(); @@ -74,7 +75,7 @@ @Override public void start() { if (!running) { - config.startup(workQueue); + destinations.get().startup(workQueue); running = true; firePendingEvents(); fireBeforeStartupEvents(); @@ -84,7 +85,7 @@ @Override public void stop() { running = false; - int discarded = config.shutdown(); + int discarded = destinations.get().shutdown(); if (discarded > 0) { repLog.warn("Canceled {} replication events during shutdown", discarded); } @@ -110,7 +111,7 @@ return; } - for (Destination cfg : config.getDestinations(FilterType.ALL)) { + for (Destination cfg : destinations.get().getAll(FilterType.ALL)) { if (cfg.wouldPushProject(project)) { for (URIish uri : cfg.getURIs(project, urlMatch)) { cfg.schedule(project, PushOne.ALL_REFS, uri, state, now); @@ -137,8 +138,8 @@ return; } - Project.NameKey project = new Project.NameKey(projectName); - for (Destination cfg : config.getDestinations(FilterType.ALL)) { + Project.NameKey project = Project.nameKey(projectName); + for (Destination cfg : destinations.get().getAll(FilterType.ALL)) { if (cfg.wouldPushProject(project) && cfg.wouldPushRef(refName)) { for (URIish uri : cfg.getURIs(project, null)) { replicationTasksStorage.persist( @@ -170,15 +171,15 @@ @Override public void onProjectDeleted(ProjectDeletedListener.Event event) { - Project.NameKey p = new Project.NameKey(event.getProjectName()); - config.getURIs(Optional.empty(), p, FilterType.PROJECT_DELETION).entries().stream() + Project.NameKey p = Project.nameKey(event.getProjectName()); + destinations.get().getURIs(Optional.empty(), p, FilterType.PROJECT_DELETION).entries().stream() .forEach(e -> e.getKey().scheduleDeleteProject(e.getValue(), p)); } @Override public void onHeadUpdated(HeadUpdatedListener.Event event) { - Project.NameKey p = new Project.NameKey(event.getProjectName()); - config.getURIs(Optional.empty(), p, FilterType.ALL).entries().stream() + Project.NameKey p = Project.nameKey(event.getProjectName()); + destinations.get().getURIs(Optional.empty(), p, FilterType.ALL).entries().stream() .forEach(e -> e.getKey().scheduleUpdateHead(e.getValue(), p, event.getNewHeadName())); }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationScheduledEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationScheduledEvent.java index aa965fe..005d983 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationScheduledEvent.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationScheduledEvent.java
@@ -38,6 +38,6 @@ @Override public Project.NameKey getProjectNameKey() { - return new Project.NameKey(project); + return Project.nameKey(project); } }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index 32bc630..d9d4bae 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -419,6 +419,15 @@ By default, replicates without matching, i.e. replicates everything to all remotes. +remote.NAME.slowLatencyThreshold +: the time duration after which the replication of a project to this + destination will be considered "slow". A slow project replication + will cause additional metrics to be exposed for further investigation. + See [metrics.md](metrics.md) for further details. + + default: 15 minutes + + File `secure.config` --------------------
diff --git a/src/main/resources/Documentation/metrics.md b/src/main/resources/Documentation/metrics.md new file mode 100644 index 0000000..ef8b150 --- /dev/null +++ b/src/main/resources/Documentation/metrics.md
@@ -0,0 +1,61 @@ +# Metrics + +Some metrics are emitted when replication occurs to a remote destination. +The granularity of the metrics recorded is at destination level, however when a particular project replication is flagged +as slow. This happens when the replication took longer than allowed threshold (see _remote.NAME.slowLatencyThreshold_ in [config.md](config.md)) + +The reason only slow metrics are published, rather than all, is to contain their number, which, on a big Gerrit installation +could potentially be considerably big. + +### Project level + +* plugins_replication_latency_slower_than_<threshold>_<destinationName>_<ProjectName> - Time spent pushing <ProjectName> to remote <destinationName> (in ms) + +### Destination level + +* plugins_replication_replication_delay_<destinationName> - Time spent waiting before pushing to remote <destinationName> (in ms) +* plugins_replication_replication_retries_<destinationName> - Number of retries when pushing to remote <destinationName> +* plugins_replication_replication_latency_<destinationName> - Time spent pushing to remote <destinationName> (in ms) + +### Example +``` +# HELP plugins_replication_replication_delay_destination Generated from Dropwizard metric import (metric=plugins/replication/replication_delay/destination, type=com.codahale.metrics.Histogram) +# TYPE plugins_replication_replication_delay_destination summary +plugins_replication_replication_delay_destinationName{quantile="0.5",} 65726.0 +plugins_replication_replication_delay_destinationName{quantile="0.75",} 65726.0 +plugins_replication_replication_delay_destinationName{quantile="0.95",} 65726.0 +plugins_replication_replication_delay_destinationName{quantile="0.98",} 65726.0 +plugins_replication_replication_delay_destinationName{quantile="0.99",} 65726.0 +plugins_replication_replication_delay_destinationName{quantile="0.999",} 65726.0 +plugins_replication_replication_delay_destinationName_count 3.0 + +# HELP plugins_replication_replication_retries_destination Generated from Dropwizard metric import (metric=plugins/replication/replication_retries/destination, type=com.codahale.metrics.Histogram) +# TYPE plugins_replication_replication_retries_destination summary +plugins_replication_replication_retries_destinationName{quantile="0.5",} 1.0 +plugins_replication_replication_retries_destinationName{quantile="0.75",} 1.0 +plugins_replication_replication_retries_destinationName{quantile="0.95",} 1.0 +plugins_replication_replication_retries_destinationName{quantile="0.98",} 1.0 +plugins_replication_replication_retries_destinationName{quantile="0.99",} 1.0 +plugins_replication_replication_retries_destinationName{quantile="0.999",} 1.0 +plugins_replication_replication_retries_destinationName_count 3.0 + +# HELP plugins_replication_replication_latency_destinationName Generated from Dropwizard metric import (metric=plugins/replication/replication_latency/destinationName, type=com.codahale.metrics.Timer) +# TYPE plugins_replication_replication_latency_destinationName summary +plugins_replication_replication_latency_destinationName{quantile="0.5",} 0.21199641400000002 +plugins_replication_replication_latency_destinationName{quantile="0.75",} 0.321083881 +plugins_replication_replication_latency_destinationName{quantile="0.95",} 0.321083881 +plugins_replication_replication_latency_destinationName{quantile="0.98",} 0.321083881 +plugins_replication_replication_latency_destinationName{quantile="0.99",} 0.321083881 +plugins_replication_replication_latency_destinationName{quantile="0.999",} 0.321083881 +plugins_replication_replication_latency_destinationName_count 2.0 + +# HELP plugins_replication_latency_slower_than_60_destinationName_projectName Generated from Dropwizard metric import (metric=plugins/replication/latency_slower_than/60/destinationName/projectName, type=com.codahale.metrics.Histogram) +# TYPE plugins_replication_latency_slower_than_60_destinationName_projectName summary +plugins_replication_latency_slower_than_60_destinationName_projectName{quantile="0.5",} 278.0 +plugins_replication_latency_slower_than_60_destinationName_projectName{quantile="0.75",} 278.0 +plugins_replication_latency_slower_than_60_destinationName_projectName{quantile="0.95",} 278.0 +plugins_replication_latency_slower_than_60_destinationName_projectName{quantile="0.98",} 278.0 +plugins_replication_latency_slower_than_60_destinationName_projectName{quantile="0.99",} 278.0 +plugins_replication_latency_slower_than_60_destinationName_projectName{quantile="0.999",} 278.0 +plugins_replication_latency_slower_than_60_destinationName_projectName 1.0 +``` \ No newline at end of file
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/AbstractConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/AbstractConfigTest.java index 77dc1cc..3932fb3 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/AbstractConfigTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/AbstractConfigTest.java
@@ -16,26 +16,28 @@ import static com.google.common.truth.Truth.assertThat; import static java.nio.file.Files.createTempDirectory; -import static org.easymock.EasyMock.anyObject; -import static org.easymock.EasyMock.createMock; -import static org.easymock.EasyMock.createNiceMock; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.getCurrentArguments; -import static org.easymock.EasyMock.isA; -import static org.easymock.EasyMock.replay; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import com.google.common.eventbus.EventBus; import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Injector; import com.google.inject.Module; +import com.google.inject.util.Providers; import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.stream.Collectors; -import org.easymock.IAnswer; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.util.FS; import org.junit.Before; import org.junit.Ignore; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; @Ignore public abstract class AbstractConfigTest { @@ -43,6 +45,10 @@ protected final SitePaths sitePaths; protected final Destination.Factory destinationFactoryMock; protected final Path pluginDataPath; + protected ReplicationQueue replicationQueueMock; + protected WorkQueue workQueueMock; + protected EventBus eventBus = new EventBus(); + protected FakeExecutorService executorService = new FakeExecutorService(); static class FakeDestination extends Destination { public final DestinationConfiguration config; @@ -53,11 +59,9 @@ } private static Injector injectorMock() { - Injector injector = createNiceMock(Injector.class); - Injector childInjectorMock = createNiceMock(Injector.class); - expect(injector.createChildInjector((Module) anyObject())).andReturn(childInjectorMock); - replay(childInjectorMock); - replay(injector); + Injector injector = mock(Injector.class); + Injector childInjectorMock = mock(Injector.class); + when(injector.createChildInjector(any(Module.class))).thenReturn(childInjectorMock); return injector; } } @@ -66,21 +70,25 @@ sitePath = createTempPath("site"); sitePaths = new SitePaths(sitePath); pluginDataPath = createTempPath("data"); - destinationFactoryMock = createMock(Destination.Factory.class); + destinationFactoryMock = mock(Destination.Factory.class); } @Before public void setup() { - expect(destinationFactoryMock.create(isA(DestinationConfiguration.class))) - .andAnswer( - new IAnswer<Destination>() { + when(destinationFactoryMock.create(any(DestinationConfiguration.class))) + .thenAnswer( + new Answer<Destination>() { @Override - public Destination answer() throws Throwable { - return new FakeDestination((DestinationConfiguration) getCurrentArguments()[0]); + public Destination answer(InvocationOnMock invocation) throws Throwable { + return new FakeDestination((DestinationConfiguration) invocation.getArguments()[0]); } - }) - .anyTimes(); - replay(destinationFactoryMock); + }); + + replicationQueueMock = mock(ReplicationQueue.class); + when(replicationQueueMock.isRunning()).thenReturn(Boolean.TRUE); + + workQueueMock = mock(WorkQueue.class); + when(workQueueMock.createQueue(anyInt(), any(String.class))).thenReturn(executorService); } protected static Path createTempPath(String prefix) throws IOException { @@ -113,4 +121,13 @@ assertThatIsDestination(matchingDestinations.get(0), remoteName, remoteUrls); } + + protected DestinationsCollection newDestinationsCollections( + ReplicationFileBasedConfig replicationFileBasedConfig) throws ConfigInvalidException { + return new DestinationsCollection( + destinationFactoryMock, + Providers.of(replicationQueueMock), + replicationFileBasedConfig, + eventBus); + } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecoratorTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecoratorTest.java index 211cafa..4b16314 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecoratorTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecoratorTest.java
@@ -15,127 +15,18 @@ package com.googlesource.gerrit.plugins.replication; import static com.google.common.truth.Truth.assertThat; -import static org.easymock.EasyMock.anyInt; -import static org.easymock.EasyMock.anyObject; -import static org.easymock.EasyMock.createNiceMock; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.replay; -import com.google.gerrit.server.git.WorkQueue; import com.google.inject.util.Providers; import com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType; import java.io.IOException; -import java.util.Collection; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.junit.Before; import org.junit.Test; public class AutoReloadConfigDecoratorTest extends AbstractConfigTest { - private AutoReloadConfigDecorator autoReloadConfig; - private ReplicationQueue replicationQueueMock; - private WorkQueue workQueueMock; - private FakeExecutorService executorService = new FakeExecutorService(); - - public class FakeExecutorService implements ScheduledExecutorService { - public Runnable refreshCommand; - - @Override - public void shutdown() {} - - @Override - public List<Runnable> shutdownNow() { - return null; - } - - @Override - public boolean isShutdown() { - return false; - } - - @Override - public boolean isTerminated() { - return false; - } - - @Override - public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { - return false; - } - - @Override - public <T> Future<T> submit(Callable<T> task) { - return null; - } - - @Override - public <T> Future<T> submit(Runnable task, T result) { - return null; - } - - @Override - public Future<?> submit(Runnable task) { - return null; - } - - @Override - public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) - throws InterruptedException { - return null; - } - - @Override - public <T> List<Future<T>> invokeAll( - Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) - throws InterruptedException { - return null; - } - - @Override - public <T> T invokeAny(Collection<? extends Callable<T>> tasks) - throws InterruptedException, ExecutionException { - return null; - } - - @Override - public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException { - return null; - } - - @Override - public void execute(Runnable command) {} - - @Override - public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { - return null; - } - - @Override - public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { - return null; - } - - @Override - public ScheduledFuture<?> scheduleAtFixedRate( - Runnable command, long initialDelay, long period, TimeUnit unit) { - refreshCommand = command; - return null; - } - - @Override - public ScheduledFuture<?> scheduleWithFixedDelay( - Runnable command, long initialDelay, long delay, TimeUnit unit) { - return null; - } - } + ReplicationFileBasedConfig replicationFileBasedConfig; public AutoReloadConfigDecoratorTest() throws IOException { super(); @@ -146,39 +37,7 @@ public void setup() { super.setup(); - setupMocks(); - } - - private void setupMocks() { - replicationQueueMock = createNiceMock(ReplicationQueue.class); - expect(replicationQueueMock.isRunning()).andReturn(true); - replay(replicationQueueMock); - - workQueueMock = createNiceMock(WorkQueue.class); - expect(workQueueMock.createQueue(anyInt(), anyObject(String.class))).andReturn(executorService); - replay(workQueueMock); - } - - @Test - public void shouldLoadNotEmptyInitialReplicationConfig() throws Exception { - FileBasedConfig replicationConfig = newReplicationConfig(); - String remoteName = "foo"; - String remoteUrl = "ssh://git@git.somewhere.com/${name}"; - replicationConfig.setString("remote", remoteName, "url", remoteUrl); - replicationConfig.save(); - - autoReloadConfig = - new AutoReloadConfigDecorator( - sitePaths, - destinationFactoryMock, - Providers.of(replicationQueueMock), - pluginDataPath, - "replication", - workQueueMock); - - List<Destination> destinations = autoReloadConfig.getDestinations(FilterType.ALL); - assertThat(destinations).hasSize(1); - assertThatIsDestination(destinations.get(0), remoteName, remoteUrl); + replicationFileBasedConfig = newReplicationFileBasedConfig(); } @Test @@ -190,17 +49,12 @@ replicationConfig.setString("remote", remoteName1, "url", remoteUrl1); replicationConfig.save(); - autoReloadConfig = - new AutoReloadConfigDecorator( - sitePaths, - destinationFactoryMock, - Providers.of(replicationQueueMock), - pluginDataPath, - "replication", - workQueueMock); - autoReloadConfig.startup(workQueueMock); + newAutoReloadConfig().start(); - List<Destination> destinations = autoReloadConfig.getDestinations(FilterType.ALL); + DestinationsCollection destinationsCollections = + newDestinationsCollections(replicationFileBasedConfig); + destinationsCollections.startup(workQueueMock); + List<Destination> destinations = destinationsCollections.getAll(FilterType.ALL); assertThat(destinations).hasSize(1); assertThatIsDestination(destinations.get(0), remoteName1, remoteUrl1); @@ -212,7 +66,7 @@ replicationConfig.save(); executorService.refreshCommand.run(); - destinations = autoReloadConfig.getDestinations(FilterType.ALL); + destinations = destinationsCollections.getAll(FilterType.ALL); assertThat(destinations).hasSize(2); assertThatContainsDestination(destinations, remoteName1, remoteUrl1); assertThatContainsDestination(destinations, remoteName2, remoteUrl2); @@ -227,17 +81,10 @@ replicationConfig.setString("remote", remoteName1, "url", remoteUrl1); replicationConfig.save(); - autoReloadConfig = - new AutoReloadConfigDecorator( - sitePaths, - destinationFactoryMock, - Providers.of(replicationQueueMock), - pluginDataPath, - "replication", - workQueueMock); - autoReloadConfig.startup(workQueueMock); - - List<Destination> destinations = autoReloadConfig.getDestinations(FilterType.ALL); + DestinationsCollection destinationsCollections = + newDestinationsCollections(replicationFileBasedConfig); + destinationsCollections.startup(workQueueMock); + List<Destination> destinations = destinationsCollections.getAll(FilterType.ALL); assertThat(destinations).hasSize(1); assertThatIsDestination(destinations.get(0), remoteName1, remoteUrl1); @@ -247,6 +94,22 @@ replicationConfig.save(); executorService.refreshCommand.run(); - assertThat(autoReloadConfig.getDestinations(FilterType.ALL)).isEqualTo(destinations); + assertThat(destinationsCollections.getAll(FilterType.ALL)).isEqualTo(destinations); + } + + private AutoReloadConfigDecorator newAutoReloadConfig() { + AutoReloadRunnable autoReloadRunnable = + new AutoReloadRunnable( + replicationFileBasedConfig, + sitePaths, + pluginDataPath, + eventBus, + Providers.of(replicationQueueMock)); + return new AutoReloadConfigDecorator( + "replication", workQueueMock, replicationFileBasedConfig, autoReloadRunnable, eventBus); + } + + private ReplicationFileBasedConfig newReplicationFileBasedConfig() { + return new ReplicationFileBasedConfig(sitePaths, pluginDataPath); } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/FakeExecutorService.java b/src/test/java/com/googlesource/gerrit/plugins/replication/FakeExecutorService.java new file mode 100644 index 0000000..2f7059e --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/FakeExecutorService.java
@@ -0,0 +1,118 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class FakeExecutorService implements ScheduledExecutorService { + public Runnable refreshCommand = () -> {}; + + @Override + public void shutdown() {} + + @Override + public List<Runnable> shutdownNow() { + return null; + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return false; + } + + @Override + public <T> Future<T> submit(Callable<T> task) { + return null; + } + + @Override + public <T> Future<T> submit(Runnable task, T result) { + return null; + } + + @Override + public Future<?> submit(Runnable task) { + return null; + } + + @Override + public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) + throws InterruptedException { + return null; + } + + @Override + public <T> List<Future<T>> invokeAll( + Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + return null; + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> tasks) + throws InterruptedException, ExecutionException { + return null; + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return null; + } + + @Override + public void execute(Runnable command) {} + + @Override + public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { + return null; + } + + @Override + public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { + return null; + } + + @Override + public ScheduledFuture<?> scheduleAtFixedRate( + Runnable command, long initialDelay, long period, TimeUnit unit) { + refreshCommand = command; + return null; + } + + @Override + public ScheduledFuture<?> scheduleWithFixedDelay( + Runnable command, long initialDelay, long delay, TimeUnit unit) { + return null; + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java index 0f6d629..2ee9a39 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/GitUpdateProcessingTest.java
@@ -14,11 +14,10 @@ package com.googlesource.gerrit.plugins.replication; -import static org.easymock.EasyMock.createMock; -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 static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import com.google.gerrit.server.events.EventDispatcher; import com.google.gerrit.server.permissions.PermissionBackendException; @@ -36,14 +35,12 @@ @Before public void setUp() throws Exception { - dispatcherMock = createMock(EventDispatcher.class); - replay(dispatcherMock); + dispatcherMock = mock(EventDispatcher.class); gitUpdateProcessing = new GitUpdateProcessing(dispatcherMock); } @Test public void headRefReplicated() throws URISyntaxException, PermissionBackendException { - reset(dispatcherMock); RefReplicatedEvent expectedEvent = new RefReplicatedEvent( "someProject", @@ -51,9 +48,6 @@ "someHost", RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); - dispatcherMock.postEvent(RefReplicatedEventEquals.eqEvent(expectedEvent)); - expectLastCall().once(); - replay(dispatcherMock); gitUpdateProcessing.onRefReplicatedToOneNode( "someProject", @@ -61,12 +55,11 @@ new URIish("git://someHost/someProject.git"), RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); - verify(dispatcherMock); + verify(dispatcherMock, times(1)).postEvent(eq(expectedEvent)); } @Test public void changeRefReplicated() throws URISyntaxException, PermissionBackendException { - reset(dispatcherMock); RefReplicatedEvent expectedEvent = new RefReplicatedEvent( "someProject", @@ -74,9 +67,6 @@ "someHost", RefPushResult.FAILED, RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD); - dispatcherMock.postEvent(RefReplicatedEventEquals.eqEvent(expectedEvent)); - expectLastCall().once(); - replay(dispatcherMock); gitUpdateProcessing.onRefReplicatedToOneNode( "someProject", @@ -84,19 +74,15 @@ new URIish("git://someHost/someProject.git"), RefPushResult.FAILED, RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD); - verify(dispatcherMock); + verify(dispatcherMock, times(1)).postEvent(eq(expectedEvent)); } @Test public void onAllNodesReplicated() throws PermissionBackendException { - reset(dispatcherMock); RefReplicationDoneEvent expectedDoneEvent = new RefReplicationDoneEvent("someProject", "refs/heads/master", 5); - dispatcherMock.postEvent(RefReplicationDoneEventEquals.eqEvent(expectedDoneEvent)); - expectLastCall().once(); - replay(dispatcherMock); gitUpdateProcessing.onRefReplicatedToAllNodes("someProject", "refs/heads/master", 5); - verify(dispatcherMock); + verify(dispatcherMock, times(1)).postEvent(eq(expectedDoneEvent)); } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/PushOneTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/PushOneTest.java index c010ddb..ff902cf 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/PushOneTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/PushOneTest.java
@@ -14,14 +14,13 @@ package com.googlesource.gerrit.plugins.replication; -import static com.googlesource.gerrit.plugins.replication.RemoteRefUpdateCollectionMatcher.eqRemoteRef; -import static org.easymock.EasyMock.anyObject; -import static org.easymock.EasyMock.createNiceMock; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.getCurrentArguments; -import static org.easymock.EasyMock.replay; -import static org.easymock.EasyMock.verify; import static org.eclipse.jgit.lib.Ref.Storage.NEW; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; import com.google.gerrit.extensions.registration.DynamicItem; @@ -45,8 +44,6 @@ import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import junit.framework.AssertionFailedError; -import org.easymock.IAnswer; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.lib.Config; @@ -68,6 +65,8 @@ import org.eclipse.jgit.util.FS; import org.junit.Before; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; public class PushOneTest { private static final int TEST_PUSH_TIMEOUT_SECS = 10; @@ -86,7 +85,7 @@ private IdGenerator idGeneratorMock; private ReplicationStateListeners replicationStateListenersMock; private ReplicationMetrics replicationMetricsMock; - private Timer1.Context timerContextMock; + private Timer1.Context<String> timerContextMock; private ProjectCache projectCacheMock; private TransportFactory transportFactoryMock; private Transport transportMock; @@ -108,7 +107,7 @@ @Before public void setup() throws Exception { - projectNameKey = new Project.NameKey("fooProject"); + projectNameKey = Project.nameKey("fooProject"); urish = new URIish("http://foo.com/fooProject.git"); newLocalRef = @@ -126,6 +125,7 @@ setupMocks(); } + @SuppressWarnings("unchecked") private void setupMocks() throws Exception { FileBasedConfig config = new FileBasedConfig(new Config(), new File("/foo"), FS.DETECTED); config.setString("remote", "Replication", "push", "foo"); @@ -134,8 +134,8 @@ setupRepositoryMock(config); setupGitRepoManagerMock(); - projectStateMock = createNiceMock(ProjectState.class); - forProjectMock = createNiceMock(ForProject.class); + projectStateMock = mock(ProjectState.class); + forProjectMock = mock(ForProject.class); setupWithUserMock(); setupPermissionBackedMock(); @@ -144,46 +144,22 @@ setupRefSpecMock(); setupRemoteConfigMock(); - credentialsFactory = createNiceMock(CredentialsFactory.class); + credentialsFactory = mock(CredentialsFactory.class); setupFetchConnectionMock(); setupPushConnectionMock(); setupRequestScopeMock(); - idGeneratorMock = createNiceMock(IdGenerator.class); - replicationStateListenersMock = createNiceMock(ReplicationStateListeners.class); + idGeneratorMock = mock(IdGenerator.class); + replicationStateListenersMock = mock(ReplicationStateListeners.class); - timerContextMock = createNiceMock(Timer1.Context.class); + timerContextMock = mock(Timer1.Context.class); setupReplicationMetricsMock(); setupTransportMock(); setupProjectCacheMock(); - replicationConfigMock = createNiceMock(ReplicationConfig.class); - - replay( - gitRepositoryManagerMock, - refUpdateMock, - repositoryMock, - permissionBackendMock, - destinationMock, - remoteConfigMock, - credentialsFactory, - threadRequestScoperMock, - idGeneratorMock, - replicationStateListenersMock, - replicationMetricsMock, - projectCacheMock, - timerContextMock, - transportFactoryMock, - projectStateMock, - withUserMock, - forProjectMock, - fetchConnection, - pushConnection, - refSpecMock, - refDatabaseMock, - replicationConfigMock); + replicationConfigMock = mock(ReplicationConfig.class); } @Test @@ -212,10 +188,7 @@ PushResult pushResult = new PushResult(); - expect(transportMock.push(anyObject(), eqRemoteRef(expectedUpdates))) - .andReturn(pushResult) - .once(); - replay(transportMock); + when(transportMock.push(any(), eq(expectedUpdates))).thenReturn(pushResult); PushOne pushOne = createPushOne(replicationPushFilter); @@ -223,8 +196,6 @@ pushOne.run(); isCallFinished.await(TEST_PUSH_TIMEOUT_SECS, TimeUnit.SECONDS); - - verify(transportMock); } @Test @@ -241,12 +212,6 @@ } }); - // easymock way to check if method was never called - expect(transportMock.push(anyObject(), anyObject())) - .andThrow(new AssertionFailedError()) - .anyTimes(); - replay(transportMock); - PushOne pushOne = createPushOne(replicationPushFilter); pushOne.addRef(PushOne.ALL_REFS); @@ -254,7 +219,7 @@ isCallFinished.await(10, TimeUnit.SECONDS); - verify(transportMock); + verify(transportMock, never()).push(any(), any()); } private PushOne createPushOne(DynamicItem<ReplicationPushFilter> replicationPushFilter) { @@ -281,31 +246,31 @@ } private void setupProjectCacheMock() throws IOException { - projectCacheMock = createNiceMock(ProjectCache.class); - expect(projectCacheMock.checkedGet(projectNameKey)).andReturn(projectStateMock); + projectCacheMock = mock(ProjectCache.class); + when(projectCacheMock.checkedGet(projectNameKey)).thenReturn(projectStateMock); } private void setupTransportMock() throws NotSupportedException, TransportException { - transportMock = createNiceMock(Transport.class); - expect(transportMock.openFetch()).andReturn(fetchConnection); - transportFactoryMock = createNiceMock(TransportFactory.class); - expect(transportFactoryMock.open(repositoryMock, urish)).andReturn(transportMock).anyTimes(); + transportMock = mock(Transport.class); + when(transportMock.openFetch()).thenReturn(fetchConnection); + transportFactoryMock = mock(TransportFactory.class); + when(transportFactoryMock.open(repositoryMock, urish)).thenReturn(transportMock); } private void setupReplicationMetricsMock() { - replicationMetricsMock = createNiceMock(ReplicationMetrics.class); - expect(replicationMetricsMock.start(anyObject())).andReturn(timerContextMock); + replicationMetricsMock = mock(ReplicationMetrics.class); + when(replicationMetricsMock.start(any())).thenReturn(timerContextMock); } private void setupRequestScopeMock() { - threadRequestScoperMock = createNiceMock(PerThreadRequestScope.Scoper.class); - expect(threadRequestScoperMock.scope(anyObject())) - .andAnswer( - new IAnswer<Callable<Object>>() { + threadRequestScoperMock = mock(PerThreadRequestScope.Scoper.class); + when(threadRequestScoperMock.scope(any())) + .thenAnswer( + new Answer<Callable<Object>>() { @SuppressWarnings("unchecked") @Override - public Callable<Object> answer() throws Throwable { - Callable<Object> originalCall = (Callable<Object>) getCurrentArguments()[0]; + public Callable<Object> answer(InvocationOnMock invocation) throws Throwable { + Callable<Object> originalCall = (Callable<Object>) invocation.getArguments()[0]; return new Callable<Object>() { @Override @@ -316,66 +281,64 @@ } }; } - }) - .anyTimes(); + }); } private void setupPushConnectionMock() { - pushConnection = createNiceMock(PushConnection.class); - expect(pushConnection.getRefsMap()).andReturn(remoteRefs); + pushConnection = mock(PushConnection.class); + when(pushConnection.getRefsMap()).thenReturn(remoteRefs); } private void setupFetchConnectionMock() { - fetchConnection = createNiceMock(FetchConnection.class); - expect(fetchConnection.getRefsMap()).andReturn(remoteRefs); + fetchConnection = mock(FetchConnection.class); + when(fetchConnection.getRefsMap()).thenReturn(remoteRefs); } private void setupRemoteConfigMock() { - remoteConfigMock = createNiceMock(RemoteConfig.class); - expect(remoteConfigMock.getPushRefSpecs()).andReturn(ImmutableList.of(refSpecMock)); + remoteConfigMock = mock(RemoteConfig.class); + when(remoteConfigMock.getPushRefSpecs()).thenReturn(ImmutableList.of(refSpecMock)); } private void setupRefSpecMock() { - refSpecMock = createNiceMock(RefSpec.class); - expect(refSpecMock.matchSource(anyObject(String.class))).andReturn(true); - expect(refSpecMock.expandFromSource(anyObject(String.class))).andReturn(refSpecMock); - expect(refSpecMock.getDestination()).andReturn("fooProject").anyTimes(); - expect(refSpecMock.isForceUpdate()).andReturn(false).anyTimes(); + refSpecMock = mock(RefSpec.class); + when(refSpecMock.matchSource(any(String.class))).thenReturn(true); + when(refSpecMock.expandFromSource(any(String.class))).thenReturn(refSpecMock); + when(refSpecMock.getDestination()).thenReturn("fooProject"); + when(refSpecMock.isForceUpdate()).thenReturn(false); } private void setupDestinationMock() { - destinationMock = createNiceMock(Destination.class); - expect(destinationMock.requestRunway(anyObject())).andReturn(RunwayStatus.allowed()); + destinationMock = mock(Destination.class); + when(destinationMock.requestRunway(any())).thenReturn(RunwayStatus.allowed()); } private void setupPermissionBackedMock() { - permissionBackendMock = createNiceMock(PermissionBackend.class); - expect(permissionBackendMock.currentUser()).andReturn(withUserMock); + permissionBackendMock = mock(PermissionBackend.class); + when(permissionBackendMock.currentUser()).thenReturn(withUserMock); } private void setupWithUserMock() { - withUserMock = createNiceMock(WithUser.class); - expect(withUserMock.project(projectNameKey)).andReturn(forProjectMock); + withUserMock = mock(WithUser.class); + when(withUserMock.project(projectNameKey)).thenReturn(forProjectMock); } private void setupGitRepoManagerMock() throws IOException { - gitRepositoryManagerMock = createNiceMock(GitRepositoryManager.class); - expect(gitRepositoryManagerMock.openRepository(projectNameKey)).andReturn(repositoryMock); + gitRepositoryManagerMock = mock(GitRepositoryManager.class); + when(gitRepositoryManagerMock.openRepository(projectNameKey)).thenReturn(repositoryMock); } private void setupRepositoryMock(FileBasedConfig config) throws IOException { - repositoryMock = createNiceMock(Repository.class); - refDatabaseMock = createNiceMock(RefDatabase.class); - expect(repositoryMock.getConfig()).andReturn(config).anyTimes(); - expect(repositoryMock.getRefDatabase()).andReturn(refDatabaseMock); - expect(refDatabaseMock.getRefs()).andReturn(localRefs); - expect(repositoryMock.updateRef("fooProject")).andReturn(refUpdateMock); + repositoryMock = mock(Repository.class); + refDatabaseMock = mock(RefDatabase.class); + when(repositoryMock.getConfig()).thenReturn(config); + when(repositoryMock.getRefDatabase()).thenReturn(refDatabaseMock); + when(refDatabaseMock.getRefs()).thenReturn(localRefs); + when(repositoryMock.updateRef("fooProject")).thenReturn(refUpdateMock); } private void setupRefUpdateMock() { - refUpdateMock = createNiceMock(RefUpdate.class); - expect(refUpdateMock.getOldObjectId()) - .andReturn(ObjectId.fromString("0000000000000000000000000000000000000001")) - .anyTimes(); + refUpdateMock = mock(RefUpdate.class); + when(refUpdateMock.getOldObjectId()) + .thenReturn(ObjectId.fromString("0000000000000000000000000000000000000001")); } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java b/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java deleted file mode 100644 index 983e97f..0000000 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicatedEventEquals.java +++ /dev/null
@@ -1,83 +0,0 @@ -// 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; - } - if (!equals(expected.refStatus, actualRefReplicatedEvent.refStatus)) { - 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("\" and refStatus \""); - buffer.append(expected.refStatus); - 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 deleted file mode 100644 index d1284e1..0000000 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/RefReplicationDoneEventEquals.java +++ /dev/null
@@ -1,73 +0,0 @@ -// 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/RemoteRefUpdateCollectionMatcher.java b/src/test/java/com/googlesource/gerrit/plugins/replication/RemoteRefUpdateCollectionMatcher.java deleted file mode 100644 index 111a792..0000000 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/RemoteRefUpdateCollectionMatcher.java +++ /dev/null
@@ -1,64 +0,0 @@ -// Copyright (C) 2019 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.googlesource.gerrit.plugins.replication; - -import java.util.Collection; -import java.util.Objects; -import org.easymock.EasyMock; -import org.easymock.IArgumentMatcher; -import org.eclipse.jgit.transport.RemoteRefUpdate; - -public class RemoteRefUpdateCollectionMatcher implements IArgumentMatcher { - Collection<RemoteRefUpdate> expectedRemoteRefs; - - public static Collection<RemoteRefUpdate> eqRemoteRef( - Collection<RemoteRefUpdate> expectedRemoteRefs) { - EasyMock.reportMatcher(new RemoteRefUpdateCollectionMatcher(expectedRemoteRefs)); - return null; - } - - public RemoteRefUpdateCollectionMatcher(Collection<RemoteRefUpdate> expectedRemoteRefs) { - this.expectedRemoteRefs = expectedRemoteRefs; - } - - @Override - public boolean matches(Object argument) { - if (!(argument instanceof Collection)) return false; - - @SuppressWarnings("unchecked") - Collection<RemoteRefUpdate> refs = (Collection<RemoteRefUpdate>) argument; - - if (expectedRemoteRefs.size() != refs.size()) return false; - return refs.stream() - .allMatch( - ref -> expectedRemoteRefs.stream().anyMatch(expectedRef -> compare(ref, expectedRef))); - } - - @Override - public void appendTo(StringBuffer buffer) { - buffer.append("expected:" + expectedRemoteRefs.toString()); - } - - private boolean compare(RemoteRefUpdate ref, RemoteRefUpdate expectedRef) { - return Objects.equals(ref.getRemoteName(), expectedRef.getRemoteName()) - && Objects.equals(ref.getStatus(), expectedRef.getStatus()) - && Objects.equals(ref.getExpectedOldObjectId(), expectedRef.getExpectedOldObjectId()) - && Objects.equals(ref.getNewObjectId(), expectedRef.getNewObjectId()) - && Objects.equals(ref.isFastForward(), expectedRef.isFastForward()) - && Objects.equals(ref.getSrcRef(), expectedRef.getSrcRef()) - && Objects.equals(ref.isForceUpdate(), expectedRef.isForceUpdate()) - && Objects.equals(ref.getMessage(), expectedRef.getMessage()); - } -}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfigTest.java index 36cc209..f2b027c 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfigTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfigTest.java
@@ -19,7 +19,6 @@ import com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType; import java.io.IOException; import java.util.List; -import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.junit.Test; @@ -37,8 +36,10 @@ config.setString("remote", remoteName, "url", remoteUrl); config.save(); - ReplicationFileBasedConfig replicationConfig = newReplicationFileBasedConfig(); - List<Destination> destinations = replicationConfig.getDestinations(FilterType.ALL); + DestinationsCollection destinationsCollections = + newDestinationsCollections(newReplicationFileBasedConfig()); + destinationsCollections.startup(workQueueMock); + List<Destination> destinations = destinationsCollections.getAll(FilterType.ALL); assertThat(destinations).hasSize(1); assertThatIsDestination(destinations.get(0), remoteName, remoteUrl); @@ -55,18 +56,19 @@ config.setString("remote", remoteName2, "url", remoteUrl2); config.save(); - ReplicationFileBasedConfig replicationConfig = newReplicationFileBasedConfig(); - List<Destination> destinations = replicationConfig.getDestinations(FilterType.ALL); + DestinationsCollection destinationsCollections = + newDestinationsCollections(newReplicationFileBasedConfig()); + destinationsCollections.startup(workQueueMock); + List<Destination> destinations = destinationsCollections.getAll(FilterType.ALL); assertThat(destinations).hasSize(2); assertThatIsDestination(destinations.get(0), remoteName1, remoteUrl1); assertThatIsDestination(destinations.get(1), remoteName2, remoteUrl2); } - private ReplicationFileBasedConfig newReplicationFileBasedConfig() - throws ConfigInvalidException, IOException { + private ReplicationFileBasedConfig newReplicationFileBasedConfig() { ReplicationFileBasedConfig replicationConfig = - new ReplicationFileBasedConfig(sitePaths, destinationFactoryMock, pluginDataPath); + new ReplicationFileBasedConfig(sitePaths, pluginDataPath); return replicationConfig; } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java index a6a8ac8..eed5158 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationIT.java
@@ -100,7 +100,7 @@ assertThat(listReplicationTasks("refs/meta/config")).hasSize(1); - waitUntil(() -> projectExists(new Project.NameKey(sourceProject + "replica.git"))); + waitUntil(() -> projectExists(Project.nameKey(sourceProject + "replica.git"))); ProjectInfo replicaProject = gApi.projects().name(sourceProject + "replica").get(); assertThat(replicaProject).isNotNull(); @@ -115,7 +115,7 @@ Result pushResult = createChange(); RevCommit sourceCommit = pushResult.getCommit(); - String sourceRef = pushResult.getPatchSet().getRefName(); + String sourceRef = pushResult.getPatchSet().refName(); assertThat(listReplicationTasks("refs/changes/\\d*/\\d*/\\d*")).hasSize(1); @@ -164,7 +164,7 @@ Result pushResult = createChange(); RevCommit sourceCommit = pushResult.getCommit(); - String sourceRef = pushResult.getPatchSet().getRefName(); + String sourceRef = pushResult.getPatchSet().refName(); assertThat(listReplicationTasks("refs/changes/\\d*/\\d*/\\d*")).hasSize(2); @@ -267,10 +267,10 @@ setReplicationDestination(remoteName, "replica", ALL_PROJECTS); Result pushResult = createChange(); - shutdownConfig(); + shutdownDestinations(); pushResult.getCommit(); - String sourceRef = pushResult.getPatchSet().getRefName(); + String sourceRef = pushResult.getPatchSet().refName(); assertThrows( InterruptedException.class, @@ -293,10 +293,10 @@ reloadConfig(); Result pushResult = createChange(); - shutdownConfig(); + shutdownDestinations(); RevCommit sourceCommit = pushResult.getCommit(); - String sourceRef = pushResult.getPatchSet().getRefName(); + String sourceRef = pushResult.getPatchSet().refName(); try (Repository repo = repoManager.openRepository(targetProject)) { waitUntil(() -> checkedGetRef(repo, sourceRef) != null); @@ -318,7 +318,7 @@ replicationQueueStart(); RevCommit sourceCommit = pushResult.getCommit(); - String sourceRef = pushResult.getPatchSet().getRefName(); + String sourceRef = pushResult.getPatchSet().refName(); try (Repository repo = repoManager.openRepository(targetProject)) { waitUntil(() -> checkedGetRef(repo, sourceRef) != null); @@ -357,6 +357,7 @@ config.setStringList("remote", remoteName, "url", replicaUrls); config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY); project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj)); + config.setBoolean("gerrit", null, "autoReload", true); config.save(); } @@ -365,11 +366,11 @@ } private void reloadConfig() { - getAutoReloadConfigDecoratorInstance().forceReload(); + getAutoReloadConfigDecoratorInstance().reload(); } - private void shutdownConfig() { - getAutoReloadConfigDecoratorInstance().shutdown(); + private void shutdownDestinations() { + getInstance(DestinationsCollection.class).shutdown(); } private void replicationQueueStart() {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java index 193af1e..e0de577 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationStateTest.java
@@ -15,11 +15,9 @@ package com.googlesource.gerrit.plugins.replication; import static com.google.common.truth.Truth.assertThat; -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.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import com.googlesource.gerrit.plugins.replication.ReplicationState.RefPushResult; import java.net.URISyntaxException; @@ -35,8 +33,7 @@ @Before public void setUp() throws Exception { - pushResultProcessingMock = createNiceMock(PushResultProcessing.class); - replay(pushResultProcessingMock); + pushResultProcessingMock = mock(PushResultProcessing.class); replicationState = new ReplicationState(pushResultProcessingMock); } @@ -53,52 +50,36 @@ @Test public void shouldFireOneReplicationEventWhenNothingToReplicate() { - resetToDefault(pushResultProcessingMock); - - // expected event - pushResultProcessingMock.onAllRefsReplicatedToAllNodes(0); - replay(pushResultProcessingMock); - // actual test replicationState.markAllPushTasksScheduled(); - verify(pushResultProcessingMock); + + // expected event + verify(pushResultProcessingMock).onAllRefsReplicatedToAllNodes(0); } @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, RemoteRefUpdate.Status.OK); - 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, RemoteRefUpdate.Status.OK); - verify(pushResultProcessingMock); + + // expected events + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "someProject", "someRef", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); + verify(pushResultProcessingMock).onRefReplicatedToAllNodes("someProject", "someRef", 1); + verify(pushResultProcessingMock).onAllRefsReplicatedToAllNodes(1); } @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, RemoteRefUpdate.Status.OK); - pushResultProcessingMock.onRefReplicatedToOneNode( - "someProject", "someRef", uri2, RefPushResult.FAILED, RemoteRefUpdate.Status.NON_EXISTING); - pushResultProcessingMock.onRefReplicatedToAllNodes("someProject", "someRef", 2); - pushResultProcessingMock.onAllRefsReplicatedToAllNodes(2); - replay(pushResultProcessingMock); - // actual test replicationState.increasePushTaskCount("someProject", "someRef"); replicationState.increasePushTaskCount("someProject", "someRef"); @@ -107,33 +88,29 @@ "someProject", "someRef", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); replicationState.notifyRefReplicated( "someProject", "someRef", uri2, RefPushResult.FAILED, RemoteRefUpdate.Status.NON_EXISTING); - verify(pushResultProcessingMock); + + // expected events + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "someProject", "someRef", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "someProject", + "someRef", + uri2, + RefPushResult.FAILED, + RemoteRefUpdate.Status.NON_EXISTING); + verify(pushResultProcessingMock).onRefReplicatedToAllNodes("someProject", "someRef", 2); + verify(pushResultProcessingMock).onAllRefsReplicatedToAllNodes(2); } @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, RemoteRefUpdate.Status.OK); - pushResultProcessingMock.onRefReplicatedToOneNode( - "someProject", "ref1", uri2, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); - pushResultProcessingMock.onRefReplicatedToOneNode( - "someProject", "ref1", uri3, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); - pushResultProcessingMock.onRefReplicatedToOneNode( - "someProject", "ref2", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); - pushResultProcessingMock.onRefReplicatedToOneNode( - "someProject", "ref2", uri2, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); - 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"); @@ -151,24 +128,32 @@ "someProject", "ref2", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); replicationState.notifyRefReplicated( "someProject", "ref2", uri2, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); - verify(pushResultProcessingMock); + + // expected events + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "someProject", "ref1", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "someProject", "ref1", uri2, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "someProject", "ref1", uri3, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "someProject", "ref2", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "someProject", "ref2", uri2, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); + verify(pushResultProcessingMock).onRefReplicatedToAllNodes("someProject", "ref1", 3); + verify(pushResultProcessingMock).onRefReplicatedToAllNodes("someProject", "ref2", 2); + verify(pushResultProcessingMock).onAllRefsReplicatedToAllNodes(5); } @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, RemoteRefUpdate.Status.OK); - pushResultProcessingMock.onRefReplicatedToOneNode( - "project2", "ref2", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); - 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"); @@ -177,25 +162,24 @@ "project1", "ref1", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); replicationState.notifyRefReplicated( "project2", "ref2", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); - verify(pushResultProcessingMock); + + // expected events + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "project1", "ref1", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "project2", "ref2", uri, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); + verify(pushResultProcessingMock).onRefReplicatedToAllNodes("project1", "ref1", 1); + verify(pushResultProcessingMock).onRefReplicatedToAllNodes("project2", "ref2", 1); + verify(pushResultProcessingMock).onAllRefsReplicatedToAllNodes(2); } @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, RemoteRefUpdate.Status.OK); - pushResultProcessingMock.onRefReplicatedToOneNode( - "someProject", "ref2", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); - 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"); @@ -204,7 +188,17 @@ replicationState.notifyRefReplicated( "someProject", "ref2", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); replicationState.markAllPushTasksScheduled(); - verify(pushResultProcessingMock); + + // expected events + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "someProject", "ref1", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); + verify(pushResultProcessingMock) + .onRefReplicatedToOneNode( + "someProject", "ref2", uri1, RefPushResult.SUCCEEDED, RemoteRefUpdate.Status.OK); + verify(pushResultProcessingMock).onRefReplicatedToAllNodes("someProject", "ref1", 1); + verify(pushResultProcessingMock).onRefReplicatedToAllNodes("someProject", "ref2", 1); + verify(pushResultProcessingMock).onAllRefsReplicatedToAllNodes(2); } @Test