Clarify logging when replication start hits a persist-only remote

A remote configured with threads=0 persists ref updates to
ReplicationTasksStorage for another node to pick up, but never
increments the push task count. Running `replication start --wait`
against such a remote therefore printed "No replication scheduled.",
which reads as if nothing happened even though the refs were
persisted.

Change-Id: I769e97c4b1f1b87267e3fc24ac054218a460dbbe
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 f17b50b..5704255 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -467,6 +467,11 @@
       boolean now,
       boolean fromStorage) {
     if (!isPushEnabled()) {
+      state.addPersistOnlyRemote(getRemoteConfigName());
+      repLog.atFine().log(
+          "Not scheduling push for %s:%s => %s; remote %s is persist-only (threads=0),"
+              + " task was written to task storage for another node to push",
+          project, refs, uri, getRemoteConfigName());
       return;
     }
     ImmutableSet.Builder<String> toSchedule = ImmutableSet.builder();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStarter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStarter.java
index fc88c34..39669e3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStarter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationStarter.java
@@ -23,6 +23,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 @Singleton
 class ReplicationStarter {
@@ -70,6 +71,17 @@
         } catch (InterruptedException e) {
           processing.writeStdErr("We are interrupted while waiting replication to complete");
         }
+        if (state.hasPersistOnlyRemotes()) {
+          processing.writeStdOut(
+              "Note: replication task(s) for persist-only remote(s) "
+                  + formatRemotes(state)
+                  + " were also written to task storage for another node to push");
+        }
+      } else if (state.hasPersistOnlyRemotes()) {
+        processing.writeStdOut(
+            "No pushes performed by this node. Replication task(s) for persist-only remote(s) "
+                + formatRemotes(state)
+                + " were written to task storage for another node to push");
       } else {
         processing.writeStdOut(
             "WARNING: No replication scheduled. No configured destination matched the requested"
@@ -79,6 +91,10 @@
     }
   }
 
+  private static String formatRemotes(ReplicationState state) {
+    return state.getPersistOnlyRemotes().stream().sorted().collect(Collectors.joining(", "));
+  }
+
   void start(
       @Nullable String urlMatch,
       String refName,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java
index fa65803..b375737 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationState.java
@@ -16,6 +16,8 @@
 
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.Table;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.Lock;
@@ -50,6 +52,7 @@
   private final Table<String, String, RefReplicationStatus> statusByProjectRef;
   private final AtomicInteger totalPushTasksCount = new AtomicInteger();
   private final AtomicInteger finishedPushTasksCount = new AtomicInteger();
+  private final Set<String> persistOnlyRemotes = ConcurrentHashMap.newKeySet();
 
   ReplicationState(PushResultProcessing processing) {
     pushResultProcessing = processing;
@@ -70,6 +73,19 @@
     return totalPushTasksCount.get() != 0;
   }
 
+  /** Records that a task was persisted for a remote that does not push (e.g. persist-only). */
+  public void addPersistOnlyRemote(String remoteName) {
+    persistOnlyRemotes.add(remoteName);
+  }
+
+  public boolean hasPersistOnlyRemotes() {
+    return !persistOnlyRemotes.isEmpty();
+  }
+
+  public Set<String> getPersistOnlyRemotes() {
+    return persistOnlyRemotes;
+  }
+
   public void notifyRefReplicated(
       String project,
       String ref,
diff --git a/src/main/resources/Documentation/cmd-start.md b/src/main/resources/Documentation/cmd-start.md
index 724728e..8676693 100644
--- a/src/main/resources/Documentation/cmd-start.md
+++ b/src/main/resources/Documentation/cmd-start.md
@@ -63,6 +63,10 @@
 pattern in command options, or the authGroup in the `replication.config`
 has no read access for the replicated projects.
 
+If the message instead says replication tasks were written to the task
+storage for another node to push, this is expected for a remote
+configured with `threads = 0` and does not indicate a problem.
+
 If one or several project patterns are supplied, only those projects
 conforming to both this/these pattern(s) and those defined in
 `replication.config` for the target host(s) are queued for replication.