diff options
author | Martin Fick <martin.fick@linaro.org> | 2023-09-29 11:30:24 -0600 |
---|---|---|
committer | Martin Fick <martin.fick@linaro.org> | 2023-09-29 11:30:24 -0600 |
commit | 3759af53102d631c25e3a65abcfad92c659eda5f (patch) | |
tree | 5ebae096ce9fdafe97be3736f91c2536f75a4b1a | |
parent | 643633c49f769bd8862ca1d8c7e9b9308a46fbd2 (diff) | |
parent | 90bfc86419e723599e04ec56f1a9aae7f4d6a817 (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
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()); |