Poll the filesystem for new events

In a multi-primary environment events may be created and written
to the filestore from any primary.  Without a notification system,
events written by other servers might only be seen by the current
server when the current server creates a new event.  If the traffic
on the current server is low, this delay may be significant and
unacceptable.

In order to get events from other primaries output in a timely manner,
poll the filesystem to recongnize when new events may have been
written to the filestore and output them. This makes events timely
in a multi-primary environment without any notification system, and
makes it more robustly timely in systems with notifications.

Change-Id: Ic474f3ca639cc839b555f1ee82c97cc3e5882a89
diff --git a/src/main/java/com/googlesource/gerrit/plugins/events/FileSystemEventBroker.java b/src/main/java/com/googlesource/gerrit/plugins/events/FileSystemEventBroker.java
index ef73a36..f3f5855 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/events/FileSystemEventBroker.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/events/FileSystemEventBroker.java
@@ -125,7 +125,7 @@
     }
   }
 
-  protected synchronized void sendAllPendingEvents() throws PermissionBackendException {
+  public synchronized void sendAllPendingEvents() throws PermissionBackendException {
     try {
       long current = store.getHead();
       while (lastSent < current) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/events/Module.java b/src/main/java/com/googlesource/gerrit/plugins/events/Module.java
index c0e1271..7774a38 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/events/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/events/Module.java
@@ -14,17 +14,38 @@
 
 package com.googlesource.gerrit.plugins.events;
 
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.events.EventDispatcher;
-import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.events.fsstore.FsListener.FsLifecycleListener;
 import com.googlesource.gerrit.plugins.events.fsstore.FsStore;
 
-public class Module extends AbstractModule {
+public class Module extends LifecycleModule {
+  private static final int DEFAULT_POLLING_INTERVAL = 0;
+
+  @Provides
+  @Singleton
+  @PollingInterval
+  protected Long getCleanupInterval(PluginConfigFactory cfg, @PluginName String pluginName) {
+    String fromConfig =
+        Strings.nullToEmpty(cfg.getFromGerritConfig(pluginName).getString("pollingInterval"));
+    return SECONDS.toMillis(ConfigUtil.getTimeUnit(fromConfig, DEFAULT_POLLING_INTERVAL, SECONDS));
+  }
+
   @Override
   protected void configure() {
     DynamicSet.setOf(binder(), StreamEventListener.class);
     bind(EventStore.class).to(FsStore.class);
     DynamicItem.bind(binder(), EventDispatcher.class).to(FileSystemEventBroker.class);
+    listener().to(FsLifecycleListener.class);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/events/PollingInterval.java b/src/main/java/com/googlesource/gerrit/plugins/events/PollingInterval.java
new file mode 100644
index 0000000..bf05885
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/events/PollingInterval.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2017 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.events;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface PollingInterval {}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/events/fsstore/FsListener.java b/src/main/java/com/googlesource/gerrit/plugins/events/fsstore/FsListener.java
new file mode 100644
index 0000000..7ed0c1b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/events/fsstore/FsListener.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2017 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.events.fsstore;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.events.EventDispatcher;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.events.FileSystemEventBroker;
+import com.googlesource.gerrit.plugins.events.PollingInterval;
+import java.util.concurrent.ScheduledFuture;
+
+@Singleton
+public class FsListener implements Runnable {
+  public static class FsLifecycleListener implements LifecycleListener {
+    protected final WorkQueue queue;
+    protected final long pollingInterval;
+    protected final FileSystemEventBroker broker;
+    protected ScheduledFuture<?> future;
+
+    @Inject
+    protected FsLifecycleListener(
+        WorkQueue queue, @PollingInterval long pollingInterval, EventDispatcher dispatcher) {
+      this.queue = queue;
+      this.pollingInterval = pollingInterval;
+      this.broker = (FileSystemEventBroker) dispatcher;
+    }
+
+    @Override
+    public void start() {
+      if (pollingInterval > 0) {
+        ScheduledFuture<?> future =
+            queue
+                .getDefaultQueue()
+                .scheduleAtFixedRate(
+                    new FsListener(broker), pollingInterval, pollingInterval, MILLISECONDS);
+      }
+    }
+
+    @Override
+    public void stop() {
+      if (future != null) {
+        future.cancel(true);
+      }
+    }
+  }
+
+  protected final FileSystemEventBroker broker;
+
+  @Inject
+  protected FsListener(FileSystemEventBroker broker) {
+    this.broker = broker;
+  }
+
+  @Override
+  public void run() {
+    try {
+      broker.sendAllPendingEvents();
+    } catch (PermissionBackendException e) {
+      // Ignore
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "Events FS Polling Listener";
+  }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..2fef2db
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,40 @@
+@PLUGIN@ Configuration
+======================
+
+In a multi-primary environment events may be created and written
+to the filestore from any primary.  Without a notification system,
+events written by other servers might only be seen by the current
+server when the current server creates a new event.  If the traffic
+on the current server is low, this delay may be significant and
+unacceptable.
+
+In order to get events from other primaries output in a timely manner,
+the @PLUGIN@ plugin can be configured to poll and recongnize when
+new events may have been written to the filestore and output them.
+
+Reload the plugin on each primary for the changes to take effect.
+
+The polling frequency can be specified in the configuration.
+For example:
+
+```
+  [plugin "@PLUGIN@"]
+    pollingInterval = 3s
+```
+
+causes polling to be done every 3 seconds.
+
+Values should use common time unit suffixes to express their setting:
+
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+* d, day, days
+* w, week, weeks (`1 week` is treated as `7 days`)
+* mon, month, months (`1 month` is treated as `30 days`)
+* y, year, years (`1 year` is treated as `365 days`)
+
+If a time unit suffix is not specified, `seconds` is assumed.
+
+If 'pollingInterval' is not present in the configuration, polling
+will not be enabled.