summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Fick <martin.fick@linaro.org>2023-09-29 11:30:24 -0600
committerMartin Fick <martin.fick@linaro.org>2023-09-29 11:30:24 -0600
commit3759af53102d631c25e3a65abcfad92c659eda5f (patch)
tree5ebae096ce9fdafe97be3736f91c2536f75a4b1a
parent643633c49f769bd8862ca1d8c7e9b9308a46fbd2 (diff)
parent90bfc86419e723599e04ec56f1a9aae7f4d6a817 (diff)
Merge branch 'stable-3.7' into stable-3.8
* stable-3.7: Add in memory change data cache by project Change-Id: Icf31e1c992558f48894e429c17929da926934766 Release-Notes: skip
-rw-r--r--Documentation/config-gerrit.txt7
-rw-r--r--java/com/google/gerrit/httpd/init/WebAppInitializer.java4
-rw-r--r--java/com/google/gerrit/pgm/Daemon.java7
-rw-r--r--java/com/google/gerrit/pgm/util/BatchProgramModule.java8
-rw-r--r--java/com/google/gerrit/server/git/ChangesByProjectCache.java63
-rw-r--r--java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java360
-rw-r--r--java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java35
-rw-r--r--java/com/google/gerrit/server/notedb/ChangeNotes.java6
-rw-r--r--java/com/google/gerrit/server/permissions/ChangeControl.java2
-rw-r--r--java/com/google/gerrit/server/permissions/DefaultRefFilter.java16
-rw-r--r--java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java55
-rw-r--r--java/com/google/gerrit/server/permissions/PermissionBackend.java2
-rw-r--r--java/com/google/gerrit/server/permissions/ProjectControl.java16
-rw-r--r--java/com/google/gerrit/server/query/change/ChangeData.java93
-rw-r--r--java/com/google/gerrit/testing/InMemoryModule.java4
15 files changed, 574 insertions, 104 deletions
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ecee0b7385..98ce770888 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -990,6 +990,13 @@ or multiple replica nodes.
The cache should be flushed whenever NoteDb change metadata in a repository is
modified outside of Gerrit.
+cache `"changes_by_project"`::
++
+Ideally, the memorylimit of this cache is large enough to cover all projects.
+This should significantly speed up change ref advertisements and git pushes,
+especially for projects with lots of changes, and particularly on replicas
+where there is no index.
+
cache `"git_modified_files"`::
+
Each item caches the list of git modified files between two git trees
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 7293f35f49..e3cc0a5ccb 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -76,9 +76,9 @@ import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.config.SysExecutorModule;
import com.google.gerrit.server.events.EventBroker.EventBrokerModule;
import com.google.gerrit.server.events.StreamEventsApiListener.StreamEventsApiListenerModule;
+import com.google.gerrit.server.git.ChangesByProjectCache;
import com.google.gerrit.server.git.GarbageCollectionModule;
import com.google.gerrit.server.git.GitRepositoryManagerModule;
-import com.google.gerrit.server.git.SearchingChangeCacheImpl.SearchingChangeCacheImplModule;
import com.google.gerrit.server.git.SystemReaderInstaller;
import com.google.gerrit.server.git.WorkQueue.WorkQueueModule;
import com.google.gerrit.server.index.IndexModule;
@@ -313,7 +313,7 @@ public class WebAppInitializer extends GuiceServletContextListener implements Fi
modules.add(new ProjectQueryBuilderModule());
modules.add(new DefaultRefLogIdentityProvider.Module());
modules.add(new PluginApiModule());
- modules.add(new SearchingChangeCacheImplModule());
+ modules.add(new ChangesByProjectCache.Module(ChangesByProjectCache.UseIndex.TRUE, config));
modules.add(new InternalAccountDirectoryModule());
modules.add(new DefaultPermissionBackendModule());
modules.add(new DefaultMemoryCacheModule());
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 7474709353..166d35e87b 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -85,8 +85,8 @@ import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SysExecutorModule;
import com.google.gerrit.server.events.EventBroker.EventBrokerModule;
import com.google.gerrit.server.events.StreamEventsApiListener.StreamEventsApiListenerModule;
+import com.google.gerrit.server.git.ChangesByProjectCache;
import com.google.gerrit.server.git.GarbageCollectionModule;
-import com.google.gerrit.server.git.SearchingChangeCacheImpl.SearchingChangeCacheImplModule;
import com.google.gerrit.server.git.WorkQueue.WorkQueueModule;
import com.google.gerrit.server.group.PeriodicGroupIndexer.PeriodicGroupIndexerModule;
import com.google.gerrit.server.index.AbstractIndexModule;
@@ -467,7 +467,10 @@ public class Daemon extends SiteProgram {
modules.add(new DefaultRefLogIdentityProvider.Module());
modules.add(new PluginApiModule());
- modules.add(new SearchingChangeCacheImplModule(replica));
+ modules.add(
+ new ChangesByProjectCache.Module(
+ replica ? ChangesByProjectCache.UseIndex.FALSE : ChangesByProjectCache.UseIndex.TRUE,
+ config));
modules.add(new InternalAccountDirectoryModule());
modules.add(new DefaultPermissionBackendModule());
modules.add(new DefaultMemoryCacheModule());
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index cae7ca6733..9ad70eb3f5 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -68,8 +68,8 @@ import com.google.gerrit.server.extensions.events.EventUtil;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.extensions.events.RevisionCreated;
import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
+import com.google.gerrit.server.git.ChangesByProjectCache;
import com.google.gerrit.server.git.PureRevertCache;
-import com.google.gerrit.server.git.SearchingChangeCacheImpl;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.notedb.NoteDbModule;
import com.google.gerrit.server.patch.DiffExecutorModule;
@@ -162,10 +162,6 @@ public class BatchProgramModule extends FactoryModule {
factory(PatchSetInserter.Factory.class);
factory(RebaseChangeOp.Factory.class);
- // As Reindex is a batch program, don't assume the index is available for
- // the change cache.
- bind(SearchingChangeCacheImpl.class).toProvider(Providers.of(null));
-
bind(new TypeLiteral<ImmutableSet<GroupReference>>() {})
.annotatedWith(AdministrateServerGroups.class)
.toInstance(ImmutableSet.of());
@@ -177,6 +173,8 @@ public class BatchProgramModule extends FactoryModule {
.toInstance(Collections.emptySet());
modules.add(new BatchGitModule());
+ modules.add(
+ new ChangesByProjectCache.Module(ChangesByProjectCache.UseIndex.FALSE, getConfig()));
modules.add(new DefaultPermissionBackendModule());
modules.add(new DefaultMemoryCacheModule());
modules.add(new H2CacheModule());
diff --git a/java/com/google/gerrit/server/git/ChangesByProjectCache.java b/java/com/google/gerrit/server/git/ChangesByProjectCache.java
new file mode 100644
index 0000000000..df91891544
--- /dev/null
+++ b/java/com/google/gerrit/server/git/ChangesByProjectCache.java
@@ -0,0 +1,63 @@
+// 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.google.gerrit.server.git;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.AbstractModule;
+import java.io.IOException;
+import java.util.stream.Stream;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+
+public interface ChangesByProjectCache {
+ public enum UseIndex {
+ TRUE,
+ FALSE;
+ }
+
+ public static class Module extends AbstractModule {
+ private UseIndex useIndex;
+ private @GerritServerConfig Config config;
+
+ public Module(UseIndex useIndex, @GerritServerConfig Config config) {
+ this.useIndex = useIndex;
+ this.config = config;
+ }
+
+ @Override
+ protected void configure() {
+ boolean searchingCacheEnabled =
+ config.getLong("cache", SearchingChangeCacheImpl.ID_CACHE, "memoryLimit", 0) > 0;
+ if (searchingCacheEnabled && UseIndex.TRUE.equals(useIndex)) {
+ install(new SearchingChangeCacheImpl.SearchingChangeCacheImplModule());
+ } else {
+ bind(UseIndex.class).toInstance(useIndex);
+ install(new ChangesByProjectCacheImpl.Module());
+ }
+ }
+ }
+
+ /**
+ * Stream changeDatas for the project
+ *
+ * @param project project to read.
+ * @param repository repository for the project to read.
+ * @return Stream of known changes; empty if no changes.
+ */
+ Stream<ChangeData> streamChangeDatas(Project.NameKey project, Repository repository)
+ throws IOException;
+}
diff --git a/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java b/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java
new file mode 100644
index 0000000000..094287b7be
--- /dev/null
+++ b/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java
@@ -0,0 +1,360 @@
+// 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.google.gerrit.server.git;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.cache.Cache;
+import com.google.common.cache.Weigher;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.git.ChangesByProjectCache.UseIndex;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Stream;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Lightweight cache of changes in each project.
+ *
+ * <p>This cache is intended to be used when filtering references and stores only the minimal fields
+ * required for a read permission check.
+ */
+@Singleton
+public class ChangesByProjectCacheImpl implements ChangesByProjectCache {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private static final String CACHE_NAME = "changes_by_project";
+
+ public static class Module extends CacheModule {
+ @Override
+ protected void configure() {
+ cache(CACHE_NAME, Project.NameKey.class, CachedProjectChanges.class)
+ .weigher(ChangesByProjetCacheWeigher.class);
+ bind(ChangesByProjectCache.class).to(ChangesByProjectCacheImpl.class);
+ }
+ }
+
+ private final Cache<Project.NameKey, CachedProjectChanges> cache;
+ private final ChangeData.Factory cdFactory;
+ private final UseIndex useIndex;
+ private final Provider<InternalChangeQuery> queryProvider;
+
+ @Inject
+ ChangesByProjectCacheImpl(
+ @Named(CACHE_NAME) Cache<Project.NameKey, CachedProjectChanges> cache,
+ ChangeData.Factory cdFactory,
+ UseIndex useIndex,
+ Provider<InternalChangeQuery> queryProvider) {
+ this.cache = cache;
+ this.cdFactory = cdFactory;
+ this.useIndex = useIndex;
+ this.queryProvider = queryProvider;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Stream<ChangeData> streamChangeDatas(Project.NameKey project, Repository repo)
+ throws IOException {
+ CachedProjectChanges projectChanges = cache.getIfPresent(project);
+ if (projectChanges != null) {
+ return projectChanges
+ .getUpdatedChangeDatas(
+ project, repo, cdFactory, ChangeNotes.Factory.scanChangeIds(repo), "Updating")
+ .stream();
+ }
+ if (UseIndex.TRUE.equals(useIndex)) {
+ return queryChangeDatasAndLoad(project).stream();
+ }
+ return scanChangeDatasAndLoad(project, repo).stream();
+ }
+
+ private Collection<ChangeData> scanChangeDatasAndLoad(Project.NameKey project, Repository repo)
+ throws IOException {
+ CachedProjectChanges ours = new CachedProjectChanges();
+ CachedProjectChanges projectChanges = ours;
+ try {
+ projectChanges = cache.get(project, () -> ours);
+ } catch (ExecutionException e) {
+ logger.atWarning().withCause(e).log("Cannot load %s for %s", CACHE_NAME, project.get());
+ }
+ return projectChanges.getUpdatedChangeDatas(
+ project,
+ repo,
+ cdFactory,
+ ChangeNotes.Factory.scanChangeIds(repo),
+ ours == projectChanges ? "Scanning" : "Updating");
+ }
+
+ private Collection<ChangeData> queryChangeDatasAndLoad(Project.NameKey project) {
+ Collection<ChangeData> cds = queryChangeDatas(project);
+ cache.put(project, new CachedProjectChanges(cds));
+ return cds;
+ }
+
+ private Collection<ChangeData> queryChangeDatas(Project.NameKey project) {
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Querying changes of project", Metadata.builder().projectName(project.get()).build())) {
+ return queryProvider
+ .get()
+ .setRequestedFields(
+ ChangeField.CHANGE_SPEC, ChangeField.REVIEWER_SPEC, ChangeField.REF_STATE_SPEC)
+ .byProject(project);
+ }
+ }
+
+ private static class CachedProjectChanges {
+ Map<String, Map<Change.Id, ObjectId>> metaObjectIdByNonPrivateChangeByBranch =
+ new ConcurrentHashMap<>(); // BranchNameKey "normalized" to a String to dedup project
+ Map<Change.Id, PrivateChange> privateChangeById = new ConcurrentHashMap<>();
+
+ public CachedProjectChanges() {}
+
+ public CachedProjectChanges(Collection<ChangeData> cds) {
+ cds.stream().forEach(cd -> insert(cd));
+ }
+
+ public Collection<ChangeData> getUpdatedChangeDatas(
+ Project.NameKey project,
+ Repository repo,
+ ChangeData.Factory cdFactory,
+ Map<Change.Id, ObjectId> metaObjectIdByChange,
+ String operation) {
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ operation + " changes of project",
+ Metadata.builder().projectName(project.get()).build())) {
+ Map<Change.Id, ChangeData> cachedCdByChange = getChangeDataByChange(project, cdFactory);
+ List<ChangeData> cds = new ArrayList<>();
+ for (Map.Entry<Change.Id, ObjectId> e : metaObjectIdByChange.entrySet()) {
+ Change.Id id = e.getKey();
+ ChangeData cached = cachedCdByChange.get(id);
+ ChangeData cd = cached;
+ try {
+ if (cd == null || !cached.metaRevisionOrThrow().equals(e.getValue())) {
+ cd = cdFactory.create(project, id);
+ update(cached, cd);
+ }
+ } catch (Exception ex) {
+ // Do not let a bad change prevent other changes from being available.
+ logger.atFinest().withCause(ex).log("Can't load changeData for %s", id);
+ }
+ cds.add(cd);
+ }
+ return cds;
+ }
+ }
+
+ public CachedProjectChanges update(ChangeData old, ChangeData updated) {
+ if (old != null) {
+ if (old.isPrivateOrThrow()) {
+ privateChangeById.remove(old.getId());
+ } else {
+ Map<Change.Id, ObjectId> metaObjectIdByNonPrivateChange =
+ metaObjectIdByNonPrivateChangeByBranch.get(old.branchOrThrow().branch());
+ if (metaObjectIdByNonPrivateChange != null) {
+ metaObjectIdByNonPrivateChange.remove(old.getId());
+ }
+ }
+ }
+ return insert(updated);
+ }
+
+ public CachedProjectChanges insert(ChangeData cd) {
+ if (cd.isPrivateOrThrow()) {
+ privateChangeById.put(
+ cd.getId(),
+ new AutoValue_ChangesByProjectCacheImpl_PrivateChange(
+ cd.change(), cd.reviewers(), cd.metaRevisionOrThrow()));
+ } else {
+ metaObjectIdByNonPrivateChangeByBranch
+ .computeIfAbsent(cd.branchOrThrow().branch(), b -> new ConcurrentHashMap<>())
+ .put(cd.getId(), cd.metaRevisionOrThrow());
+ }
+ return this;
+ }
+
+ public Map<Change.Id, ChangeData> getChangeDataByChange(
+ Project.NameKey project, ChangeData.Factory cdFactory) {
+ Map<Change.Id, ChangeData> cdByChange = new HashMap<>(privateChangeById.size());
+ for (PrivateChange pc : privateChangeById.values()) {
+ try {
+ ChangeData cd = cdFactory.create(pc.change());
+ cd.setReviewers(pc.reviewers());
+ cd.setMetaRevision(pc.metaRevision());
+ cdByChange.put(cd.getId(), cd);
+ } catch (Exception ex) {
+ // Do not let a bad change prevent other changes from being available.
+ logger.atFinest().withCause(ex).log("Can't load changeData for %s", pc.change().getId());
+ }
+ }
+
+ for (Map.Entry<String, Map<Change.Id, ObjectId>> e :
+ metaObjectIdByNonPrivateChangeByBranch.entrySet()) {
+ BranchNameKey branch = BranchNameKey.create(project, e.getKey());
+ for (Map.Entry<Change.Id, ObjectId> e2 : e.getValue().entrySet()) {
+ Change.Id id = e2.getKey();
+ try {
+ cdByChange.put(id, cdFactory.createNonPrivate(branch, id, e2.getValue()));
+ } catch (Exception ex) {
+ // Do not let a bad change prevent other changes from being available.
+ logger.atFinest().withCause(ex).log("Can't load changeData for %s", id);
+ }
+ }
+ }
+ return cdByChange;
+ }
+
+ public int weigh() {
+ int size = 0;
+ size += 24 * 2; // guess at basic ConcurrentHashMap overhead * 2
+ for (Map.Entry<String, Map<Change.Id, ObjectId>> e :
+ metaObjectIdByNonPrivateChangeByBranch.entrySet()) {
+ size += JavaWeights.REFERENCE + e.getKey().length();
+ size +=
+ e.getValue().size()
+ * (JavaWeights.REFERENCE
+ + JavaWeights.OBJECT // Map.Entry
+ + JavaWeights.REFERENCE
+ + GerritWeights.CHANGE_NUM
+ + JavaWeights.REFERENCE
+ + GerritWeights.OBJECTID);
+ }
+ for (Map.Entry<Change.Id, PrivateChange> e : privateChangeById.entrySet()) {
+ size += JavaWeights.REFERENCE + GerritWeights.CHANGE_NUM;
+ size += JavaWeights.REFERENCE + e.getValue().weigh();
+ }
+ return size;
+ }
+ }
+
+ @AutoValue
+ abstract static class PrivateChange {
+ // Fields needed to serve permission checks on private Changes
+ abstract Change change();
+
+ @Nullable
+ abstract ReviewerSet reviewers();
+
+ abstract ObjectId metaRevision(); // Needed to confirm whether up-to-date
+
+ public int weigh() {
+ int size = 0;
+ size += JavaWeights.OBJECT; // this
+ size += JavaWeights.REFERENCE + weigh(change());
+ size += JavaWeights.REFERENCE + weigh(reviewers());
+ size += JavaWeights.REFERENCE + GerritWeights.OBJECTID; // metaRevision
+ return size;
+ }
+
+ private static int weigh(Change c) {
+ int size = 0;
+ size += JavaWeights.OBJECT; // change
+ size += JavaWeights.REFERENCE + GerritWeights.KEY_INT; // changeId
+ size += JavaWeights.REFERENCE + JavaWeights.OBJECT + 40; // changeKey;
+ size += JavaWeights.REFERENCE + GerritWeights.TIMESTAMP; // createdOn;
+ size += JavaWeights.REFERENCE + GerritWeights.TIMESTAMP; // lastUpdatedOn;
+ size += JavaWeights.REFERENCE + GerritWeights.ACCOUNT_ID; // owner;
+ size +=
+ JavaWeights.REFERENCE
+ + c.getDest().project().get().length()
+ + c.getDest().branch().length();
+ size += JavaWeights.CHAR; // status;
+ size += JavaWeights.INT; // currentPatchSetId;
+ size += JavaWeights.REFERENCE + c.getSubject().length();
+ size += JavaWeights.REFERENCE + (c.getTopic() == null ? 0 : c.getTopic().length());
+ size +=
+ JavaWeights.REFERENCE
+ + (c.getOriginalSubject().equals(c.getSubject()) ? 0 : c.getSubject().length());
+ size +=
+ JavaWeights.REFERENCE + (c.getSubmissionId() == null ? 0 : c.getSubmissionId().length());
+ size += JavaWeights.REFERENCE + GerritWeights.ACCOUNT_ID; // assignee;
+ size += JavaWeights.REFERENCE + JavaWeights.BOOLEAN; // isPrivate;
+ size += JavaWeights.REFERENCE + JavaWeights.BOOLEAN; // workInProgress;
+ size += JavaWeights.REFERENCE + JavaWeights.BOOLEAN; // reviewStarted;
+ size += JavaWeights.REFERENCE + GerritWeights.CHANGE_NUM; // revertOf;
+ size += JavaWeights.REFERENCE + GerritWeights.PACTCH_SET_ID; // cherryPickOf;
+ return size;
+ }
+
+ private static int weigh(ReviewerSet rs) {
+ int size = 0;
+ size += JavaWeights.OBJECT; // ReviewerSet
+ size += JavaWeights.REFERENCE; // table
+ size +=
+ rs.asTable().cellSet().size()
+ * (JavaWeights.OBJECT // cell (at least one object)
+ + JavaWeights.REFERENCE // ReviewerStateInternal
+ + (JavaWeights.REFERENCE + GerritWeights.ACCOUNT_ID)
+ + (JavaWeights.REFERENCE + GerritWeights.TIMESTAMP));
+ size += JavaWeights.REFERENCE; // accounts
+ return size;
+ }
+ }
+
+ private static class ChangesByProjetCacheWeigher
+ implements Weigher<Project.NameKey, CachedProjectChanges> {
+ @Override
+ public int weigh(Project.NameKey project, CachedProjectChanges changes) {
+ int size = 0;
+ size += project.get().length();
+ size += changes.weigh();
+ return size;
+ }
+ }
+
+ private static class GerritWeights {
+ public static final int KEY_INT = JavaWeights.OBJECT + JavaWeights.INT; // IntKey
+ public static final int CHANGE_NUM = KEY_INT;
+ public static final int ACCOUNT_ID = KEY_INT;
+ public static final int PACTCH_SET_ID =
+ JavaWeights.OBJECT
+ + (JavaWeights.REFERENCE + GerritWeights.CHANGE_NUM) // PatchSet.Id.changeId
+ + JavaWeights.INT; // PatchSet.Id patch_num;
+ public static final int TIMESTAMP = JavaWeights.OBJECT + 8; // Timestamp
+ public static final int OBJECTID = JavaWeights.OBJECT + (5 * JavaWeights.INT); // (w1-w5)
+ }
+
+ private static class JavaWeights {
+ public static final int BOOLEAN = 1;
+ public static final int CHAR = 1;
+ public static final int INT = 4;
+ public static final int OBJECT = 16;
+ public static final int REFERENCE = 8;
+ }
+}
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
index cfeec70fdd..83024e3f45 100644
--- a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -40,12 +40,12 @@ import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
-import com.google.inject.util.Providers;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
+import org.eclipse.jgit.lib.Repository;
/**
* Cache based on an index query of the most recent changes. The number of cached items depends on
@@ -55,35 +55,22 @@ import java.util.stream.Stream;
* fraction of all changes. These are the changes that were modified last.
*/
@Singleton
-public class SearchingChangeCacheImpl implements GitReferenceUpdatedListener {
+public class SearchingChangeCacheImpl
+ implements ChangesByProjectCache, GitReferenceUpdatedListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
static final String ID_CACHE = "changes";
public static class SearchingChangeCacheImplModule extends CacheModule {
- private final boolean slave;
-
- public SearchingChangeCacheImplModule() {
- this(false);
- }
-
- public SearchingChangeCacheImplModule(boolean slave) {
- this.slave = slave;
- }
-
@Override
protected void configure() {
- if (slave) {
- bind(SearchingChangeCacheImpl.class).toProvider(Providers.of(null));
- } else {
- cache(ID_CACHE, Project.NameKey.class, new TypeLiteral<List<CachedChange>>() {})
- .maximumWeight(0)
- .loader(Loader.class);
+ cache(ID_CACHE, Project.NameKey.class, new TypeLiteral<List<CachedChange>>() {})
+ .maximumWeight(0)
+ .loader(Loader.class);
- bind(SearchingChangeCacheImpl.class);
- DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
- .to(SearchingChangeCacheImpl.class);
- }
+ bind(ChangesByProjectCache.class).to(SearchingChangeCacheImpl.class);
+ DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
+ .to(SearchingChangeCacheImpl.class);
}
}
@@ -117,9 +104,11 @@ public class SearchingChangeCacheImpl implements GitReferenceUpdatedListener {
* Additional stored fields are not loaded from the index.
*
* @param project project to read.
+ * @param unusedrepo repository for the project to read.
* @return stream of known changes; empty if no changes.
*/
- public Stream<ChangeData> getChangeData(Project.NameKey project) {
+ @Override
+ public Stream<ChangeData> streamChangeDatas(Project.NameKey project, Repository unusedrepo) {
List<CachedChange> cached;
try {
cached = cache.get(project);
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 222be70ed0..c5d2428198 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -166,6 +166,12 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
return new ChangeNotes(args, newChange(project, changeId), true, null).load();
}
+ public ChangeNotes create(
+ Project.NameKey project, Change.Id changeId, @Nullable ObjectId metaRevId) {
+ checkArgument(project != null, "project is required");
+ return new ChangeNotes(args, newChange(project, changeId), true, null, metaRevId).load();
+ }
+
public ChangeNotes create(Repository repository, Project.NameKey project, Change.Id changeId) {
checkArgument(project != null, "project is required");
return new ChangeNotes(args, newChange(project, changeId), true, null).load(repository);
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index a2f7357f11..db15da801d 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -62,7 +62,7 @@ class ChangeControl {
/** Can this user see this change? */
boolean isVisible() {
- if (getChange().isPrivate() && !isPrivateVisible(changeData)) {
+ if (changeData.isPrivateOrThrow() && !isPrivateVisible(changeData)) {
return false;
}
// Does the user have READ permission on the destination?
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index a70695171b..f1790454e5 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -27,7 +27,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
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.Change;
import com.google.gerrit.entities.RefNames;
@@ -36,12 +35,11 @@ import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.SearchingChangeCacheImpl;
+import com.google.gerrit.server.git.ChangesByProjectCache;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TagMatcher;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
-import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
@@ -94,9 +92,8 @@ class DefaultRefFilter {
private final CurrentUser user;
private final ProjectState projectState;
private final PermissionBackend.ForProject permissionBackendForProject;
- private final @Nullable SearchingChangeCacheImpl searchingChangeDataProvider;
+ private final ChangesByProjectCache changesByProjectCache;
private final ChangeData.Factory changeDataFactory;
- private final ChangeNotes.Factory changeNotesFactory;
private final Metrics metrics;
private final boolean skipFullRefEvaluationIfAllRefsAreVisible;
@@ -107,16 +104,14 @@ class DefaultRefFilter {
RefVisibilityControl refVisibilityControl,
@GerritServerConfig Config config,
Metrics metrics,
- @Nullable SearchingChangeCacheImpl searchingChangeDataProvider,
+ ChangesByProjectCache changesByProjectCache,
ChangeData.Factory changeDataFactory,
- ChangeNotes.Factory changeNotesFactory,
@Assisted ProjectControl projectControl) {
this.tagCache = tagCache;
this.permissionBackend = permissionBackend;
this.refVisibilityControl = refVisibilityControl;
- this.searchingChangeDataProvider = searchingChangeDataProvider;
+ this.changesByProjectCache = changesByProjectCache;
this.changeDataFactory = changeDataFactory;
- this.changeNotesFactory = changeNotesFactory;
this.skipFullRefEvaluationIfAllRefsAreVisible =
config.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true);
this.projectControl = projectControl;
@@ -150,8 +145,7 @@ class DefaultRefFilter {
Suppliers.memoize(
() ->
GitVisibleChangeFilter.getVisibleChanges(
- searchingChangeDataProvider,
- changeNotesFactory,
+ changesByProjectCache,
changeDataFactory,
projectState.getNameKey(),
permissionBackendForProject,
diff --git a/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java b/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
index ae72b067a9..640ea9a6b8 100644
--- a/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
+++ b/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
@@ -17,11 +17,9 @@ package com.google.gerrit.server.permissions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.server.git.SearchingChangeCacheImpl;
-import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.git.ChangesByProjectCache;
import com.google.gerrit.server.query.change.ChangeData;
import java.io.IOException;
import java.util.HashMap;
@@ -39,11 +37,7 @@ import org.eclipse.jgit.lib.Repository;
*
* <ul>
* <li>For a low number of expected checks, we check visibility one-by-one.
- * <li>For a high number of expected checks and settings where the change index is available, we
- * load the N most recent changes from the index and filter them by visibility. This is fast,
- * but comes with the caveat that older changes are pretended to be invisible.
- * <li>For a high number of expected checks and settings where the change index is unavailable, we
- * scan the repo and determine visibility one-by-one. This is *very* expensive.
+ * <li>For a high number of expected checks we use the ChangesByProjectCache.
* </ul>
*
* <p>Changes that fail to load are pretended to be invisible. This is important on the Git paths as
@@ -60,24 +54,23 @@ public class GitVisibleChangeFilter {
/** Returns a map of all visible changes. Might pretend old changes are invisible. */
static ImmutableMap<Change.Id, ChangeData> getVisibleChanges(
- @Nullable SearchingChangeCacheImpl searchingChangeCache,
- ChangeNotes.Factory changeNotesFactory,
+ ChangesByProjectCache changesByProjectCache,
ChangeData.Factory changeDataFactory,
Project.NameKey projectName,
PermissionBackend.ForProject forProject,
Repository repository,
ImmutableSet<Change.Id> changes) {
- Stream<ChangeData> changeDatas;
+ Stream<ChangeData> changeDatas = Stream.empty();
if (changes.size() < CHANGE_LIMIT_FOR_DIRECT_FILTERING) {
logger.atFine().log("Loading changes one by one for project %s", projectName);
changeDatas = loadChangeDatasOneByOne(changes, changeDataFactory, projectName);
- } else if (searchingChangeCache != null) {
- logger.atFine().log("Loading changes from SearchingChangeCache for project %s", projectName);
- changeDatas = searchingChangeCache.getChangeData(projectName);
} else {
- logger.atFine().log("Loading changes from all refs for project %s", projectName);
- changeDatas =
- scanRepoForChangeDatas(changeNotesFactory, changeDataFactory, repository, projectName);
+ logger.atFine().log("Loading changes from ChangesByProjectCache for project %s", projectName);
+ try {
+ changeDatas = changesByProjectCache.streamChangeDatas(projectName, repository);
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log("Unable to streamChangeDatas for %s", projectName);
+ }
}
HashMap<Change.Id, ChangeData> result = new HashMap<>();
changeDatas
@@ -127,32 +120,4 @@ public class GitVisibleChangeFilter {
})
.filter(Objects::nonNull);
}
-
- /** Get a stream of all changes by scanning the repo. This is extremely slow. */
- private static Stream<ChangeData> scanRepoForChangeDatas(
- ChangeNotes.Factory changeNotesFactory,
- ChangeData.Factory changeDataFactory,
- Repository repository,
- Project.NameKey projectName) {
- Stream<ChangeData> cds;
- try {
- cds =
- changeNotesFactory
- .scan(repository, projectName)
- .map(
- notesResult -> {
- if (!notesResult.error().isPresent()) {
- return changeDataFactory.create(notesResult.notes());
- }
- logger.atWarning().withCause(notesResult.error().get()).log(
- "Unable to load ChangeNotes for %s", notesResult.id());
- return null;
- })
- .filter(Objects::nonNull);
- } catch (IOException e) {
- logger.atWarning().withCause(e).log("Unable to scanChangeIds for %s", projectName);
- return Stream.empty();
- }
- return cds;
- }
}
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index eb5e053dc6..ac9ac98c42 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -274,7 +274,7 @@ public abstract class PermissionBackend {
/** Returns an instance scoped for the change, and its destination ref and project. */
public ForChange change(ChangeData cd) {
try {
- return ref(cd.change().getDest().branch()).change(cd);
+ return ref(cd.branchOrThrow().branch()).change(cd);
} catch (StorageException e) {
return FailedPermissionBackend.change("unavailable", e);
}
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index c23501265d..fab894e108 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -115,7 +115,7 @@ class ProjectControl {
}
ChangeControl controlFor(ChangeData cd) {
- return new ChangeControl(controlForRef(cd.change().getDest()), cd);
+ return new ChangeControl(controlForRef(cd.branchOrThrow()), cd);
}
RefControl controlForRef(BranchNameKey ref) {
@@ -366,7 +366,7 @@ class ProjectControl {
@Override
public ForChange change(ChangeData cd) {
try {
- checkProject(cd.change());
+ checkProject(cd);
return super.change(cd);
} catch (StorageException e) {
return FailedPermissionBackend.change("unavailable", e);
@@ -379,13 +379,21 @@ class ProjectControl {
return super.change(notes);
}
+ private void checkProject(ChangeData cd) {
+ checkProject(cd.project());
+ }
+
private void checkProject(Change change) {
+ checkProject(change.getProject());
+ }
+
+ private void checkProject(Project.NameKey changeProject) {
Project.NameKey project = getProject().getNameKey();
checkArgument(
- project.equals(change.getProject()),
+ project.equals(changeProject),
"expected change in project %s, not %s",
project,
- change.getProject());
+ changeProject);
}
@Override
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index e55dea921f..b50662089e 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -40,6 +40,7 @@ import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
@@ -239,6 +240,19 @@ public class ChangeData {
return assistedFactory.create(project, id, null, null);
}
+ public ChangeData create(Project.NameKey project, Change.Id id, ObjectId metaRevision) {
+ ChangeData cd = assistedFactory.create(project, id, null, null);
+ cd.setMetaRevision(metaRevision);
+ return cd;
+ }
+
+ public ChangeData createNonPrivate(BranchNameKey branch, Change.Id id, ObjectId metaRevision) {
+ ChangeData cd = create(branch.project(), id, metaRevision);
+ cd.branch = branch.branch();
+ cd.isPrivate = false;
+ return cd;
+ }
+
public ChangeData create(Change change) {
return assistedFactory.create(change.getProject(), change.getId(), change, null);
}
@@ -382,7 +396,10 @@ public class ChangeData {
private List<ChangeMessage> messages;
private Optional<ChangedLines> changedLines;
private SubmitTypeRecord submitTypeRecord;
+ private String branch;
+ private Boolean isPrivate;
private Boolean mergeable;
+ private ObjectId metaRevision;
private Set<String> hashtags;
/**
* Map from {@link com.google.gerrit.entities.Account.Id} to the tip of the edit ref for this
@@ -603,6 +620,55 @@ public class ChangeData {
return project;
}
+ public BranchNameKey branchOrThrow() {
+ if (change == null) {
+ if (branch != null) {
+ return BranchNameKey.create(project, branch);
+ }
+ throwIfNotLazyLoad("branch");
+ change();
+ }
+ return change.getDest();
+ }
+
+ public boolean isPrivateOrThrow() {
+ if (change == null) {
+ if (isPrivate != null) {
+ return isPrivate;
+ }
+ throwIfNotLazyLoad("isPrivate");
+ change();
+ }
+ return change.isPrivate();
+ }
+
+ public ChangeData setMetaRevision(ObjectId metaRevision) {
+ this.metaRevision = metaRevision;
+ return this;
+ }
+
+ public ObjectId metaRevisionOrThrow() {
+ if (notes == null) {
+ if (metaRevision != null) {
+ return metaRevision;
+ }
+ if (refStates != null) {
+ Set<RefState> refs = refStates.get(project);
+ if (refs != null) {
+ String metaRef = RefNames.changeMetaRef(getId());
+ for (RefState r : refs) {
+ if (r.ref().equals(metaRef)) {
+ return r.id();
+ }
+ }
+ }
+ }
+ throwIfNotLazyLoad("metaRevision");
+ notes();
+ }
+ return notes.getRevision();
+ }
+
boolean fastIsVisibleTo(CurrentUser user) {
return visibleTo == user;
}
@@ -613,7 +679,7 @@ public class ChangeData {
public Change change() {
if (change == null && lazyload()) {
- reloadChange();
+ loadChange();
}
return change;
}
@@ -623,13 +689,19 @@ public class ChangeData {
}
public Change reloadChange() {
+ metaRevision = null;
+ return loadChange();
+ }
+
+ private Change loadChange() {
try {
- notes = notesFactory.createChecked(project, legacyId);
+ notes = notesFactory.createChecked(project, legacyId, metaRevision);
} catch (NoSuchChangeException e) {
throw new StorageException("Unable to load change " + legacyId, e);
}
change = notes.getChange();
changeServerId = notes.getServerId();
+ metaRevision = null;
setPatchSets(null);
return change;
}
@@ -647,7 +719,8 @@ public class ChangeData {
if (!lazyload()) {
throw new StorageException("ChangeNotes not available, lazyLoad = false");
}
- notes = notesFactory.create(project(), legacyId);
+ notes = notesFactory.create(project(), legacyId, metaRevision);
+ change = notes.getChange();
}
return notes;
}
@@ -873,11 +946,7 @@ public class ChangeData {
public ReviewerSet reviewers() {
if (reviewers == null) {
- if (!lazyload()) {
- // We are not allowed to load values from NoteDb. Reviewers were not populated with values
- // from the index. However, we need these values for permission checks.
- throw new IllegalStateException("reviewers not populated");
- }
+ throwIfNotLazyLoad("reviewers");
reviewers = approvalsUtil.getReviewers(notes());
}
return reviewers;
@@ -1401,6 +1470,14 @@ public class ChangeData {
this.refStatePatterns = ImmutableList.copyOf(refStatePatterns);
}
+ private void throwIfNotLazyLoad(String field) {
+ if (!lazyload()) {
+ // We are not allowed to load values from NoteDb. 'field' was not populated, however,
+ // we need this value for permission checks.
+ throw new IllegalStateException("'" + field + "' field not populated");
+ }
+ }
+
@AutoValue
abstract static class ReviewedByEvent {
private static ReviewedByEvent create(ChangeMessage msg) {
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 52ac58a183..00020302a6 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -81,10 +81,10 @@ import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.config.TrackingFootersProvider;
import com.google.gerrit.server.experiments.ConfigExperimentFeatures.ConfigExperimentFeaturesModule;
+import com.google.gerrit.server.git.ChangesByProjectCache;
import com.google.gerrit.server.git.GarbageCollection;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.PerThreadRequestScope;
-import com.google.gerrit.server.git.SearchingChangeCacheImpl.SearchingChangeCacheImplModule;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.WorkQueue.WorkQueueModule;
import com.google.gerrit.server.group.testing.TestGroupBackend;
@@ -200,7 +200,7 @@ public class InMemoryModule extends FactoryModule {
factory(PluginUser.Factory.class);
install(new PluginApiModule());
install(new DefaultPermissionBackendModule());
- install(new SearchingChangeCacheImplModule());
+ install(new ChangesByProjectCache.Module(ChangesByProjectCache.UseIndex.TRUE, cfg));
factory(GarbageCollection.Factory.class);
install(new AuditModule());
install(new SubscriptionGraphModule());