Merge changes Id72def08,Id2d9c0f2

* changes:
  Load each code owner config only once for computing code owner statuses
  Add a metric to count the number of code owner config reads
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index a582589..4e10e8f 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -89,7 +89,7 @@
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final ChangedFiles changedFiles;
   private final PureRevertCache pureRevertCache;
-  private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
+  private final Provider<CodeOwnerConfigHierarchy> codeOwnerConfigHierarchyProvider;
   private final Provider<CodeOwnerResolver> codeOwnerResolver;
   private final ApprovalsUtil approvalsUtil;
   private final CodeOwnerMetrics codeOwnerMetrics;
@@ -101,7 +101,7 @@
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
       ChangedFiles changedFiles,
       PureRevertCache pureRevertCache,
-      CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
+      Provider<CodeOwnerConfigHierarchy> codeOwnerConfigHierarchyProvider,
       Provider<CodeOwnerResolver> codeOwnerResolver,
       ApprovalsUtil approvalsUtil,
       CodeOwnerMetrics codeOwnerMetrics) {
@@ -110,7 +110,7 @@
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
     this.changedFiles = changedFiles;
     this.pureRevertCache = pureRevertCache;
-    this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
+    this.codeOwnerConfigHierarchyProvider = codeOwnerConfigHierarchyProvider;
     this.codeOwnerResolver = codeOwnerResolver;
     this.approvalsUtil = approvalsUtil;
     this.codeOwnerMetrics = codeOwnerMetrics;
@@ -285,12 +285,14 @@
       FallbackCodeOwners fallbackCodeOwners =
           codeOwnersPluginConfiguration.getFallbackCodeOwners(branch.project());
 
+      CodeOwnerConfigHierarchy codeOwnerConfigHierarchy = codeOwnerConfigHierarchyProvider.get();
       return changedFiles
           .compute(changeNotes.getProjectName(), changeNotes.getCurrentPatchSet().commitId())
           .stream()
           .map(
               changedFile ->
                   getFileStatus(
+                      codeOwnerConfigHierarchy,
                       branch,
                       revision,
                       globalCodeOwners,
@@ -350,10 +352,12 @@
         return getAllPathsAsApproved(changeNotes, patchSet);
       }
 
+      CodeOwnerConfigHierarchy codeOwnerConfigHierarchy = codeOwnerConfigHierarchyProvider.get();
       return changedFiles.compute(changeNotes.getProjectName(), patchSet.commitId()).stream()
           .map(
               changedFile ->
                   getFileStatus(
+                      codeOwnerConfigHierarchy,
                       branch,
                       revision,
                       /* globalCodeOwners= */ CodeOwnerResolverResult.createEmpty(),
@@ -406,6 +410,7 @@
   }
 
   private FileCodeOwnerStatus getFileStatus(
+      CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
       BranchNameKey branch,
       ObjectId revision,
       CodeOwnerResolverResult globalCodeOwners,
@@ -426,6 +431,7 @@
               .map(
                   newPath ->
                       getPathCodeOwnerStatus(
+                          codeOwnerConfigHierarchy,
                           branch,
                           revision,
                           globalCodeOwners,
@@ -448,6 +454,7 @@
         oldPathStatus =
             Optional.of(
                 getPathCodeOwnerStatus(
+                    codeOwnerConfigHierarchy,
                     branch,
                     revision,
                     globalCodeOwners,
@@ -468,6 +475,7 @@
   }
 
   private PathCodeOwnerStatus getPathCodeOwnerStatus(
+      CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
       BranchNameKey branch,
       ObjectId revision,
       CodeOwnerResolverResult globalCodeOwners,
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
index cecd46d..6b4da27 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Optional;
@@ -44,18 +43,21 @@
  * using {@code set noparent} in the root code owner config if the {@code find-owners} backend is
  * used).
  */
-@Singleton
 public class CodeOwnerConfigHierarchy {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GitRepositoryManager repoManager;
   private final PathCodeOwners.Factory pathCodeOwnersFactory;
+  private final TransientCodeOwnerConfigCache transientCodeOwnerConfigCache;
 
   @Inject
   CodeOwnerConfigHierarchy(
-      GitRepositoryManager repoManager, PathCodeOwners.Factory pathCodeOwnersFactory) {
+      GitRepositoryManager repoManager,
+      PathCodeOwners.Factory pathCodeOwnersFactory,
+      TransientCodeOwnerConfigCache transientCodeOwnerConfigCache) {
     this.repoManager = repoManager;
     this.pathCodeOwnersFactory = pathCodeOwnersFactory;
+    this.transientCodeOwnerConfigCache = transientCodeOwnerConfigCache;
   }
 
   /**
@@ -153,7 +155,8 @@
       CodeOwnerConfig.Key codeOwnerConfigKey =
           CodeOwnerConfig.Key.create(branchNameKey, ownerConfigFolder);
       Optional<PathCodeOwners> pathCodeOwners =
-          pathCodeOwnersFactory.create(codeOwnerConfigKey, revision, absolutePath);
+          pathCodeOwnersFactory.create(
+              transientCodeOwnerConfigCache, codeOwnerConfigKey, revision, absolutePath);
       if (pathCodeOwners.isPresent()) {
         logger.atFine().log("visit code owner config for %s", ownerConfigFolder);
         boolean visitFurtherCodeOwnerConfigs = pathCodeOwnersVisitor.visit(pathCodeOwners.get());
@@ -218,7 +221,8 @@
       }
       RevCommit metaRevision = rw.parseCommit(ref.getObjectId());
       Optional<PathCodeOwners> pathCodeOwners =
-          pathCodeOwnersFactory.create(metaCodeOwnerConfigKey, metaRevision, absolutePath);
+          pathCodeOwnersFactory.create(
+              transientCodeOwnerConfigCache, metaCodeOwnerConfigKey, metaRevision, absolutePath);
       if (pathCodeOwners.isPresent()) {
         logger.atFine().log("visit code owner config %s", metaCodeOwnerConfigKey);
         pathCodeOwnersVisitor.visit(pathCodeOwners.get());
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigLoader.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigLoader.java
new file mode 100644
index 0000000..b263c20
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigLoader.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** API to load {@link CodeOwnerConfig}s. */
+public interface CodeOwnerConfigLoader {
+  /**
+   * Retrieves the code owner config for the given key from the given branch revision.
+   *
+   * @param codeOwnerConfigKey the key of the code owner config that should be retrieved
+   * @param revision the branch revision from which the code owner config should be loaded
+   * @return the code owner config for the given key if it exists, otherwise {@link
+   *     Optional#empty()}
+   */
+  public Optional<CodeOwnerConfig> get(CodeOwnerConfig.Key codeOwnerConfigKey, ObjectId revision);
+
+  /**
+   * Retrieves the code owner config for the given key from the current revision of the branch.
+   *
+   * @param codeOwnerConfigKey the key of the code owner config that should be retrieved
+   * @return the code owner config for the given key if it exists, otherwise {@link
+   *     Optional#empty()}
+   */
+  public Optional<CodeOwnerConfig> getFromCurrentRevision(CodeOwnerConfig.Key codeOwnerConfigKey);
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index a16714c..1fece0d 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -155,7 +155,8 @@
     requireNonNull(codeOwnerConfig, "codeOwnerConfig");
     requireNonNull(absolutePath, "absolutePath");
     checkState(absolutePath.isAbsolute(), "path %s must be absolute", absolutePath);
-    return resolvePathCodeOwners(pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath));
+    return resolvePathCodeOwners(
+        pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, absolutePath));
   }
 
   /**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
index 843a054..9315523 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Throwables;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.nio.file.Path;
@@ -35,39 +36,32 @@
  * that we avoid code repetition in the code owner backends.
  */
 @Singleton
-public class CodeOwners {
+public class CodeOwners implements CodeOwnerConfigLoader {
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+  private final CodeOwnerMetrics codeOwnerMetrics;
 
   @Inject
-  CodeOwners(CodeOwnersPluginConfiguration codeOwnersPluginConfiguration) {
+  CodeOwners(
+      CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+      CodeOwnerMetrics codeOwnerMetrics) {
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+    this.codeOwnerMetrics = codeOwnerMetrics;
   }
 
-  /**
-   * Retrieves the code owner config for the given key from the given branch revision.
-   *
-   * @param codeOwnerConfigKey the key of the code owner config that should be retrieved
-   * @param revision the branch revision from which the code owner config should be loaded
-   * @return the code owner config for the given key if it exists, otherwise {@link
-   *     Optional#empty()}
-   */
+  @Override
   public Optional<CodeOwnerConfig> get(CodeOwnerConfig.Key codeOwnerConfigKey, ObjectId revision) {
     requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
     requireNonNull(revision, "revision");
+    codeOwnerMetrics.countCodeOwnerConfigReads.increment();
     CodeOwnerBackend codeOwnerBackend =
         codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
     return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, revision);
   }
 
-  /**
-   * Retrieves the code owner config for the given key from the current revision of the branch.
-   *
-   * @param codeOwnerConfigKey the key of the code owner config that should be retrieved
-   * @return the code owner config for the given key if it exists, otherwise {@link
-   *     Optional#empty()}
-   */
+  @Override
   public Optional<CodeOwnerConfig> getFromCurrentRevision(CodeOwnerConfig.Key codeOwnerConfigKey) {
     requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
+    codeOwnerMetrics.countCodeOwnerConfigReads.increment();
     CodeOwnerBackend codeOwnerBackend =
         codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
     return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, /* revision= */ null);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
index 78e315a..c382801 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.metrics.Timer0;
@@ -73,11 +74,12 @@
       this.codeOwners = codeOwners;
     }
 
-    public PathCodeOwners create(CodeOwnerConfig codeOwnerConfig, Path absolutePath) {
+    public PathCodeOwners createWithoutCache(CodeOwnerConfig codeOwnerConfig, Path absolutePath) {
       requireNonNull(codeOwnerConfig, "codeOwnerConfig");
       return new PathCodeOwners(
           codeOwnerMetrics,
           projectCache,
+          /* transientCodeOwnerConfigCache= */ null,
           codeOwners,
           codeOwnerConfig,
           absolutePath,
@@ -85,14 +87,21 @@
     }
 
     public Optional<PathCodeOwners> create(
-        CodeOwnerConfig.Key codeOwnerConfigKey, ObjectId revision, Path absolutePath) {
-      return codeOwners
+        TransientCodeOwnerConfigCache transientCodeOwnerConfigCache,
+        CodeOwnerConfig.Key codeOwnerConfigKey,
+        ObjectId revision,
+        Path absolutePath) {
+      requireNonNull(transientCodeOwnerConfigCache, "transientCodeOwnerConfigCache");
+      requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
+      requireNonNull(revision, "revision");
+      return transientCodeOwnerConfigCache
           .get(codeOwnerConfigKey, revision)
           .map(
               codeOwnerConfig ->
                   new PathCodeOwners(
                       codeOwnerMetrics,
                       projectCache,
+                      transientCodeOwnerConfigCache,
                       codeOwners,
                       codeOwnerConfig,
                       absolutePath,
@@ -127,6 +136,7 @@
 
   private final CodeOwnerMetrics codeOwnerMetrics;
   private final ProjectCache projectCache;
+  private final CodeOwnerConfigLoader codeOwnerConfigLoader;
   private final CodeOwners codeOwners;
   private final CodeOwnerConfig codeOwnerConfig;
   private final Path path;
@@ -137,12 +147,15 @@
   private PathCodeOwners(
       CodeOwnerMetrics codeOwnerMetrics,
       ProjectCache projectCache,
+      @Nullable TransientCodeOwnerConfigCache transientCodeOwnerConfigCache,
       CodeOwners codeOwners,
       CodeOwnerConfig codeOwnerConfig,
       Path path,
       PathExpressionMatcher pathExpressionMatcher) {
     this.codeOwnerMetrics = requireNonNull(codeOwnerMetrics, "codeOwnerMetrics");
     this.projectCache = requireNonNull(projectCache, "projectCache");
+    this.codeOwnerConfigLoader =
+        transientCodeOwnerConfigCache != null ? transientCodeOwnerConfigCache : codeOwners;
     this.codeOwners = requireNonNull(codeOwners, "codeOwners");
     this.codeOwnerConfig = requireNonNull(codeOwnerConfig, "codeOwnerConfig");
     this.path = requireNonNull(path, "path");
@@ -356,8 +369,8 @@
 
           Optional<CodeOwnerConfig> mayBeImportedCodeOwnerConfig =
               revision.isPresent()
-                  ? codeOwners.get(keyOfImportedCodeOwnerConfig, revision.get())
-                  : codeOwners.getFromCurrentRevision(keyOfImportedCodeOwnerConfig);
+                  ? codeOwnerConfigLoader.get(keyOfImportedCodeOwnerConfig, revision.get())
+                  : codeOwnerConfigLoader.getFromCurrentRevision(keyOfImportedCodeOwnerConfig);
 
           if (!mayBeImportedCodeOwnerConfig.isPresent()) {
             unresolvedImports.add(
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/TransientCodeOwnerConfigCache.java b/java/com/google/gerrit/plugins/codeowners/backend/TransientCodeOwnerConfigCache.java
new file mode 100644
index 0000000..96e0a31
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/TransientCodeOwnerConfigCache.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Class to load and cache {@link CodeOwnerConfig}s within a request.
+ *
+ * <p>This cache is transient, which means the code owner configs stay cached only for the lifetime
+ * of the {@code TransientCodeOwnerConfigCache} instance.
+ *
+ * <p><strong>Note</strong>: This class is not thread-safe.
+ */
+public class TransientCodeOwnerConfigCache implements CodeOwnerConfigLoader {
+  private final GitRepositoryManager repoManager;
+  private final CodeOwners codeOwners;
+  private final CodeOwnerMetrics codeOwnerMetrics;
+  private final HashMap<CacheKey, Optional<CodeOwnerConfig>> cache = new HashMap<>();
+
+  @Inject
+  TransientCodeOwnerConfigCache(
+      GitRepositoryManager repoManager, CodeOwners codeOwners, CodeOwnerMetrics codeOwnerMetrics) {
+    this.repoManager = repoManager;
+    this.codeOwners = codeOwners;
+    this.codeOwnerMetrics = codeOwnerMetrics;
+  }
+
+  /**
+   * Gets the specified code owner config from the cache, if it was previously retrieved. Otherwise
+   * loads and returns the code owner config.
+   */
+  @Override
+  public Optional<CodeOwnerConfig> get(
+      CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) {
+    CacheKey cacheKey = CacheKey.create(codeOwnerConfigKey, revision);
+    Optional<CodeOwnerConfig> cachedCodeOwnerConfig = cache.get(cacheKey);
+    if (cachedCodeOwnerConfig != null) {
+      codeOwnerMetrics.countCodeOwnerConfigCacheReads.increment();
+      return cachedCodeOwnerConfig;
+    }
+    return loadAndCache(cacheKey);
+  }
+
+  /**
+   * Gets the specified code owner config from the cache, if it was previously retrieved. Otherwise
+   * loads and returns the code owner config.
+   */
+  @Override
+  public Optional<CodeOwnerConfig> getFromCurrentRevision(CodeOwnerConfig.Key codeOwnerConfigKey) {
+    return get(codeOwnerConfigKey, /* revision= */ null);
+  }
+
+  /** Load a code owner config and puts it into the cache. */
+  private Optional<CodeOwnerConfig> loadAndCache(CacheKey cacheKey) {
+    Optional<CodeOwnerConfig> codeOwnerConfig;
+    if (cacheKey.revision().isPresent()) {
+      codeOwnerConfig = codeOwners.get(cacheKey.codeOwnerConfigKey(), cacheKey.revision().get());
+    } else {
+      Optional<ObjectId> revision = getRevision(cacheKey.codeOwnerConfigKey().branchNameKey());
+      if (revision.isPresent()) {
+        codeOwnerConfig = codeOwners.get(cacheKey.codeOwnerConfigKey(), revision.get());
+      } else {
+        // branch does not exists, hence the code owner config also doesn't exist
+        codeOwnerConfig = Optional.empty();
+      }
+    }
+    cache.put(cacheKey, codeOwnerConfig);
+    return codeOwnerConfig;
+  }
+
+  /**
+   * Gets the revision for the given branch.
+   *
+   * <p>Returns {@link Optional#empty()} if the branch doesn't exist.
+   */
+  private Optional<ObjectId> getRevision(BranchNameKey branchNameKey) {
+    try (Repository repo = repoManager.openRepository(branchNameKey.project())) {
+      Ref ref = repo.exactRef(branchNameKey.branch());
+      if (ref == null) {
+        // branch does not exist
+        return Optional.empty();
+      }
+      return Optional.of(ref.getObjectId());
+    } catch (IOException e) {
+      throw new CodeOwnersInternalServerErrorException(
+          String.format(
+              "failed to get revision of branch %s in project %s",
+              branchNameKey.shortName(), branchNameKey.project()),
+          e);
+    }
+  }
+
+  @AutoValue
+  abstract static class CacheKey {
+    /** The key of the code owner config. */
+    public abstract CodeOwnerConfig.Key codeOwnerConfigKey();
+
+    /** The revision from which the code owner config was loaded. */
+    public abstract Optional<ObjectId> revision();
+
+    public static CacheKey create(
+        CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) {
+      return new AutoValue_TransientCodeOwnerConfigCache_CacheKey(
+          codeOwnerConfigKey, Optional.ofNullable(revision));
+    }
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
index be681e6..e5a3c58 100644
--- a/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
+++ b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.codeowners.metrics;
 
+import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
@@ -24,6 +25,7 @@
 /** Metrics of the code-owners plugin. */
 @Singleton
 public class CodeOwnerMetrics {
+  // latency metrics
   public final Timer0 computeChangedFiles;
   public final Timer0 computeFileStatus;
   public final Timer0 computeFileStatuses;
@@ -36,12 +38,17 @@
   public final Timer0 resolvePathCodeOwners;
   public final Timer0 runCodeOwnerSubmitRule;
 
+  // counter metrics
+  public final Counter0 countCodeOwnerConfigReads;
+  public final Counter0 countCodeOwnerConfigCacheReads;
+
   private final MetricMaker metricMaker;
 
   @Inject
   CodeOwnerMetrics(MetricMaker metricMaker) {
     this.metricMaker = metricMaker;
 
+    // latency metrics
     this.computeChangedFiles =
         createLatencyTimer("compute_changed_files", "Latency for computing changed files");
     this.computeFileStatus =
@@ -79,6 +86,16 @@
     this.runCodeOwnerSubmitRule =
         createLatencyTimer(
             "run_code_owner_submit_rule", "Latency for running the code owner submit rule");
+
+    // counter metrics
+    this.countCodeOwnerConfigReads =
+        createCounter(
+            "count_code_owner_config_reads",
+            "Total number of code owner config reads from backend");
+    this.countCodeOwnerConfigCacheReads =
+        createCounter(
+            "count_code_owner_config_cache_reads",
+            "Total number of code owner config reads from cache");
   }
 
   private Timer0 createLatencyTimer(String name, String description) {
@@ -86,4 +103,8 @@
         "code_owners/" + name,
         new Description(description).setCumulative().setUnit(Units.MILLISECONDS));
   }
+
+  private Counter0 createCounter(String name, String description) {
+    return metricMaker.newCounter("code_owners/" + name, new Description(description).setRate());
+  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
index d01d0fc..d326da6 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -145,7 +145,9 @@
               String.format(
                   "checking code owner config file %s", codeOwnerConfig.key().format(codeOwners)));
           OptionalResultWithMessages<PathCodeOwnersResult> pathCodeOwnersResult =
-              pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath).resolveCodeOwnerConfig();
+              pathCodeOwnersFactory
+                  .createWithoutCache(codeOwnerConfig, absolutePath)
+                  .resolveCodeOwnerConfig();
           messages.addAll(pathCodeOwnersResult.messages());
           pathCodeOwnersResult
               .get()
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
index 5490e2e..8929a72 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
@@ -41,6 +41,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.Inject;
 import com.google.inject.Key;
+import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -58,6 +59,7 @@
   private CodeOwnerConfigOperations codeOwnerConfigOperations;
   private PathCodeOwners.Factory pathCodeOwnersFactory;
   private DynamicMap<CodeOwnerBackend> codeOwnerBackends;
+  private Provider<TransientCodeOwnerConfigCache> transientCodeOwnerConfigCacheProvider;
 
   @Before
   public void setUpCodeOwnersPlugin() throws Exception {
@@ -66,12 +68,14 @@
     pathCodeOwnersFactory = plugin.getSysInjector().getInstance(PathCodeOwners.Factory.class);
     codeOwnerBackends =
         plugin.getSysInjector().getInstance(new Key<DynamicMap<CodeOwnerBackend>>() {});
+    transientCodeOwnerConfigCacheProvider =
+        plugin.getSysInjector().getInstance(new Key<Provider<TransientCodeOwnerConfigCache>>() {});
   }
 
   @Test
   public void createPathCodeOwnersForCodeOwnerConfig() throws Exception {
     PathCodeOwners pathCodeOwners =
-        pathCodeOwnersFactory.create(
+        pathCodeOwnersFactory.createWithoutCache(
             createCodeOwnerBuilder().build(), Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners).isNotNull();
   }
@@ -82,7 +86,7 @@
         assertThrows(
             NullPointerException.class,
             () ->
-                pathCodeOwnersFactory.create(
+                pathCodeOwnersFactory.createWithoutCache(
                     /* codeOwnerConfig= */ null, Paths.get("/foo/bar/baz.md")));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfig");
   }
@@ -93,7 +97,9 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> pathCodeOwnersFactory.create(codeOwnerConfig, /* absolutePath= */ null));
+            () ->
+                pathCodeOwnersFactory.createWithoutCache(
+                    codeOwnerConfig, /* absolutePath= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("path");
   }
 
@@ -104,7 +110,8 @@
     IllegalStateException exception =
         assertThrows(
             IllegalStateException.class,
-            () -> pathCodeOwnersFactory.create(codeOwnerConfig, Paths.get(relativePath)));
+            () ->
+                pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Paths.get(relativePath)));
     assertThat(exception)
         .hasMessageThat()
         .isEqualTo(String.format("path %s must be absolute", relativePath));
@@ -123,6 +130,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             codeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
@@ -130,12 +138,28 @@
   }
 
   @Test
+  public void cannotCreatePathCodeOwnersForNullCache() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                pathCodeOwnersFactory.create(
+                    /* transientCodeOwnerConfigCache= */ null,
+                    CodeOwnerConfig.Key.create(
+                        BranchNameKey.create(project, "master"), Paths.get("/")),
+                    projectOperations.project(project).getHead("master"),
+                    Paths.get("/foo/bar/baz.md")));
+    assertThat(npe).hasMessageThat().isEqualTo("transientCodeOwnerConfigCache");
+  }
+
+  @Test
   public void cannotCreatePathCodeOwnersForNullCodeOwnerConfigKey() throws Exception {
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
             () ->
                 pathCodeOwnersFactory.create(
+                    transientCodeOwnerConfigCacheProvider.get(),
                     /* codeOwnerConfigKey= */ null,
                     projectOperations.project(project).getHead("master"),
                     Paths.get("/foo/bar/baz.md")));
@@ -149,6 +173,7 @@
             NullPointerException.class,
             () ->
                 pathCodeOwnersFactory.create(
+                    transientCodeOwnerConfigCacheProvider.get(),
                     CodeOwnerConfig.Key.create(
                         BranchNameKey.create(project, "master"), Paths.get("/")),
                     /* revision= */ null,
@@ -172,6 +197,7 @@
             NullPointerException.class,
             () ->
                 pathCodeOwnersFactory.create(
+                    transientCodeOwnerConfigCacheProvider.get(),
                     codeOwnerConfigKey,
                     projectOperations.project(project).getHead("master"),
                     /* absolutePath= */ null));
@@ -195,6 +221,7 @@
             IllegalStateException.class,
             () ->
                 pathCodeOwnersFactory.create(
+                    transientCodeOwnerConfigCacheProvider.get(),
                     codeOwnerConfigKey,
                     projectOperations.project(project).getHead("master"),
                     Paths.get(relativePath)));
@@ -207,7 +234,8 @@
   public void getEmptyPathCodeOwners() throws Exception {
     CodeOwnerConfig emptyCodeOwnerConfig = createCodeOwnerBuilder().build();
     PathCodeOwners pathCodeOwners =
-        pathCodeOwnersFactory.create(emptyCodeOwnerConfig, Paths.get("/foo/bar/baz.md"));
+        pathCodeOwnersFactory.createWithoutCache(
+            emptyCodeOwnerConfig, Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners()).isEmpty();
     assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().hasUnresolvedImports()).isFalse();
   }
@@ -219,7 +247,7 @@
             .addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email(), user.email()))
             .build();
     PathCodeOwners pathCodeOwners =
-        pathCodeOwnersFactory.create(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
+        pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners())
         .comparingElementsUsing(hasEmail())
         .containsExactly(admin.email(), user.email());
@@ -257,7 +285,7 @@
               .addCodeOwnerSet(nonMatchingCodeOwnerSet)
               .build();
       PathCodeOwners pathCodeOwners =
-          pathCodeOwnersFactory.create(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
+          pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
       assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners())
           .comparingElementsUsing(hasEmail())
           .containsExactly(admin.email(), user.email());
@@ -278,7 +306,7 @@
                       .build())
               .build();
       PathCodeOwners pathCodeOwners =
-          pathCodeOwnersFactory.create(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
+          pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
       assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners()).isEmpty();
     }
   }
@@ -310,7 +338,7 @@
               .addCodeOwnerSet(globalCodeOwnerSet)
               .build();
       PathCodeOwners pathCodeOwners =
-          pathCodeOwnersFactory.create(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
+          pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
       assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners())
           .comparingElementsUsing(hasEmail())
           .containsExactly(admin.email());
@@ -344,7 +372,7 @@
               .addCodeOwnerSet(globalCodeOwnerSet)
               .build();
       PathCodeOwners pathCodeOwners =
-          pathCodeOwnersFactory.create(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
+          pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
       assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners())
           .comparingElementsUsing(hasEmail())
           .containsExactly(admin.email(), user.email());
@@ -379,7 +407,7 @@
               .addCodeOwnerSet(perFileCodeOwnerSet2)
               .build();
       PathCodeOwners pathCodeOwners =
-          pathCodeOwnersFactory.create(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
+          pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, Paths.get("/foo/bar/baz.md"));
       assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners())
           .comparingElementsUsing(hasEmail())
           .containsExactly(admin.email(), user.email());
@@ -390,7 +418,7 @@
   public void checkThatParentCodeOwnersAreIgnoredIfCodeOwnerConfigIgnoresParentCodeOwners()
       throws Exception {
     PathCodeOwners pathCodeOwners =
-        pathCodeOwnersFactory.create(
+        pathCodeOwnersFactory.createWithoutCache(
             createCodeOwnerBuilder().setIgnoreParentCodeOwners().build(),
             Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().ignoreParentCodeOwners()).isTrue();
@@ -400,7 +428,7 @@
   public void checkThatParentCodeOwnersAreNotIgnoredIfCodeOwnerConfigDoesNotIgnoreParentCodeOwners()
       throws Exception {
     PathCodeOwners pathCodeOwners =
-        pathCodeOwnersFactory.create(
+        pathCodeOwnersFactory.createWithoutCache(
             createCodeOwnerBuilder().setIgnoreParentCodeOwners(false).build(),
             Paths.get("/foo/bar/baz.md"));
     assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().ignoreParentCodeOwners()).isFalse();
@@ -410,7 +438,7 @@
   public void checkThatParentCodeOwnersAreIgnoredIfMatchingCodeOwnerSetIgnoresParentCodeOwners()
       throws Exception {
     PathCodeOwners pathCodeOwners =
-        pathCodeOwnersFactory.create(
+        pathCodeOwnersFactory.createWithoutCache(
             createCodeOwnerBuilder()
                 .addCodeOwnerSet(
                     CodeOwnerSet.builder()
@@ -427,7 +455,7 @@
       checkThatParentCodeOwnersAreNotIgnoredIfNonMatchingCodeOwnerSetIgnoresParentCodeOwners()
           throws Exception {
     PathCodeOwners pathCodeOwners =
-        pathCodeOwnersFactory.create(
+        pathCodeOwnersFactory.createWithoutCache(
             createCodeOwnerBuilder()
                 .addCodeOwnerSet(
                     CodeOwnerSet.builder()
@@ -456,6 +484,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -502,6 +531,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -549,6 +579,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -596,6 +627,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -645,6 +677,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -693,6 +726,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -745,6 +779,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -796,6 +831,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -839,6 +875,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -877,6 +914,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -936,6 +974,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -995,6 +1034,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -1048,6 +1088,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -1086,6 +1127,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -1134,7 +1176,11 @@
         .update();
 
     Optional<PathCodeOwners> pathCodeOwners =
-        pathCodeOwnersFactory.create(rootCodeOwnerConfigKey, oldRevision, Paths.get("/foo.md"));
+        pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
+            rootCodeOwnerConfigKey,
+            oldRevision,
+            Paths.get("/foo.md"));
     assertThat(pathCodeOwners).isPresent();
 
     // Expectation: we get the global owners from the importing and the imported code owner config
@@ -1169,6 +1215,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
@@ -1200,6 +1247,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
@@ -1243,6 +1291,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
@@ -1276,6 +1325,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
@@ -1317,6 +1367,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
@@ -1366,6 +1417,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead(branchName),
             Paths.get("/foo.md"));
@@ -1411,6 +1463,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
@@ -1458,6 +1511,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo/bar/baz.md"));
@@ -1493,6 +1547,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -1545,6 +1600,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             importingCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -1608,6 +1664,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -1668,6 +1725,7 @@
 
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -1743,6 +1801,7 @@
     // *.txt
     Optional<PathCodeOwners> pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.xyz"));
@@ -1752,6 +1811,7 @@
     // Expectation for foo.md file: code owners contains only user since foo.md only matches *.md
     pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.md"));
@@ -1764,6 +1824,7 @@
     // *.txt
     pathCodeOwners =
         pathCodeOwnersFactory.create(
+            transientCodeOwnerConfigCacheProvider.get(),
             rootCodeOwnerConfigKey,
             projectOperations.project(project).getHead("master"),
             Paths.get("/foo.txt"));
diff --git a/resources/Documentation/metrics.md b/resources/Documentation/metrics.md
index 55bb736..d236b3e 100644
--- a/resources/Documentation/metrics.md
+++ b/resources/Documentation/metrics.md
@@ -28,6 +28,13 @@
 * `run_code_owner_submit_rule`:
   Latency for running the code owner submit rule.
 
+## <a id="counterMetrics"> Counter Metrics
+
+* `count_code_owner_config_reads`:
+  Total number of code owner config reads from backend.
+* `count_code_owner_config_cache_reads`:
+  Total number of code owner config reads from cache.
+
 ---
 
 Back to [@PLUGIN@ documentation index](index.html)