Introduce auth.externalIdsRefExpiry

When reading the `refs/meta/external-ids` ref, Gerrit opens the
`All-Users` repository over an over again just to extract the tip of the
external-ids ref.

This can be really expensive in response to queries that return loads of
changes along with their account identities.

Allow to mitigate this by providing a memoization mechanism that allows
to use the latest read external-ids sha1 for a configurable period of
time, rather than opening the repository an excessive number of times.

Release-Notes: Introduce auth.externalIdsRefExpiry
Change-Id: Id08282c88f3b6b216bbcbdcd558590f97767090a
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 952496d..02643b9 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -535,6 +535,22 @@
 +
 By default, `false`.
 
+[[auth.externalIdsRefExpiry]]auth.externalIdsRefExpiry::
++
+Define TTL value for memoizing the refs/meta/external-ids sha1 rather than opening
+the All-Users repo. This allows faster external-ids refs lookup.
++
+Values should use common unit suffixes to express their setting:
++
+* s, sec, second, seconds
+* m, min, minute, minutes
++
++ Setting this to `0` disables memoization.
++
+**Note** during the memoization time exterenal-ids modification are not visible by Gerrit.
++
+By default `0`.
+
 [[auth.emailFormat]]auth.emailFormat::
 +
 Optional format string to construct user email addresses out of
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
index dbaed04..cf6f010 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.server.account.externalids.storage.notedb;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.metrics.Description;
@@ -30,6 +32,8 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -54,6 +58,13 @@
  */
 @Singleton
 public class ExternalIdReader {
+
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  /** Defined only for handling the case when externalIdsRefExpirySecs is zero */
+  private static final Supplier<ObjectId> UNUSED_OBJECT_ID_SUPPLIER =
+      Suppliers.ofInstance(ObjectId.zeroId());
+
   public static ObjectId readRevision(Repository repo) throws IOException {
     Ref ref = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
     return ref != null ? ref.getObjectId() : ObjectId.zeroId();
@@ -73,6 +84,8 @@
   private final Timer0 readSingleLatency;
   private final ExternalIdFactoryNoteDbImpl externalIdFactory;
   private final AuthConfig authConfig;
+  private final Supplier<ObjectId> allUsersSupplier;
+  private final int externalIdsRefExpirySecs;
 
   @VisibleForTesting
   @Inject
@@ -98,6 +111,22 @@
                 .setUnit(Units.MILLISECONDS));
     this.externalIdFactory = externalIdFactory;
     this.authConfig = authConfig;
+    this.externalIdsRefExpirySecs = authConfig.getExternalIdsRefExpirySecs();
+    this.allUsersSupplier =
+        externalIdsRefExpirySecs > 0
+            ? Suppliers.memoizeWithExpiration(
+                () -> {
+                  try {
+                    logger.atFine().log("Refreshing external-ids revision from All-Users repo");
+                    return readRevision(repoManager, allUsersName);
+                  } catch (IOException e) {
+                    throw new IllegalStateException(
+                        "Couldn't refresh external-ids from All-Users repo", e);
+                  }
+                },
+                externalIdsRefExpirySecs,
+                TimeUnit.SECONDS)
+            : UNUSED_OBJECT_ID_SUPPLIER;
   }
 
   @VisibleForTesting
@@ -112,6 +141,13 @@
   }
 
   public ObjectId readRevision() throws IOException {
+    return externalIdsRefExpirySecs > 0
+        ? allUsersSupplier.get()
+        : readRevision(repoManager, allUsersName);
+  }
+
+  private static ObjectId readRevision(GitRepositoryManager repoManager, AllUsersName allUsersName)
+      throws IOException {
     try (Repository repo = repoManager.openRepository(allUsersName)) {
       return readRevision(repo);
     }
diff --git a/java/com/google/gerrit/server/config/AuthConfig.java b/java/com/google/gerrit/server/config/AuthConfig.java
index b6ffcee..2d118a6 100644
--- a/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/java/com/google/gerrit/server/config/AuthConfig.java
@@ -66,6 +66,7 @@
   private final boolean allowRegisterNewEmail;
   private final boolean userNameCaseInsensitive;
   private final boolean userNameCaseInsensitiveMigrationMode;
+  private final int externalIdsRefExpirySecs;
   private GitBasicAuthPolicy gitBasicAuthPolicy;
 
   @Inject
@@ -100,6 +101,9 @@
     userNameCaseInsensitive = cfg.getBoolean("auth", "userNameCaseInsensitive", false);
     userNameCaseInsensitiveMigrationMode =
         cfg.getBoolean("auth", "userNameCaseInsensitiveMigrationMode", false);
+    externalIdsRefExpirySecs =
+        (int)
+            ConfigUtil.getTimeUnit(cfg, "auth", null, "externalIdsRefExpiry", 0, TimeUnit.SECONDS);
 
     if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP
         && authType != AuthType.LDAP
@@ -218,6 +222,10 @@
     return cookieSecure;
   }
 
+  public int getExternalIdsRefExpirySecs() {
+    return externalIdsRefExpirySecs;
+  }
+
   public SignedToken getEmailRegistrationToken() {
     return emailReg;
   }