Merge "Fix threading issue in Diff cache"
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
index a735bd2..2b856fb 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
@@ -43,6 +43,7 @@
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.patch.DiffExecutor;
import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.util.git.CloseablePool;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
@@ -68,7 +69,6 @@
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.util.io.DisabledOutputStream;
@@ -207,8 +207,7 @@
.collect(Collectors.groupingBy(GitFileDiffCacheKey::project));
for (Map.Entry<Project.NameKey, List<GitFileDiffCacheKey>> entry : byProject.entrySet()) {
- try (Repository repo = repoManager.openRepository(entry.getKey());
- ObjectReader reader = repo.newObjectReader()) {
+ try (Repository repo = repoManager.openRepository(entry.getKey())) {
// Grouping keys by diff options because each group of keys will be processed with a
// separate call to JGit using the DiffFormatter object.
@@ -217,7 +216,7 @@
for (Map.Entry<DiffOptions, List<GitFileDiffCacheKey>> group :
optionsGroups.entrySet()) {
- result.putAll(loadAllImpl(repo, reader, group.getKey(), group.getValue()));
+ result.putAll(loadAllImpl(repo, group.getKey(), group.getValue()));
}
}
}
@@ -232,42 +231,46 @@
* @return The git file diffs for all input keys.
*/
private Map<GitFileDiffCacheKey, GitFileDiff> loadAllImpl(
- Repository repo, ObjectReader reader, DiffOptions options, List<GitFileDiffCacheKey> keys)
+ Repository repo, DiffOptions options, List<GitFileDiffCacheKey> keys)
throws IOException, DiffNotAvailableException {
ImmutableMap.Builder<GitFileDiffCacheKey, GitFileDiff> result =
ImmutableMap.builderWithExpectedSize(keys.size());
Map<GitFileDiffCacheKey, String> filePaths =
keys.stream().collect(Collectors.toMap(identity(), GitFileDiffCacheKey::newFilePath));
- DiffFormatter formatter = createDiffFormatter(options, repo, reader);
- ListMultimap<String, DiffEntry> diffEntries =
- loadDiffEntries(formatter, options, filePaths.values());
- for (GitFileDiffCacheKey key : filePaths.keySet()) {
- String newFilePath = filePaths.get(key);
- if (!diffEntries.containsKey(newFilePath)) {
- result.put(
- key,
- GitFileDiff.empty(
- AbbreviatedObjectId.fromObjectId(key.oldTree()),
- AbbreviatedObjectId.fromObjectId(key.newTree()),
- newFilePath));
- continue;
+ try (CloseablePool<DiffFormatter> diffPool =
+ new CloseablePool<>(() -> createDiffFormatter(options, repo))) {
+ ListMultimap<String, DiffEntry> diffEntries;
+ try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) {
+ diffEntries = loadDiffEntries(formatter.get(), options, filePaths.values());
}
- List<DiffEntry> entries = diffEntries.get(newFilePath);
- if (entries.size() == 1) {
- result.put(key, createGitFileDiff(entries.get(0), formatter, key));
- } else {
- // Handle when JGit returns two {Added, Deleted} entries for the same file. This happens,
- // for example, when a file's mode is changed between patchsets (e.g. converting a
- // symlink to a regular file). We combine both diff entries into a single entry with
- // {changeType = Rewrite}.
- List<GitFileDiff> gitDiffs = new ArrayList<>();
- for (DiffEntry entry : diffEntries.get(newFilePath)) {
- gitDiffs.add(createGitFileDiff(entry, formatter, key));
+ for (GitFileDiffCacheKey key : filePaths.keySet()) {
+ String newFilePath = filePaths.get(key);
+ if (!diffEntries.containsKey(newFilePath)) {
+ result.put(
+ key,
+ GitFileDiff.empty(
+ AbbreviatedObjectId.fromObjectId(key.oldTree()),
+ AbbreviatedObjectId.fromObjectId(key.newTree()),
+ newFilePath));
+ continue;
}
- result.put(key, createRewriteEntry(gitDiffs));
+ List<DiffEntry> entries = diffEntries.get(newFilePath);
+ if (entries.size() == 1) {
+ result.put(key, createGitFileDiff(entries.get(0), key, diffPool));
+ } else {
+ // Handle when JGit returns two {Added, Deleted} entries for the same file. This
+ // happens, for example, when a file's mode is changed between patchsets (e.g.
+ // converting a symlink to a regular file). We combine both diff entries into a single
+ // entry with {changeType = Rewrite}.
+ List<GitFileDiff> gitDiffs = new ArrayList<>();
+ for (DiffEntry entry : diffEntries.get(newFilePath)) {
+ gitDiffs.add(createGitFileDiff(entry, key, diffPool));
+ }
+ result.put(key, createRewriteEntry(gitDiffs));
+ }
}
+ return result.build();
}
- return result.build();
}
private static ListMultimap<String, DiffEntry> loadDiffEntries(
@@ -288,10 +291,9 @@
MultimapBuilder.treeKeys().arrayListValues()::build));
}
- private static DiffFormatter createDiffFormatter(
- DiffOptions diffOptions, Repository repo, ObjectReader reader) {
+ private static DiffFormatter createDiffFormatter(DiffOptions diffOptions, Repository repo) {
try (DiffFormatter diffFormatter = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
- diffFormatter.setReader(reader, repo.getConfig());
+ diffFormatter.setRepository(repo);
RawTextComparator cmp = comparatorFor(diffOptions.whitespace());
diffFormatter.setDiffComparator(cmp);
if (diffOptions.renameScore() != -1) {
@@ -334,25 +336,31 @@
* timeout enforcement.
*/
private GitFileDiff createGitFileDiff(
- DiffEntry diffEntry, DiffFormatter formatter, GitFileDiffCacheKey key) throws IOException {
+ DiffEntry diffEntry, GitFileDiffCacheKey key, CloseablePool<DiffFormatter> diffPool)
+ throws IOException {
if (!key.useTimeout()) {
- FileHeader fileHeader = formatter.toFileHeader(diffEntry);
- return GitFileDiff.create(diffEntry, fileHeader);
+ try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) {
+ FileHeader fileHeader = formatter.get().toFileHeader(diffEntry);
+ return GitFileDiff.create(diffEntry, fileHeader);
+ }
}
- Future<FileHeader> fileHeaderFuture =
+ // This submits the DiffFormatter to a different thread. The CloseablePool and our usage of it
+ // ensures that any DiffFormatter instance and the ObjectReader it references internally is
+ // only used by a single thread concurrently. However, ObjectReaders have a reference to
+ // Repository which might not be thread safe (FileRepository is, DfsRepository might not).
+ // This could lead to a race condition.
+ Future<GitFileDiff> fileDiffFuture =
diffExecutor.submit(
() -> {
- synchronized (diffEntry) {
- return formatter.toFileHeader(diffEntry);
+ try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) {
+ return GitFileDiff.create(diffEntry, formatter.get().toFileHeader(diffEntry));
}
});
try {
// We employ the timeout because of a bug in Myers diff in JGit. See
// bugs.chromium.org/p/gerrit/issues/detail?id=487 for more details. The bug may happen
// if the algorithm used in diffs is HISTOGRAM_WITH_FALLBACK_MYERS.
- fileHeaderFuture.get(timeoutMillis, TimeUnit.MILLISECONDS);
- FileHeader fileHeader = formatter.toFileHeader(diffEntry);
- return GitFileDiff.create(diffEntry, fileHeader);
+ return fileDiffFuture.get(timeoutMillis, TimeUnit.MILLISECONDS);
} catch (InterruptedException | TimeoutException e) {
// If timeout happens, create a negative result
metrics.timeouts.increment();
diff --git a/java/com/google/gerrit/server/util/git/BUILD b/java/com/google/gerrit/server/util/git/BUILD
index 4f4ba83..bbc6bf0 100644
--- a/java/com/google/gerrit/server/util/git/BUILD
+++ b/java/com/google/gerrit/server/util/git/BUILD
@@ -7,5 +7,6 @@
deps = [
"//java/com/google/gerrit/entities",
"//lib:jgit",
+ "//lib/flogger:api",
],
)
diff --git a/java/com/google/gerrit/server/util/git/CloseablePool.java b/java/com/google/gerrit/server/util/git/CloseablePool.java
new file mode 100644
index 0000000..442bd09
--- /dev/null
+++ b/java/com/google/gerrit/server/util/git/CloseablePool.java
@@ -0,0 +1,116 @@
+// 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.google.gerrit.server.util.git;
+
+import com.google.common.flogger.FluentLogger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Pool to manage resources that need to be closed but to whom we might lose the reference to or
+ * where closing resources individually is not always possible.
+ *
+ * <p>This pool can be used when we want to reuse closable resources in a multithreaded context.
+ * Example:
+ *
+ * <pre>{@code
+ * try (CloseablePool<T> pool = new CloseablePool(() -> new T())) {
+ * for (int i = 0; i < 100; i++) {
+ * executor.submit(() -> {
+ * try (CloseablePool<T>.Handle handle = pool.get()) {
+ * // Do work that might potentially take longer than the timeout.
+ * handle.get(); // pooled instance to be used
+ * }
+ * }).get(1000, MILLISECONDS);
+ * }
+ * }
+ * }</pre>
+ */
+public class CloseablePool<T extends AutoCloseable> implements AutoCloseable {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final Supplier<T> tCreator;
+ private List<T> ts;
+
+ /**
+ * Instantiate a new pool. The {@link Supplier} must be capable of creating a new instance on
+ * every call.
+ */
+ public CloseablePool(Supplier<T> tCreator) {
+ this.ts = new ArrayList<>();
+ this.tCreator = tCreator;
+ }
+
+ /**
+ * Get a shared instance or create a new instance. Close the returned handle to return it to the
+ * pool.
+ */
+ public synchronized Handle get() {
+ if (ts.isEmpty()) {
+ return new Handle(tCreator.get());
+ }
+ return new Handle(ts.remove(ts.size() - 1));
+ }
+
+ private synchronized boolean discard(T t) {
+ if (ts != null) {
+ ts.add(t);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public synchronized void close() {
+ for (T t : ts)
+ try {
+ t.close();
+ } catch (Exception e) {
+ logger.atWarning().withCause(e).log(
+ "Failed to close resource %s in CloseablePool %s", t, this);
+ }
+ ts = null;
+ }
+
+ /**
+ * Wrapper around an {@link AutoCloseable}. Will try to return the resource to the pool and close
+ * it in case the pool was already closed.
+ */
+ public class Handle implements AutoCloseable {
+ private final T t;
+
+ private Handle(T t) {
+ this.t = t;
+ }
+
+ /** Returns the managed instance. */
+ public T get() {
+ return t;
+ }
+
+ @Override
+ public void close() {
+ if (!discard(t)) {
+ try {
+ t.close();
+ } catch (Exception e) {
+ logger.atWarning().withCause(e).log(
+ "Failed to close resource %s in CloseablePool %s", this, CloseablePool.this);
+ }
+ }
+ }
+ }
+}