Add a new core.usePerRequestRefCache setting If the setting is true, use a per request (currently per request thread) ref cache. The ref cache uses JGit's SnapshottingRefDirectory to ensure that packed refs are checked and potentially read at least once per request (lazily) if needed. This helps reduce the overhead of checking if the packed-refs file is outdated. This setting is true by default. Release-Notes: Introduce a per request ref cache for better performance Change-Id: I174fbfdb956fc10db77ce2e1431bd47f02f69531
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index 520855c..3726e544 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt
@@ -2059,6 +2059,16 @@ + Default is 1 hour. +[[core.usePerRequestRefCache]]core.usePerRequestRefCache:: ++ +Use a per request (currently per request thread) ref cache. The ref +cache uses JGit's SnapshottingRefDirectory to ensure that packed +refs are checked and potentially read at least once per request +(lazily) if needed. This helps reduce the overhead of checking if +the packed-refs file is outdated. ++ +Default is true. + [[dashboard]] === Section dashboard
diff --git a/java/com/google/gerrit/server/cache/PerThreadRefDbCache.java b/java/com/google/gerrit/server/cache/PerThreadRefDbCache.java new file mode 100644 index 0000000..a0526e1 --- /dev/null +++ b/java/com/google/gerrit/server/cache/PerThreadRefDbCache.java
@@ -0,0 +1,45 @@ +// 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.server.cache.PerThreadCache; +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import org.eclipse.jgit.internal.storage.file.RefDirectory; +import org.eclipse.jgit.lib.RefDatabase; + +/** A per request thread cache of RefDatabases by directory (Project). */ +public class PerThreadRefDbCache { + protected static final PerThreadCache.Key<PerThreadRefDbCache> REFDB_CACHE_KEY = + PerThreadCache.Key.create(PerThreadRefDbCache.class); + + public static RefDatabase getRefDatabase(File path, RefDatabase refDb) { + if (PerThreadCache.get() != null) { + return PerThreadCache.get() + .get(REFDB_CACHE_KEY, PerThreadRefDbCache::new) + .computeIfAbsent(path, p -> ((RefDirectory) refDb).createSnapshottingRefDirectory()); + } + return refDb; + } + + protected final Map<File, RefDatabase> refDbByRefsDir = new HashMap<>(); + + public RefDatabase computeIfAbsent( + File path, Function<? super File, ? extends RefDatabase> mappingFunction) { + return refDbByRefsDir.computeIfAbsent(path, mappingFunction); + } +}
diff --git a/java/com/google/gerrit/server/git/DynamicRefDbRepository.java b/java/com/google/gerrit/server/git/DynamicRefDbRepository.java new file mode 100644 index 0000000..2e81ad4 --- /dev/null +++ b/java/com/google/gerrit/server/git/DynamicRefDbRepository.java
@@ -0,0 +1,83 @@ +// 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 java.io.File; +import java.io.IOException; +import java.util.function.BiFunction; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryCache; +import org.eclipse.jgit.util.FS; + +/** A FileRepository with a dynamic RefDatabase supplied via a BiFunction. */ +public class DynamicRefDbRepository extends FileRepository { + public static class FileKey extends RepositoryCache.FileKey { + private BiFunction<File, RefDatabase, RefDatabase> refDatabaseSupplier; + + public static FileKey lenient( + File directory, FS fs, BiFunction<File, RefDatabase, RefDatabase> refDatabaseSupplier) { + final File gitdir = resolve(directory, fs); + return new FileKey(gitdir != null ? gitdir : directory, fs, refDatabaseSupplier); + } + + private final FS fs; + + /** + * @param directory exact location of the repository. + * @param fs the file system abstraction which will be necessary to perform certain file system + * operations. + */ + public FileKey( + File directory, FS fs, BiFunction<File, RefDatabase, RefDatabase> refDatabaseSupplier) { + super(canonical(directory), fs); + this.fs = fs; + this.refDatabaseSupplier = refDatabaseSupplier; + } + + @Override + public Repository open(boolean mustExist) throws IOException { + if (mustExist && !isGitRepository(getFile(), fs)) + throw new RepositoryNotFoundException(getFile()); + return new DynamicRefDbRepository(getFile(), refDatabaseSupplier); + } + + private static File canonical(File path) { + try { + return path.getCanonicalFile(); + } catch (IOException e) { + return path.getAbsoluteFile(); + } + } + } + + private final File path; + private final BiFunction<File, RefDatabase, RefDatabase> refDatabaseSupplier; + + public DynamicRefDbRepository( + File path, BiFunction<File, RefDatabase, RefDatabase> refDatabaseSupplier) + throws IOException { + super(path); + this.path = path; + this.refDatabaseSupplier = refDatabaseSupplier; + } + + @Override + public RefDatabase getRefDatabase() { + return refDatabaseSupplier.apply(path, super.getRefDatabase()); + } +}
diff --git a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java index 57d37fa..5cf7de5 100644 --- a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java +++ b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -112,6 +112,7 @@ private final Path basePath; private final Map<Project.NameKey, FileKey> fileKeyByProject = new ConcurrentHashMap<>(); + private final boolean usePerRequestRefCache; @Inject LocalDiskRepositoryManager(SitePaths site, @GerritServerConfig Config cfg) { @@ -119,6 +120,7 @@ if (basePath == null) { throw new IllegalStateException("gerrit.basePath must be configured"); } + usePerRequestRefCache = cfg.getBoolean("core", null, "usePerRequestRefCache", true); } /** @@ -168,7 +170,13 @@ if (isUnreasonableName(name)) { throw new RepositoryNotFoundException("Invalid name: " + name); } - FileKey location = FileKey.lenient(getBasePath(name).resolve(name.get()).toFile(), FS.DETECTED); + FileKey location = + usePerRequestRefCache + ? DynamicRefDbRepository.FileKey.lenient( + getBasePath(name).resolve(name.get()).toFile(), + FS.DETECTED, + (path, refDb) -> PerThreadRefDbCache.getRefDatabase(path, refDb)) + : FileKey.lenient(getBasePath(name).resolve(name.get()).toFile(), FS.DETECTED); try { Repository repo = RepositoryCache.open(location); fileKeyByProject.put(name, location);