Cache the OWNERS file's parsed content
The OWNERS file is rarely modified however it is being read and parsed
for every single change that gets evaluated. Introduce the cache (called
`owners-path_owners_entries`) that holds the PathOwnersEntry value for a
project, branch and path (typically `refs/meta/config`, development
branch and path). The path is especially important for projects with
deep directories structure (mono-repos) and cache size should be
carefully adjusted to adhere to it (1 dir level = 1 cache entry).
It gets invalidated when project's ref is updated.
As a result it reduces 2-3 times the configuration load latency which is
reflected in performance of prolog and submit requirements.
There are several constraints that influenced the implementation
* the `PathOwners` is a common class used by both owners and
owners-autoassign plugins
* the `PathOwners`, in the owners plugin prolog's engine, is
instantiated by the constructor call and injection is not possible
there
The config.md was updated with the cache configuration.
Bug: Issue 16830
Change-Id: Ic7d61de07f3ec5169f0cf7c4ae8266351543e108
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java
index 80a03e0..5e6ad99 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java
@@ -49,6 +49,7 @@
@Override
protected void configure() {
+ install(PathOwnersEntriesCache.module());
bind(ReviewerManager.class)
.to(config.isAsyncReviewers() ? AsyncReviewerManager.class : SyncReviewerManager.class);
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(GitRefListener.class);
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
index faf5e42..d6fd574 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
@@ -89,6 +89,8 @@
private final AutoAssignConfig cfg;
+ private final PathOwnersEntriesCache cache;
+
@Inject
public GitRefListener(
GerritApi api,
@@ -100,7 +102,8 @@
OneOffRequestContext oneOffReqCtx,
Provider<CurrentUser> currentUserProvider,
ChangeNotes.Factory notesFactory,
- AutoAssignConfig cfg) {
+ AutoAssignConfig cfg,
+ PathOwnersEntriesCache cache) {
this.api = api;
this.patchListCache = patchListCache;
this.projectCache = projectCache;
@@ -111,6 +114,7 @@
this.currentUserProvider = currentUserProvider;
this.notesFactory = notesFactory;
this.cfg = cfg;
+ this.cache = cache;
}
@Override
@@ -235,7 +239,9 @@
parentProjectsNameKeys,
cfg.isBranchDisabled(change.branch) ? Optional.empty() : Optional.of(change.branch),
patchList,
- cfg.expandGroups());
+ cfg.expandGroups(),
+ projectNameKey.get(),
+ cache);
Set<Account.Id> allReviewers = Sets.newHashSet();
allReviewers.addAll(owners.get().values());
allReviewers.addAll(owners.getReviewers().values());
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerIT.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerIT.java
index e5be7b5..cb34173 100644
--- a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerIT.java
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerIT.java
@@ -37,12 +37,9 @@
import com.google.inject.Inject;
import com.google.inject.Module;
import com.googlesource.gerrit.owners.api.OwnersApiModule;
-import com.googlesource.gerrit.owners.common.AutoassignConfigModule;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.StreamSupport;
-import com.googlesource.gerrit.owners.common.ReviewerManager;
-
import org.eclipse.jgit.transport.ReceiveCommand.Type;
import org.junit.Test;
@@ -66,6 +63,7 @@
public static class TestModule extends AbstractModule {
@Override
protected void configure() {
+ install(PathOwnersEntriesCache.module());
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(GitRefListenerTest.class);
install(new AutoassignConfigModule());
}
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerTest.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerTest.java
index 9cc6e96..b651572 100644
--- a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerTest.java
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerTest.java
@@ -27,10 +27,6 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import com.googlesource.gerrit.owners.common.Accounts;
-import com.googlesource.gerrit.owners.common.AutoAssignConfig;
-import com.googlesource.gerrit.owners.common.GitRefListener;
-import com.googlesource.gerrit.owners.common.ReviewerManager;
import org.eclipse.jgit.lib.Repository;
import org.junit.Ignore;
@@ -50,7 +46,8 @@
OneOffRequestContext oneOffReqCtx,
Provider<CurrentUser> currentUserProvider,
ChangeNotes.Factory notesFactory,
- AutoAssignConfig cfg) {
+ AutoAssignConfig cfg,
+ PathOwnersEntriesCache cache) {
super(
api,
patchListCache,
@@ -61,7 +58,8 @@
oneOffReqCtx,
currentUserProvider,
notesFactory,
- cfg);
+ cfg,
+ cache);
}
@Override
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java
index 8269ad6..df398e3 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java
@@ -43,6 +43,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
@@ -99,7 +100,9 @@
List<Project.NameKey> parentProjectsNames,
Optional<String> branchWhenEnabled,
Map<String, FileDiffOutput> fileDiffMap,
- boolean expandGroups) {
+ boolean expandGroups,
+ String project,
+ PathOwnersEntriesCache cache) {
this(
accounts,
repositoryManager,
@@ -107,7 +110,9 @@
parentProjectsNames,
branchWhenEnabled,
getModifiedPaths(fileDiffMap),
- expandGroups);
+ expandGroups,
+ project,
+ cache);
}
public PathOwners(
@@ -117,7 +122,9 @@
List<Project.NameKey> parentProjectsNames,
Optional<String> branchWhenEnabled,
DiffSummary diffSummary,
- boolean expandGroups) {
+ boolean expandGroups,
+ String project,
+ PathOwnersEntriesCache cache) {
this(
accounts,
repositoryManager,
@@ -125,7 +132,9 @@
parentProjectsNames,
branchWhenEnabled,
ImmutableSet.copyOf(diffSummary.getPaths()),
- expandGroups);
+ expandGroups,
+ project,
+ cache);
}
public PathOwners(
@@ -135,7 +144,9 @@
List<Project.NameKey> parentProjectsNames,
Optional<String> branchWhenEnabled,
Set<String> modifiedPaths,
- boolean expandGroups) {
+ boolean expandGroups,
+ String project,
+ PathOwnersEntriesCache cache) {
this.repositoryManager = repositoryManager;
this.repository = repository;
this.parentProjectsNames = parentProjectsNames;
@@ -144,7 +155,10 @@
this.accounts = accounts;
this.expandGroups = expandGroups;
- OwnersMap map = branchWhenEnabled.map(branch -> fetchOwners(branch)).orElse(new OwnersMap());
+ OwnersMap map =
+ branchWhenEnabled
+ .map(branch -> fetchOwners(project, branch, cache))
+ .orElse(new OwnersMap());
owners = Multimaps.unmodifiableSetMultimap(map.getPathOwners());
reviewers = Multimaps.unmodifiableSetMultimap(map.getPathReviewers());
matchers = map.getMatchers();
@@ -195,21 +209,29 @@
*
* @return A structure containing matchers paths to owners
*/
- private OwnersMap fetchOwners(String branch) {
+ private OwnersMap fetchOwners(String project, String branch, PathOwnersEntriesCache cache) {
OwnersMap ownersMap = new OwnersMap();
try {
// Using a `map` would have needed a try/catch inside the lamba, resulting in more code
List<PathOwnersEntry> parentsPathOwnersEntries =
- getPathOwnersEntries(parentProjectsNames, RefNames.REFS_CONFIG);
- PathOwnersEntry projectEntry = getPathOwnersEntry(repository, RefNames.REFS_CONFIG);
- PathOwnersEntry rootEntry = getPathOwnersEntry(repository, branch);
+ getPathOwnersEntries(parentProjectsNames, RefNames.REFS_CONFIG, cache);
+ PathOwnersEntry projectEntry =
+ getPathOwnersEntry(project, repository, RefNames.REFS_CONFIG, cache);
+ PathOwnersEntry rootEntry = getPathOwnersEntry(project, repository, branch, cache);
Map<String, PathOwnersEntry> entries = new HashMap<>();
PathOwnersEntry currentEntry = null;
for (String path : modifiedPaths) {
currentEntry =
resolvePathEntry(
- path, branch, projectEntry, parentsPathOwnersEntries, rootEntry, entries);
+ project,
+ path,
+ branch,
+ projectEntry,
+ parentsPathOwnersEntries,
+ rootEntry,
+ entries,
+ cache);
// add owners and reviewers to file for matcher predicates
ownersMap.addFileOwners(path, currentEntry.getOwners());
@@ -239,38 +261,47 @@
}
ownersMap.setLabel(Optional.ofNullable(currentEntry).flatMap(PathOwnersEntry::getLabel));
return ownersMap;
- } catch (IOException e) {
+ } catch (IOException | ExecutionException e) {
log.warn("Invalid OWNERS file", e);
return ownersMap;
}
}
private List<PathOwnersEntry> getPathOwnersEntries(
- List<Project.NameKey> projectNames, String branch) throws IOException {
+ List<Project.NameKey> projectNames, String branch, PathOwnersEntriesCache cache)
+ throws IOException, ExecutionException {
ImmutableList.Builder<PathOwnersEntry> pathOwnersEntries = ImmutableList.builder();
for (Project.NameKey projectName : projectNames) {
try (Repository repo = repositoryManager.openRepository(projectName)) {
- pathOwnersEntries = pathOwnersEntries.add(getPathOwnersEntry(repo, branch));
+ pathOwnersEntries =
+ pathOwnersEntries.add(getPathOwnersEntry(projectName.get(), repo, branch, cache));
}
}
return pathOwnersEntries.build();
}
- private PathOwnersEntry getPathOwnersEntry(Repository repo, String branch) throws IOException {
+ private PathOwnersEntry getPathOwnersEntry(
+ String project, Repository repo, String branch, PathOwnersEntriesCache cache)
+ throws ExecutionException {
String rootPath = "OWNERS";
- return getOwnersConfig(repo, rootPath, branch)
- .map(
- conf ->
- new PathOwnersEntry(
- rootPath,
- conf,
- accounts,
- Optional.empty(),
- Collections.emptySet(),
- Collections.emptySet(),
- Collections.emptySet(),
- Collections.emptySet()))
- .orElse(new PathOwnersEntry());
+ return cache.get(
+ project,
+ branch,
+ rootPath,
+ () ->
+ getOwnersConfig(repo, rootPath, branch)
+ .map(
+ conf ->
+ new PathOwnersEntry(
+ rootPath,
+ conf,
+ accounts,
+ Optional.empty(),
+ Collections.emptySet(),
+ Collections.emptySet(),
+ Collections.emptySet(),
+ Collections.emptySet()))
+ .orElse(new PathOwnersEntry()));
}
private void processMatcherPerPath(
@@ -318,13 +349,15 @@
}
private PathOwnersEntry resolvePathEntry(
+ String project,
String path,
String branch,
PathOwnersEntry projectEntry,
List<PathOwnersEntry> parentsPathOwnersEntries,
PathOwnersEntry rootEntry,
- Map<String, PathOwnersEntry> entries)
- throws IOException {
+ Map<String, PathOwnersEntry> entries,
+ PathOwnersEntriesCache cache)
+ throws ExecutionException {
String[] parts = path.split("/");
PathOwnersEntry currentEntry = rootEntry;
StringBuilder builder = new StringBuilder();
@@ -349,25 +382,33 @@
currentEntry = entries.get(partial);
} else {
String ownersPath = partial + "OWNERS";
- Optional<OwnersConfig> conf = getOwnersConfig(repository, ownersPath, branch);
- Optional<LabelDefinition> label = currentEntry.getLabel();
- final Set<Id> owners = currentEntry.getOwners();
- final Set<Id> reviewers = currentEntry.getReviewers();
- Collection<Matcher> inheritedMatchers = currentEntry.getMatchers().values();
- Set<String> groupOwners = currentEntry.getGroupOwners();
+ PathOwnersEntry pathFallbackEntry = currentEntry;
currentEntry =
- conf.map(
- c ->
- new PathOwnersEntry(
- ownersPath,
- c,
- accounts,
- label,
- owners,
- reviewers,
- inheritedMatchers,
- groupOwners))
- .orElse(currentEntry);
+ cache.get(
+ project,
+ branch,
+ ownersPath,
+ () ->
+ getOwnersConfig(repository, ownersPath, branch)
+ .map(
+ c -> {
+ Optional<LabelDefinition> label = pathFallbackEntry.getLabel();
+ final Set<Id> owners = pathFallbackEntry.getOwners();
+ final Set<Id> reviewers = pathFallbackEntry.getReviewers();
+ Collection<Matcher> inheritedMatchers =
+ pathFallbackEntry.getMatchers().values();
+ Set<String> groupOwners = pathFallbackEntry.getGroupOwners();
+ return new PathOwnersEntry(
+ ownersPath,
+ c,
+ accounts,
+ label,
+ owners,
+ reviewers,
+ inheritedMatchers,
+ groupOwners);
+ })
+ .orElse(pathFallbackEntry));
entries.put(partial, currentEntry);
}
}
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCache.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCache.java
new file mode 100644
index 0000000..07be702
--- /dev/null
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCache.java
@@ -0,0 +1,141 @@
+// Copyright (C) 2023 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.owners.common;
+
+import com.google.common.cache.RemovalNotification;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import java.util.Objects;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+
+public interface PathOwnersEntriesCache {
+ String CACHE_NAME = "path_owners_entries";
+
+ static Module module() {
+ return new CacheModule() {
+ @Override
+ protected void configure() {
+ cache(CACHE_NAME, Key.class, PathOwnersEntry.class);
+ bind(PathOwnersEntriesCache.class).to(PathOwnersEntriesCacheImpl.class);
+ DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
+ .to(OwnersRefUpdateListener.class);
+ DynamicSet.bind(binder(), CacheRemovalListener.class).to(OwnersCacheRemovalListener.class);
+ }
+ };
+ }
+
+ PathOwnersEntry get(String project, String branch, String path, Callable<PathOwnersEntry> loader)
+ throws ExecutionException;
+
+ void invalidate(String project, String branch);
+
+ void invalidateIndexKey(Key key);
+
+ class Key {
+ final String project;
+ final String branch;
+ final String path;
+
+ Key(String project, String branch, String path) {
+ this.project = project;
+ this.branch = branch;
+ this.path = path;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(branch, path, project);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (obj == null) {
+ return false;
+ }
+
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ Key other = (Key) obj;
+ return Objects.equals(branch, other.branch)
+ && Objects.equals(path, other.path)
+ && Objects.equals(project, other.project);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Key [project=");
+ builder.append(project);
+ builder.append(", branch=");
+ builder.append(branch);
+ builder.append(", path=");
+ builder.append(path);
+ builder.append("]");
+ return builder.toString();
+ }
+ }
+
+ @Singleton
+ class OwnersRefUpdateListener implements GitReferenceUpdatedListener {
+ private final PathOwnersEntriesCache cache;
+
+ @Inject
+ OwnersRefUpdateListener(PathOwnersEntriesCache cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public void onGitReferenceUpdated(Event event) {
+ cache.invalidate(event.getProjectName(), event.getRefName());
+ }
+ }
+
+ @Singleton
+ class OwnersCacheRemovalListener implements CacheRemovalListener<Key, PathOwnersEntry> {
+ private final PathOwnersEntriesCache cache;
+ private final String cacheName;
+
+ @Inject
+ OwnersCacheRemovalListener(@PluginName String pluginName, PathOwnersEntriesCache cache) {
+ this.cache = cache;
+ this.cacheName = String.format("%s.%s", pluginName, CACHE_NAME);
+ }
+
+ @Override
+ public void onRemoval(
+ String pluginName,
+ String cacheName,
+ RemovalNotification<Key, PathOwnersEntry> notification) {
+ if (!this.cacheName.equals(cacheName)) {
+ return;
+ }
+
+ cache.invalidateIndexKey(notification.getKey());
+ }
+ }
+}
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheImpl.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheImpl.java
new file mode 100644
index 0000000..74f9585
--- /dev/null
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheImpl.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2023 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.owners.common;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+
+@Singleton
+class PathOwnersEntriesCacheImpl implements PathOwnersEntriesCache {
+
+ private final Cache<Key, PathOwnersEntry> cache;
+ private final Multimap<String, Key> keysIndex;
+ private final LoadingCache<String, Object> keyLocks;
+
+ @Inject
+ PathOwnersEntriesCacheImpl(@Named(CACHE_NAME) Cache<Key, PathOwnersEntry> cache) {
+ this.cache = cache;
+ this.keysIndex = HashMultimap.create();
+ this.keyLocks =
+ CacheBuilder.newBuilder()
+ .expireAfterAccess(Duration.ofMinutes(10L))
+ .build(CacheLoader.from(Object::new));
+ }
+
+ @Override
+ public PathOwnersEntry get(
+ String project, String branch, String path, Callable<PathOwnersEntry> loader)
+ throws ExecutionException {
+ Key key = new Key(project, branch, path);
+ return cache.get(
+ key,
+ () -> {
+ PathOwnersEntry entry = loader.call();
+ String indexKey = indexKey(project, branch);
+ synchronized (keyLocks.getUnchecked(indexKey)) {
+ keysIndex.put(indexKey, key);
+ }
+ return entry;
+ });
+ }
+
+ @Override
+ public void invalidate(String project, String branch) {
+ String indexKey = indexKey(project, branch);
+ Collection<Key> keysToInvalidate;
+
+ synchronized (keyLocks.getUnchecked(indexKey)) {
+ keysToInvalidate = keysIndex.removeAll(indexKey);
+ }
+
+ keysToInvalidate.forEach(cache::invalidate);
+ }
+
+ @Override
+ public void invalidateIndexKey(Key key) {
+ String indexKey = indexKey(key.project, key.branch);
+
+ synchronized (keyLocks.getUnchecked(indexKey)) {
+ Collection<Key> values = keysIndex.asMap().get(indexKey);
+ if (values != null) {
+ values.remove(key);
+ }
+ }
+ }
+
+ private String indexKey(String project, String branch) {
+ return new StringBuilder(project).append('@').append(branch).toString();
+ }
+}
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheMock.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheMock.java
new file mode 100644
index 0000000..1eac62c
--- /dev/null
+++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersEntriesCacheMock.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2023 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.owners.common;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import org.junit.Ignore;
+
+/** This is a test implementation that doesn't cache anything but calls loader instead. */
+@Ignore
+public class PathOwnersEntriesCacheMock implements PathOwnersEntriesCache {
+ int hit = 0;
+
+ @Override
+ public void invalidate(String project, String branch) {}
+
+ @Override
+ public void invalidateIndexKey(Key key) {}
+
+ @Override
+ public PathOwnersEntry get(
+ String project, String branch, String path, Callable<PathOwnersEntry> loader)
+ throws ExecutionException {
+ try {
+ hit++;
+ return loader.call();
+ } catch (Exception e) {
+ throw new ExecutionException(e);
+ }
+ }
+}
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java
index 710a2b7..2e23f33 100644
--- a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java
+++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java
@@ -21,7 +21,9 @@
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.powermock.api.easymock.PowerMock.replayAll;
import com.google.gerrit.entities.Account;
@@ -50,6 +52,8 @@
private static final boolean DO_NOT_EXPAND_GROUPS = false;
private static final String EXPECTED_LABEL = "expected-label";
private static final String A_LABEL = "a-label";
+ private static PathOwnersEntriesCache CACHE_MOCK = new PathOwnersEntriesCacheMock();
+
public static final String CLASSIC_FILE_TXT = "classic/file.txt";
public static final Project.NameKey parentRepository1NameKey =
Project.NameKey.parse("parentRepository1");
@@ -74,7 +78,9 @@
emptyList(),
branch,
Set.of(CLASSIC_FILE_TXT),
- EXPAND_GROUPS);
+ EXPAND_GROUPS,
+ "foo",
+ CACHE_MOCK);
Set<Account.Id> ownersSet = owners.get().get(CLASSIC_OWNERS);
assertEquals(2, ownersSet.size());
assertTrue(ownersSet.contains(USER_A_ID));
@@ -94,7 +100,9 @@
emptyList(),
branch,
Set.of(CLASSIC_FILE_TXT),
- DO_NOT_EXPAND_GROUPS);
+ DO_NOT_EXPAND_GROUPS,
+ "foo",
+ CACHE_MOCK);
Set<String> ownersSet = owners.getFileGroupOwners().get(CLASSIC_FILE_TXT);
assertEquals(2, ownersSet.size());
assertTrue(ownersSet.contains(USER_A));
@@ -114,7 +122,9 @@
emptyList(),
Optional.empty(),
Set.of(CLASSIC_FILE_TXT),
- EXPAND_GROUPS);
+ EXPAND_GROUPS,
+ "foo",
+ CACHE_MOCK);
Set<Account.Id> ownersSet = owners.get().get(CLASSIC_OWNERS);
assertEquals(0, ownersSet.size());
}
@@ -137,7 +147,9 @@
emptyList(),
branch,
Set.of("classic/file.txt"),
- EXPAND_GROUPS);
+ EXPAND_GROUPS,
+ "foo",
+ CACHE_MOCK);
Set<Account.Id> ownersSet2 = owners2.get().get(CLASSIC_OWNERS);
// in this case we are inheriting the acct3 from /OWNERS
@@ -174,7 +186,9 @@
emptyList(),
branch,
Set.of(fileName),
- EXPAND_GROUPS);
+ EXPAND_GROUPS,
+ "foo",
+ CACHE_MOCK);
Map<String, Set<Account.Id>> fileOwners = owners.getFileOwners();
assertEquals(1, fileOwners.size());
@@ -217,7 +231,9 @@
Arrays.asList(parentRepository1NameKey),
branch,
Set.of(fileName),
- EXPAND_GROUPS);
+ EXPAND_GROUPS,
+ "foo",
+ CACHE_MOCK);
Map<String, Set<Account.Id>> fileOwners = owners.getFileOwners();
assertEquals(fileOwners.size(), 1);
@@ -264,7 +280,9 @@
Arrays.asList(parentRepository1NameKey, parentRepository2NameKey),
branch,
Set.of(sqlFileName, javaFileName),
- EXPAND_GROUPS);
+ EXPAND_GROUPS,
+ "foo",
+ CACHE_MOCK);
Map<String, Set<Account.Id>> fileOwners = owners.getFileOwners();
assertEquals(fileOwners.size(), 2);
@@ -306,7 +324,9 @@
emptyList(),
branch,
Set.of("dir/subdir/file.txt"),
- EXPAND_GROUPS);
+ EXPAND_GROUPS,
+ "foo",
+ CACHE_MOCK);
Set<Account.Id> ownersSet = owners.get().get("dir/subdir/OWNERS");
assertEquals(3, ownersSet.size());
@@ -360,6 +380,48 @@
assertTrue(owners.contains(USER_C_EMAIL_COM));
}
+ @Test
+ public void testPathOwnersEntriesCacheIsCalled() throws Exception {
+ expectConfig("OWNERS", "master", createConfig(true, Optional.of(EXPECTED_LABEL), owners()));
+ expectConfig(
+ "OWNERS",
+ RefNames.REFS_CONFIG,
+ repository,
+ createConfig(true, Optional.of("foo"), owners()));
+ expectConfig("dir/OWNERS", createConfig(true, Optional.of(A_LABEL), owners(USER_B_EMAIL_COM)));
+ expectConfig(
+ "dir/subdir/OWNERS",
+ createConfig(true, Optional.of(EXPECTED_LABEL), owners(USER_A_EMAIL_COM)));
+ expectConfig(
+ "OWNERS",
+ RefNames.REFS_CONFIG,
+ parentRepository1,
+ createConfig(true, Optional.of("bar"), owners()));
+
+ mockParentRepository(parentRepository1NameKey, parentRepository1);
+ replayAll();
+
+ PathOwnersEntriesCacheMock cacheMock = new PathOwnersEntriesCacheMock();
+ PathOwners owners =
+ new PathOwners(
+ accounts,
+ repositoryManager,
+ repository,
+ Arrays.asList(parentRepository1NameKey),
+ branch,
+ Set.of("dir/subdir/file.txt"),
+ EXPAND_GROUPS,
+ "foo",
+ cacheMock);
+
+ assertThat(owners.getFileOwners()).isNotEmpty();
+ int expectedCacheCalls =
+ 1 /* for refs/meta/config/OWNERS */
+ + 3 /* for each parent directory of 'file.txt' */
+ + 1 /* for parent's refs/meta/config/OWNERS */;
+ assertThat(cacheMock.hit).isEqualTo(expectedCacheCalls);
+ }
+
private void mockOwners(String... owners) throws IOException {
expectNoConfig("OWNERS");
expectConfig(CLASSIC_OWNERS, createConfig(false, owners(owners)));
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java
index 8ae411e..ac011b0 100644
--- a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java
+++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java
@@ -158,7 +158,9 @@
"project/bfile.txt", // no matching
"projectalfa", // matches PartialRegex
"project/file.sql"), // only .sql matching b,c
- EXPAND_GROUPS);
+ EXPAND_GROUPS,
+ "foo",
+ new PathOwnersEntriesCacheMock());
// assertions on classic owners
Set<Account.Id> ownersSet = owners.get().get("project/OWNERS");
@@ -266,7 +268,9 @@
emptyList(),
branch,
Set.of("project/file.sql", "another.txt"),
- EXPAND_GROUPS);
+ EXPAND_GROUPS,
+ "foo",
+ new PathOwnersEntriesCacheMock());
Set<String> ownedFiles = owners.getFileOwners().keySet();
assertThat(ownedFiles).containsExactly("project/file.sql");
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnerPredicateProvider.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnerPredicateProvider.java
index 119f1bd..d15da8e 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnerPredicateProvider.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnerPredicateProvider.java
@@ -21,14 +21,19 @@
import com.google.gerrit.server.rules.PredicateProvider;
import com.google.inject.Inject;
import com.googlesource.gerrit.owners.common.Accounts;
+import com.googlesource.gerrit.owners.common.PathOwnersEntriesCache;
import com.googlesource.gerrit.owners.common.PluginSettings;
/** Gerrit OWNERS Prolog Predicate Provider. */
@Listen
public class OwnerPredicateProvider implements PredicateProvider {
@Inject
- public OwnerPredicateProvider(Accounts accounts, PluginSettings config, OwnersMetrics metrics) {
- OwnersStoredValues.initialize(accounts, config, metrics);
+ public OwnerPredicateProvider(
+ Accounts accounts,
+ PluginSettings config,
+ PathOwnersEntriesCache cache,
+ OwnersMetrics metrics) {
+ OwnersStoredValues.initialize(accounts, config, cache, metrics);
}
@Override
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java
index bd2f074..7d2c4d5 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java
@@ -20,6 +20,7 @@
import com.google.gerrit.server.rules.PredicateProvider;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
+import com.googlesource.gerrit.owners.common.PathOwnersEntriesCache;
import com.googlesource.gerrit.owners.common.PluginSettings;
public class OwnersModule extends AbstractModule {
@@ -34,6 +35,7 @@
@Override
protected void configure() {
+ install(PathOwnersEntriesCache.module());
DynamicSet.bind(binder(), PredicateProvider.class)
.to(OwnerPredicateProvider.class)
.asEagerSingleton();
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
index fb1de16..02ed2f8 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
@@ -26,6 +26,7 @@
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlesource.gerrit.owners.common.Accounts;
import com.googlesource.gerrit.owners.common.PathOwners;
+import com.googlesource.gerrit.owners.common.PathOwnersEntriesCache;
import com.googlesource.gerrit.owners.common.PluginSettings;
import java.util.Arrays;
import java.util.Collections;
@@ -43,7 +44,10 @@
public static StoredValue<PathOwners> PATH_OWNERS;
public static synchronized void initialize(
- Accounts accounts, PluginSettings settings, OwnersMetrics metrics) {
+ Accounts accounts,
+ PluginSettings settings,
+ PathOwnersEntriesCache cache,
+ OwnersMetrics metrics) {
if (PATH_OWNERS != null) {
return;
}
@@ -72,7 +76,9 @@
maybeParentProjectNameKey,
settings.isBranchDisabled(branch) ? Optional.empty() : Optional.of(branch),
patchList,
- settings.expandGroups());
+ settings.expandGroups(),
+ projectState.getName(),
+ cache);
}
}
};
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java
index a0339de..97d8098 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java
@@ -48,6 +48,7 @@
import com.googlesource.gerrit.owners.common.Accounts;
import com.googlesource.gerrit.owners.common.LabelDefinition;
import com.googlesource.gerrit.owners.common.PathOwners;
+import com.googlesource.gerrit.owners.common.PathOwnersEntriesCache;
import com.googlesource.gerrit.owners.common.PluginSettings;
import java.io.IOException;
import java.util.Arrays;
@@ -83,6 +84,7 @@
private final GitRepositoryManager repoManager;
private final DiffOperations diffOperations;
private final ApprovalsUtil approvalsUtil;
+ private final PathOwnersEntriesCache cache;
@Inject
OwnersSubmitRequirement(
@@ -92,7 +94,8 @@
Accounts accounts,
GitRepositoryManager repoManager,
DiffOperations diffOperations,
- ApprovalsUtil approvalsUtil) {
+ ApprovalsUtil approvalsUtil,
+ PathOwnersEntriesCache cache) {
this.metrics = metrics;
this.pluginSettings = pluginSettings;
this.projectCache = projectCache;
@@ -100,6 +103,7 @@
this.repoManager = repoManager;
this.diffOperations = diffOperations;
this.approvalsUtil = approvalsUtil;
+ this.cache = cache;
}
@Override
@@ -211,7 +215,9 @@
parents,
pluginSettings.isBranchDisabled(branch) ? Optional.empty() : Optional.of(branch),
getDiff(nameKey, cd.currentPatchSet().commitId()),
- pluginSettings.expandGroups());
+ pluginSettings.expandGroups(),
+ nameKey.get(),
+ cache);
return pathOwners;
}
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java b/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java
index 475f1cd..d5d403b 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java
@@ -40,6 +40,7 @@
import com.google.inject.Singleton;
import com.googlesource.gerrit.owners.common.Accounts;
import com.googlesource.gerrit.owners.common.PathOwners;
+import com.googlesource.gerrit.owners.common.PathOwnersEntriesCache;
import com.googlesource.gerrit.owners.common.PluginSettings;
import com.googlesource.gerrit.owners.entities.FilesOwnersResponse;
import com.googlesource.gerrit.owners.entities.GroupOwner;
@@ -62,6 +63,7 @@
private final PluginSettings pluginSettings;
private final GerritApi gerritApi;
private final ChangeData.Factory changeDataFactory;
+ private final PathOwnersEntriesCache cache;
@Inject
GetFilesOwners(
@@ -72,7 +74,8 @@
GitRepositoryManager repositoryManager,
PluginSettings pluginSettings,
GerritApi gerritApi,
- ChangeData.Factory changeDataFactory) {
+ ChangeData.Factory changeDataFactory,
+ PathOwnersEntriesCache cache) {
this.patchListCache = patchListCache;
this.accounts = accounts;
this.accountCache = accountCache;
@@ -81,6 +84,7 @@
this.pluginSettings = pluginSettings;
this.gerritApi = gerritApi;
this.changeDataFactory = changeDataFactory;
+ this.cache = cache;
}
@Override
@@ -90,14 +94,15 @@
Change change = revision.getChange();
int id = revision.getChangeResource().getChange().getChangeId();
+ Project.NameKey project = change.getProject();
List<Project.NameKey> maybeParentProjectNameKey =
projectCache
- .get(change.getProject())
+ .get(project)
.map(p -> Arrays.asList(p.getProject().getParent()))
.filter(Predicates.notNull())
.orElse(Collections.emptyList());
- try (Repository repository = repositoryManager.openRepository(change.getProject())) {
+ try (Repository repository = repositoryManager.openRepository(project)) {
Set<String> changePaths = new HashSet<>(changeDataFactory.create(change).currentFilePaths());
String branch = change.getDest().branch();
@@ -109,7 +114,9 @@
maybeParentProjectNameKey,
pluginSettings.isBranchDisabled(branch) ? Optional.empty() : Optional.of(branch),
changePaths,
- pluginSettings.expandGroups());
+ pluginSettings.expandGroups(),
+ project.get(),
+ cache);
Map<String, Set<GroupOwner>> fileToOwners =
pluginSettings.expandGroups()
diff --git a/owners/src/main/resources/Documentation/config.md b/owners/src/main/resources/Documentation/config.md
index 62ac73c..e8ae0eb 100644
--- a/owners/src/main/resources/Documentation/config.md
+++ b/owners/src/main/resources/Documentation/config.md
@@ -36,6 +36,26 @@
enableSubmitRequirement = true
```
+cache."owners.path_owners_entries".memoryLimit
+: The cache is used to hold the parsed version of `OWNERS` files in the
+ repository so that when submit rules are calculated (either through prolog
+ or through submit requirements) it is not read over and over again. The
+ cache entry gets invalidated when `OWNERS` file branch is updated.
+ By default it follows default Gerrit's cache memory limit but it makes
+ sense to adjust it as a function of number of project that use the `owners`
+ plugin multiplied by average number of active branches (plus 1 for the
+ refs/meta/config) and average number of directories (as directory hierarchy
+ back to root is checked for the `OWNERS` file existence).
+ _Note that in opposite to the previous settings the modification needs to be
+ performed in the `$GERRIT_SITE/etc/gerrit.config` file._
+
+Example
+
+```
+[cache "owners.path_owners_entries"]
+ memoryLimit = 2048
+```
+
## Configuration
Owner approval is determined based on OWNERS files located in the same
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java
index cd23d0d..ec3e816 100644
--- a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java
@@ -35,7 +35,7 @@
import org.eclipse.jgit.junit.TestRepository;
import org.junit.Test;
-@TestPlugin(name = "owners", httpModule = "com.googlesource.gerrit.owners.OwnersRestApiModule")
+@TestPlugin(name = "owners", sysModule = "com.googlesource.gerrit.owners.OwnersModule")
@UseLocalDisk
public class GetFilesOwnersIT extends LightweightPluginDaemonTest {