Schedule groups-backends in a queue

When loading groups from an auth backend (for example LDAP), Gerrit is
subject to its latency. This might be a problem because a slow backend,
or a very large number of groups, or both, might cause the warm-cache
command to last longer than the allowed timeouts imposed by Gerrit (or a
load balancer that fronts it).

Schedule the loading of the cache asynchronously over a new dedicated
queue named: "Groups-Backend-Cache-Warmer".

This allows to terminate the SSH command in a timely manner, while
loading the cache in the background.

The status of the cache loading can be observed via the `show-queue`
command as well as the `queue/groups_backend_cache_warmer/*` metrics.

Currently the size of the queue is not configurable and it is set to 8
threads.

Change-Id: Iccbb1f05bcf04c540a68e1d70cd6be9078a50815
diff --git a/admin/warm-cache-1.0.groovy b/admin/warm-cache-1.0.groovy
index 88a8148..c3c4892 100644
--- a/admin/warm-cache-1.0.groovy
+++ b/admin/warm-cache-1.0.groovy
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import com.google.common.flogger.FluentLogger
 import com.google.gerrit.common.data.GlobalCapability
 import com.google.gerrit.sshd.*
 import com.google.gerrit.extensions.annotations.*
@@ -20,9 +21,14 @@
 import com.google.gerrit.server.IdentifiedUser
 import com.google.gerrit.reviewdb.client.AccountGroup
 import com.google.inject.*
-import org.kohsuke.args4j.*
+import com.google.gerrit.server.git.WorkQueue
+import org.apache.sshd.server.Environment
+
+import java.util.concurrent.ExecutorService
 
 abstract class BaseSshCommand extends SshCommand {
+  protected static final FluentLogger logger = FluentLogger.forEnclosingClass()
+
 
   void println(String msg) {
     stdout.println msg
@@ -142,32 +148,70 @@
 @Export("groups-backends")
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 class WarmGroupsBackendsCache extends WarmAccountsCache {
+  private static final THREAD_POOL_SIZE = 8
+  private static final QUEUE_NAME = "Groups-Backend-Cache-Warmer"
 
   @Inject
   IdentifiedUser.GenericFactory userFactory
 
-  public void run() {
-    println "Loading groups ..."
+  @Inject WorkQueue queues
+
+  private static class GroupsBackendsTask implements Runnable {
+    IdentifiedUser user
+
+    GroupsBackendsTask(IdentifiedUser identifiedUser) {
+      user = identifiedUser
+    }
+
+    @Override
+    void run() {
+      def threadStart = System.currentTimeMillis()
+      def groupsUUIDs = user.getEffectiveGroups()?.getKnownGroups()
+      def threadElapsed = (System.currentTimeMillis() - threadStart)
+      logger.atInfo().log("Loaded %d groups for account %d in %s millis", groupsUUIDs.size(), user.getAccountId().get(), threadElapsed)
+    }
+
+    @Override
+    String toString() {
+      return "Warmup backend groups [accountId: ${user.getAccountId().get()}]"
+    }
+  }
+
+  ExecutorService executorService
+
+  private ExecutorService executor() {
+    def existingExecutor = queues.getExecutor(QUEUE_NAME)
+    if(existingExecutor != null) {
+      return existingExecutor
+    }
+    return queues.createQueue(THREAD_POOL_SIZE, QUEUE_NAME, true);
+  }
+
+  @Override
+  void start(Environment env) throws IOException {
+    super.start(env)
+    executorService = executor()
+  }
+
+  void run() {
+    println "Scheduling backend groups loading ..."
     def start = System.currentTimeMillis()
 
-    def loaded = 0
-    def allGroupsUUIDs = new HashSet<AccountGroup.UUID>()
+    def scheduled = 0
     def lastDisplay = 0
 
     for (accountId in accounts.allIds()) {
-      def user = userFactory.create(accountId)
-      def groupsUUIDs = user?.getEffectiveGroups()?.getKnownGroups()
-      if (groupsUUIDs != null) { allGroupsUUIDs.addAll(groupsUUIDs) }
+      scheduled++
+      executorService.submit(new GroupsBackendsTask(userFactory.create(accountId)))
 
-      loaded = allGroupsUUIDs.size()
-      if (loaded.intdiv(1000) > lastDisplay) {
-        println "$loaded groups"
-        lastDisplay = loaded.intdiv(1000)
+      if (scheduled.intdiv(1000) > lastDisplay) {
+        println "Scheduled loading of groups for $scheduled accounts"
+        lastDisplay = scheduled.intdiv(1000)
       }
     }
 
-    def elapsed = (System.currentTimeMillis()-start)/1000
-    println "$loaded groups loaded in $elapsed secs"
+    def elapsed = (System.currentTimeMillis() - start) / 1000
+    println "Scheduled loading of groups for $scheduled accounts in $elapsed secs"
   }
 }