Merge branch 'stable-3.5' * stable-3.5: Bind the cached-refdb repository manager also as named binding Cache refs/... resolutions Allow binding the module without GitRepositoryManager Ensure that cached-refdb compiles against 'stable-3.5' Preload all cache when fetching all refs Use DelegateRepository implementation from Gerrit core Add change verification Change-Id: Ib02fcbb14132ed9ad04aef9c3ff117d57f5d38ce
diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..a56df6a --- /dev/null +++ b/Jenkinsfile
@@ -0,0 +1,2 @@ +pluginPipeline(formatCheckId: 'gerritforge:cached-refdb-code-style', + buildCheckId: 'gerritforge:cached-refdb-build-test')
diff --git a/README.md b/README.md index bdcc00d..1643f01 100644 --- a/README.md +++ b/README.md
@@ -76,6 +76,19 @@ com.googlesource.gerrit.plugins.cachedrefdb.LibSysModule ``` +> NOTE: There are situations where the binding of the module to the Gerrit's +> GitRepositoryManager is not desired; e.g., when using this module together +> with others that are trying to override it at the same time. +> +> It is possible to just load the module using the following two options: +> +> ``` +> git config --file ${GERRIT_SITE}/etc/gerrit.config --add gerrit.installDbModule\ +> com.googlesource.gerrit.plugins.cachedrefdb.LibModule +> git config --file ${GERRIT_SITE}/etc/gerrit.config --add gerrit.installModule\ +> com.googlesource.gerrit.plugins.cachedrefdb.LibSysModule +> ``` + By default cache can hold up to `1024` refs which will not be sufficient for any production site therefore one can configure it through the standard Gerrit cache configuration means e.g.
diff --git a/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefDatabase.java index 04541bd..e40e41b 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefDatabase.java +++ b/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefDatabase.java
@@ -140,7 +140,12 @@ @Override public List<Ref> getRefs() throws IOException { - return delegate.getRefs(); + List<Ref> allRefs = delegate.getRefs(); + for (Ref ref : allRefs) { + refsCache.computeIfAbsent( + repo.getProjectName(), ref.getName(), () -> Optional.ofNullable(ref)); + } + return allRefs; } @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefRepository.java b/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefRepository.java index ebe4146..dada373 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefRepository.java +++ b/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefRepository.java
@@ -14,6 +14,7 @@ package com.googlesource.gerrit.plugins.cachedrefdb; +import com.google.gerrit.entities.RefNames; import com.google.gerrit.server.git.DelegateRepository; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -143,6 +144,13 @@ public ObjectId resolve(String revstr) throws AmbiguousObjectException, IncorrectObjectTypeException, RevisionSyntaxException, IOException { + if (isCacheableReference(revstr)) { + Ref ref = refDb.exactRef(revstr); + if (ref != null) { + return ref.getLeaf().getObjectId(); + } + } + return delegate.resolve(revstr); } @@ -355,4 +363,8 @@ public String getProjectName() { return projectName; } + + private boolean isCacheableReference(String ref) { + return ref.startsWith(RefNames.REFS) && !(ref.contains("^") || ref.contains("~")); + } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/LibDbModule.java b/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/LibDbModule.java index 109126d..2bbf063 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/LibDbModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/LibDbModule.java
@@ -14,10 +14,7 @@ package com.googlesource.gerrit.plugins.cachedrefdb; -import static com.google.inject.Scopes.SINGLETON; - import com.google.common.flogger.FluentLogger; -import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.lifecycle.LifecycleModule; import com.google.gerrit.server.ModuleImpl; import com.google.gerrit.server.config.RepositoryConfig; @@ -40,14 +37,7 @@ @Override protected void configure() { - DynamicItem.itemOf(binder(), RefByNameCache.class); - DynamicItem.bind(binder(), RefByNameCache.class).to(NoOpRefByNameCache.class).in(SINGLETON); - - factory(RefUpdateWithCacheUpdate.Factory.class); - factory(RefRenameWithCacheUpdate.Factory.class); - factory(BatchRefUpdateWithCacheUpdate.Factory.class); - factory(CachedRefDatabase.Factory.class); - factory(CachedRefRepository.Factory.class); + install(new LibModule()); bind(GitRepositoryManager.class).to(CachedGitRepositoryManager.class); @@ -56,6 +46,6 @@ if (!repoConfig.getAllBasePaths().isEmpty()) { bind(LocalDiskRepositoryManager.class).to(MultiBaseLocalDiskRepositoryManager.class); } - logger.atInfo().log("DB library loaded"); + logger.atInfo().log("GitRepositoryManager bind completed"); } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/LibModule.java b/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/LibModule.java new file mode 100644 index 0000000..73593be --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/cachedrefdb/LibModule.java
@@ -0,0 +1,46 @@ +// Copyright (C) 2022 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.plugins.cachedrefdb; + +import static com.google.inject.Scopes.SINGLETON; + +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.LocalDiskRepositoryManager; +import com.google.inject.name.Names; + +public class LibModule extends LifecycleModule { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + @Override + protected void configure() { + bind(GitRepositoryManager.class) + .annotatedWith(Names.named(LocalDiskRepositoryManager.class.getSimpleName())) + .to(CachedGitRepositoryManager.class); + + DynamicItem.itemOf(binder(), RefByNameCache.class); + DynamicItem.bind(binder(), RefByNameCache.class).to(NoOpRefByNameCache.class).in(SINGLETON); + + factory(RefUpdateWithCacheUpdate.Factory.class); + factory(RefRenameWithCacheUpdate.Factory.class); + factory(BatchRefUpdateWithCacheUpdate.Factory.class); + factory(CachedRefDatabase.Factory.class); + factory(CachedRefRepository.Factory.class); + + logger.atInfo().log("Ref repository and db library loaded"); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefDbIT.java b/src/test/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefDbIT.java index 23fb9ac..ff00b26 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefDbIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefDbIT.java
@@ -24,11 +24,16 @@ import com.google.gerrit.server.git.LocalDiskRepositoryManager; import com.google.gerrit.server.git.MultiBaseLocalDiskRepositoryManager; import com.google.inject.Inject; +import com.google.inject.name.Named; import org.junit.Test; @UseLocalDisk @NoHttpd public class CachedRefDbIT extends AbstractDaemonTest { + @Inject(optional = true) + @Named("LocalDiskRepositoryManager") + private GitRepositoryManager localGitRepositoryManager; + @Inject private GitRepositoryManager gitRepoManager; @Inject private RefByNameCacheWrapper refByNameCacheWrapper; @@ -61,4 +66,16 @@ .isInstanceOf(MultiBaseLocalDiskRepositoryManager.class); assertThat(refByNameCacheWrapper.cache()).isInstanceOf(RefByNameCacheImpl.class); } + + @Test + @GerritConfig( + name = "gerrit.installDbModule", + value = "com.googlesource.gerrit.plugins.cachedrefdb.LibModule") + @GerritConfig( + name = "gerrit.installModule", + value = "com.googlesource.gerrit.plugins.cachedrefdb.LibSysModule") + public void shouldBeAbleToInstallCachedGitRepoManagerAsNamedBinding() { + assertThat(localGitRepositoryManager).isNotNull(); + assertThat(localGitRepositoryManager).isInstanceOf(CachedGitRepositoryManager.class); + } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefRepositoryIT.java b/src/test/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefRepositoryIT.java new file mode 100644 index 0000000..5f2c2e4 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/cachedrefdb/CachedRefRepositoryIT.java
@@ -0,0 +1,169 @@ +// Copyright (C) 2022 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.plugins.cachedrefdb; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.registration.DynamicItem; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.Callable; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class CachedRefRepositoryIT { + TestRepository<Repository> tr; + CachedRefRepository objectUnderTest; + TestRefByNameCacheImpl cache; + + @Before + public void setUp() throws IOException { + Repository repo = new InMemoryRepository(new DfsRepositoryDescription("repo")); + objectUnderTest = createCachedRepository(repo); + tr = new TestRepository<>(repo); + } + + @After + public void tearDown() { + // both CachedRefRepository and TestRepository call close on the underlying repo hence single + // close is sufficient + objectUnderTest.close(); + } + + @Test + public void shouldResolveFullRefsFromCache() throws Exception { + String master = RefNames.fullName("master"); + RevCommit first = tr.update(master, tr.commit().add("first", "foo").create()); + String tag = "test_tag"; + String fullTag = RefNames.REFS_TAGS + tag; + tr.update(fullTag, tr.tag(tag, first)); + tr.update(master, tr.commit().parent(first).add("second", "foo").create()); + + assertThat(cache.cacheCalled).isEqualTo(0); + assertThat(objectUnderTest.resolve(master)).isEqualTo(repo().resolve(master)); + assertThat(objectUnderTest.resolve(fullTag)).isEqualTo(repo().resolve(fullTag)); + assertThat(cache.cacheCalled).isEqualTo(2); + } + + @Test + public void shouldNotResolveRefsFromCache() throws Exception { + String master = RefNames.fullName("master"); + RevCommit first = tr.update(master, tr.commit().add("first", "foo").create()); + String tag = "test_tag"; + String fullTag = RefNames.REFS_TAGS + tag; + tr.update(fullTag, tr.tag(tag, first)); + tr.update(master, tr.commit().parent(first).add("second", "foo").create()); + + assertThat(objectUnderTest.resolve("master")).isEqualTo(repo().resolve("master")); + assertThat(objectUnderTest.resolve(RefNames.HEAD)).isEqualTo(repo().resolve(RefNames.HEAD)); + assertThat(objectUnderTest.resolve(tag)).isEqualTo(repo().resolve(tag)); + + String mastersParent = master + "^"; + ObjectId resolved = objectUnderTest.resolve(mastersParent); + assertThat(resolved).isEqualTo(first); + assertThat(resolved).isEqualTo(repo().resolve(mastersParent)); + + String mastersParentByTilde = master + "~"; + ObjectId resolvedByTilde = objectUnderTest.resolve(mastersParentByTilde); + assertThat(resolvedByTilde).isEqualTo(first); + assertThat(resolvedByTilde).isEqualTo(repo().resolve(mastersParent)); + + assertThat(cache.cacheCalled).isEqualTo(0); + } + + @Test + public void shouldNotResolveSha1sFromCache() throws Exception { + String master = RefNames.fullName("master"); + RevCommit first = tr.update(master, tr.commit().add("first", "foo").create()); + String tag = "test_tag"; + String fullTag = RefNames.REFS_TAGS + tag; + tr.update(fullTag, tr.tag(tag, first)); + RevCommit second = tr.update(master, tr.commit().parent(first).add("second", "foo").create()); + + String secondAbbreviatedName = second.getName().substring(0, 6); + assertThat(objectUnderTest.resolve(second.name())).isEqualTo(repo().resolve(second.name())); + assertThat(objectUnderTest.resolve(secondAbbreviatedName)) + .isEqualTo(repo().resolve(secondAbbreviatedName)); + String parentRevString = second.name() + "^"; + String resolvedName = objectUnderTest.resolve(parentRevString).getName(); + assertThat(resolvedName).isEqualTo(first.getName()); + assertThat(resolvedName).isEqualTo(repo().resolve(parentRevString).getName()); + String ensureIdIsCommit = secondAbbreviatedName + "(commit)"; + assertThat(objectUnderTest.resolve(ensureIdIsCommit)) + .isEqualTo(repo().resolve(ensureIdIsCommit)); + + assertThat(cache.cacheCalled).isEqualTo(0); + } + + @Test + public void shouldNotResolveTagAndSha1FromCache() throws Exception { + String master = RefNames.fullName("master"); + RevCommit first = tr.update(master, tr.commit().add("first", "foo").create()); + String tag = "test_tag"; + tr.update(RefNames.REFS_TAGS + tag, tr.tag(tag, first)); + RevCommit second = tr.update(master, tr.commit().parent(first).add("second", "foo").create()); + + String tagAndSha = tag + "-1-g" + second.getName().substring(0, 6); + assertThat(objectUnderTest.resolve(tagAndSha)).isEqualTo(repo().resolve(tagAndSha)); + + assertThat(cache.cacheCalled).isEqualTo(0); + } + + private Repository repo() { + return tr.getRepository(); + } + + private CachedRefRepository createCachedRepository(Repository repo) { + cache = new TestRefByNameCacheImpl(CacheBuilder.newBuilder().build()); + RefByNameCacheWrapper wrapper = + new RefByNameCacheWrapper(DynamicItem.itemOf(RefByNameCache.class, cache)); + CachedRefDatabase.Factory refDbFactory = + new CachedRefDatabase.Factory() { + @Override + public CachedRefDatabase create(CachedRefRepository repo, RefDatabase delegate) { + return new CachedRefDatabase(wrapper, null, null, null, repo, delegate); + } + }; + return new CachedRefRepository(refDbFactory, null, null, "repo", repo); + } + + private static class TestRefByNameCacheImpl extends RefByNameCacheImpl { + private int cacheCalled; + + private TestRefByNameCacheImpl(Cache<String, Optional<Ref>> refByName) { + super(refByName); + cacheCalled = 0; + } + + @Override + public Ref computeIfAbsent( + String identifier, String ref, Callable<? extends Optional<Ref>> loader) { + cacheCalled++; + return super.computeIfAbsent(identifier, ref, loader); + } + } +}