diff options
author | Luca Milanesio <luca.milanesio@gmail.com> | 2022-05-20 10:30:53 +0100 |
---|---|---|
committer | Luca Milanesio <luca.milanesio@gmail.com> | 2022-05-20 13:19:23 +0100 |
commit | e53caf14b22384d92db0c7520c70ebcb37e7025e (patch) | |
tree | 4221853f3bda92fe01a21826b6f081b6c8d3fb8d | |
parent | 462bb1ed2f483734115c78bc638a3f8ba0dc558e (diff) | |
parent | 5003460963903cfe6294f27b5836ad4df657ea94 (diff) |
Merge branch 'stable-2.16' into stable-3.0
* stable-2.16:
Set PerThreadCache as readonly after creating a new patch-set
Set PerThreadCache as readonly when formatting change e-mails
Set PerThreadCache as readonly when formatting change JSON
Set PerThreadCache as readonly after deleting a change
Set PerThreadCache as readonly after abandoning a change
Set PerThreadCache as readonly after merging a change
Set PerThreadCache as readonly after posting review comments
Introduce unloaders on PerThreadCache entries
RepoRefCache: Hold a reference to the refDatabase with ref counting
Remove use of RefCache in ChangeNotes
Cache change /meta ref SHA1 for each change indexing task
Also fix formatting of:
java/com/google/gerrit/acceptance/GerritServer.java
java/com/google/gerrit/server/mail/send/OutgoingEmail.java
Release-Notes: skip
Change-Id: I7fab22d2bfae52294b0f7d237a0d26b9fafa54d6
21 files changed, 1142 insertions, 244 deletions
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java index f05c598b07..2328697127 100644 --- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java +++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java @@ -107,6 +107,7 @@ import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.PluginConfigFactory; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.RepoRefCache; import com.google.gerrit.server.git.meta.MetaDataUpdate; import com.google.gerrit.server.group.InternalGroup; import com.google.gerrit.server.group.SystemGroupBackend; @@ -181,6 +182,7 @@ import org.eclipse.jgit.transport.URIish; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Rule; import org.junit.rules.ExpectedException; @@ -194,6 +196,7 @@ import org.junit.runners.model.Statement; public abstract class AbstractDaemonTest { private static GerritServer commonServer; private static Description firstTest; + private static String perThreadCacheCheckStaleEntry; @ClassRule public static TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -295,6 +298,12 @@ public abstract class AbstractDaemonTest { private ProjectResetter resetter; private List<Repository> toClose; + @BeforeClass + public static void enablePerThreadCacheStalenessCheck() { + perThreadCacheCheckStaleEntry = + System.setProperty(RepoRefCache.REPO_REF_CACHE_CHECK_STALE_ENTRIES_PROPERTY, "true"); + } + @Before public void clearSender() { sender.clear(); @@ -336,6 +345,16 @@ public abstract class AbstractDaemonTest { } } + @AfterClass + public static void disablePerThreadCacheStalenessCheck() { + if (perThreadCacheCheckStaleEntry == null) { + System.clearProperty(RepoRefCache.REPO_REF_CACHE_CHECK_STALE_ENTRIES_PROPERTY); + } else { + System.setProperty( + RepoRefCache.REPO_REF_CACHE_CHECK_STALE_ENTRIES_PROPERTY, perThreadCacheCheckStaleEntry); + } + } + /** Controls which project and branches should be reset after each test case. */ protected ProjectResetter.Config resetProjects() { return new ProjectResetter.Config() diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java index 34f514c590..63ea4e3ce3 100644 --- a/java/com/google/gerrit/acceptance/GerritServer.java +++ b/java/com/google/gerrit/acceptance/GerritServer.java @@ -40,8 +40,8 @@ import com.google.gerrit.server.config.GerritRuntime; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePath; import com.google.gerrit.server.git.receive.AsyncReceiveCommits; -import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore; import com.google.gerrit.server.index.AutoFlush; +import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore; import com.google.gerrit.server.ssh.NoSshModule; import com.google.gerrit.server.util.SocketUtil; import com.google.gerrit.server.util.SystemLog; diff --git a/java/com/google/gerrit/server/cache/PerThreadCache.java b/java/com/google/gerrit/server/cache/PerThreadCache.java index 609fce7b25..9b04561dfd 100644 --- a/java/com/google/gerrit/server/cache/PerThreadCache.java +++ b/java/com/google/gerrit/server/cache/PerThreadCache.java @@ -17,12 +17,19 @@ package com.google.gerrit.server.cache; import static com.google.common.base.Preconditions.checkState; import com.google.common.base.Objects; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.gerrit.common.Nullable; +import com.google.gerrit.server.git.RefCache; +import java.util.Collection; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; /** @@ -60,10 +67,51 @@ public class PerThreadCache implements AutoCloseable { private static final int PER_THREAD_CACHE_SIZE = 25; /** - * Optional HTTP request associated with the per-thread cache, should the thread be associated - * with the incoming HTTP thread pool. + * System property for disabling caching specific key types. TODO: DO NOT MERGE into stable-3.2 + * onwards. */ - private final Optional<HttpServletRequest> httpRequest; + public static final String PER_THREAD_CACHE_DISABLED_TYPES_PROPERTY = + "PerThreadCache_disabledTypes"; + + /** + * True when the current thread is associated with an incoming API request that is not changing + * any repository /meta refs and therefore caching repo refs is safe. TODO: DO NOT MERGE into + * stable-3.2 onwards. + */ + private boolean allowRefCache; + + /** + * Sets the request status flag to read-only temporarily. TODO: DO NOT MERGE into stable-3.2 + * onwards. + */ + public interface ReadonlyRequestWindow extends AutoCloseable { + + /** + * Close the request read-only status, restoring the previous value. + * + * <p>NOTE: If the previous status was not read-only, the cache is getting cleared for making + * sure that all potential stale entries coming from a read-only windows are cleared. + */ + @Override + default void close() {} + } + + private class ReadonlyRequestWindowImpl implements ReadonlyRequestWindow { + private final boolean oldAllowRepoRefsCache; + + private ReadonlyRequestWindowImpl() { + oldAllowRepoRefsCache = allowRefCache(); + allowRefCache(true); + } + + @Override + public void close() { + allowRefCache(oldAllowRepoRefsCache); + } + } + + private final Map<Key<?>, Consumer<Object>> unloaders = + Maps.newHashMapWithExpectedSize(PER_THREAD_CACHE_SIZE); /** * Unique key for key-value mappings stored in PerThreadCache. The key is based on the value's @@ -110,9 +158,29 @@ public class PerThreadCache implements AutoCloseable { } } + /** + * Creates a thread-local cache associated to an incoming HTTP request. + * + * <p>The request is considered as read-only if the associated method is GET or HEAD. + * + * @param httpRequest HTTP request associated with the thread-local cache + * @return thread-local cache + */ public static PerThreadCache create(@Nullable HttpServletRequest httpRequest) { checkState(CACHE.get() == null, "called create() twice on the same request"); - PerThreadCache cache = new PerThreadCache(httpRequest); + PerThreadCache cache = new PerThreadCache(httpRequest, false); + CACHE.set(cache); + return cache; + } + + /** + * Creates a thread-local cache associated to an incoming read-only request. + * + * @return thread-local cache + */ + public static PerThreadCache createReadOnly() { + checkState(CACHE.get() == null, "called create() twice on the same request"); + PerThreadCache cache = new PerThreadCache(null, true); CACHE.set(cache); return cache; } @@ -122,48 +190,159 @@ public class PerThreadCache implements AutoCloseable { return CACHE.get(); } + /** + * Return a cached value associated with a key fetched with a loader and released with an unloader + * function. + * + * @param <T> The data type of the cached value + * @param key the key associated with the value + * @param loader the loader function for fetching the value from the key + * @param unloader the unloader function for releasing the value when unloaded from the cache + * @return Optional of the cached value or empty if the value could not be cached for any reason + * (e.g. cache full) + */ + public static <T> Optional<T> get(Key<T> key, Supplier<T> loader, Consumer<T> unloader) { + return Optional.ofNullable(get()).flatMap(c -> c.getWithLoader(key, loader, unloader)); + } + + /** + * Legacy way for retrieving a cached element through a loader. + * + * <p>This method is deprecated because it was error-prone due to the unclear ownership of the + * objects created through the loader. When the cache has space available, the entries are loaded + * and cached, hence owned and reused by the cache. + * + * <p>When the cache is full, this method just short-circuit to the invocation of the loader and + * the objects created aren't owned or stored by the cache, leaving the space for potential memory + * and resources leaks. + * + * <p>Because of the unclear semantics of the method (who owns the instances? are they reused?) + * this is now deprecated the the caller should use instead the {@link PerThreadCache#get(Key, + * Supplier, Consumer)} which has a clear ownership policy. + * + * @deprecated use {@link PerThreadCache#get(Key, Supplier, Consumer)} + */ public static <T> T getOrCompute(Key<T> key, Supplier<T> loader) { PerThreadCache cache = get(); return cache != null ? cache.get(key, loader) : loader.get(); } private final Map<Key<?>, Object> cache = Maps.newHashMapWithExpectedSize(PER_THREAD_CACHE_SIZE); + private final ImmutableSet<String> disabledTypes; - private PerThreadCache(@Nullable HttpServletRequest req) { - httpRequest = Optional.ofNullable(req); + private PerThreadCache(@Nullable HttpServletRequest req, boolean alwaysCacheRepoRefs) { + disabledTypes = + ImmutableSet.copyOf( + Splitter.on(',') + .split(System.getProperty(PER_THREAD_CACHE_DISABLED_TYPES_PROPERTY, ""))); + + allowRefCache = + alwaysCacheRepoRefs + || (req != null + && (req.getMethod().equalsIgnoreCase("GET") + || req.getMethod().equalsIgnoreCase("HEAD"))); } /** - * Returns an instance of {@code T} that was either loaded from the cache or obtained from the - * provided {@link Supplier}. + * Legacy way of retrieving an instance of {@code T} that was either loaded from the cache or + * obtained from the provided {@link Supplier}. + * + * <p>This method is deprecated because it was error-prone due to the unclear ownership of the + * objects created through the loader. When the cache has space available, the entries are loaded + * and cached, hence owned and reused by the cache. + * + * <p>When the cache is full, this method just short-circuit to the invocation of the loader and + * the objects created aren't owned or stored by the cache, leaving the space for potential memory + * and resources leaks. + * + * <p>Because of the unclear semantics of the method (who owns the instances? are they reused?) + * this is now deprecated the the caller should use instead the {@link PerThreadCache#get(Key, + * Supplier, Consumer)} which has a clear ownership policy. + * + * @deprecated use {@link PerThreadCache#getWithLoader(Key, Supplier, Consumer)} */ public <T> T get(Key<T> key, Supplier<T> loader) { - @SuppressWarnings("unchecked") + return getWithLoader(key, loader, null).orElse(loader.get()); + } + + @SuppressWarnings("unchecked") + public <T> Optional<T> getWithLoader( + Key<T> key, Supplier<T> loader, @Nullable Consumer<T> unloader) { + if (disabledTypes.contains(key.clazz.getCanonicalName())) { + return Optional.empty(); + } + T value = (T) cache.get(key); - if (value == null) { + if (value == null && cache.size() < PER_THREAD_CACHE_SIZE) { value = loader.get(); - if (cache.size() < PER_THREAD_CACHE_SIZE) { - cache.put(key, value); + cache.put(key, value); + if (unloader != null) { + unloaders.put(key, (Consumer<Object>) unloader); } } - return value; + return Optional.ofNullable(value); } - /** Returns the optional HTTP request associated with the local thread cache. */ - public Optional<HttpServletRequest> getHttpRequest() { - return httpRequest; + /** Returns an instance of {@code T} that is already loaded from the cache or null otherwise. */ + @SuppressWarnings("unchecked") + public <T> T get(Key<T> key) { + return (T) cache.get(key); } - /** Returns true if there is an HTTP request associated and is a GET or HEAD */ - public boolean hasReadonlyRequest() { - return httpRequest - .map(HttpServletRequest::getMethod) - .filter(m -> m.equalsIgnoreCase("GET") || m.equalsIgnoreCase("HEAD")) - .isPresent(); + /** + * Returns true if the associated request is read-only and therefore the repo refs are safe to be + * cached + */ + public boolean allowRefCache() { + return allowRefCache; + } + + /** + * Set the cache read-only request status temporarily, for enabling caching of all entries. + * + * @return {@link ReadonlyRequestWindow} associated with the incoming request + */ + public static ReadonlyRequestWindow openReadonlyRequestWindow() { + PerThreadCache perThreadCache = CACHE.get(); + return perThreadCache == null + ? new ReadonlyRequestWindow() {} + : perThreadCache.new ReadonlyRequestWindowImpl(); } @Override public void close() { + unload(unloaders.entrySet()); CACHE.remove(); } + + private void unload(Collection<Entry<Key<?>, Consumer<Object>>> entriesToUnload) { + ImmutableSet<Entry<Key<?>, Consumer<Object>>> toUnload = ImmutableSet.copyOf(entriesToUnload); + try { + toUnload.stream().forEach(this::unload); + } finally { + toUnload.stream() + .forEach( + e -> { + cache.remove(e.getKey()); + unloaders.remove(e.getKey()); + }); + } + } + + private <T> void unload(Entry<Key<?>, Consumer<Object>> unloaderEntry) { + Object valueToUnload = cache.get(unloaderEntry.getKey()); + unloaderEntry.getValue().accept(valueToUnload); + cache.remove(unloaderEntry.getKey()); + } + + private void allowRefCache(boolean allowed) { + allowRefCache = allowed; + + if (!allowRefCache) { + unload( + unloaders.entrySet().stream() + .filter(e -> RefCache.class.isAssignableFrom(e.getKey().clazz)) + .collect(Collectors.toSet())); + } + } } diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java index 1008a307d3..8d57992ec8 100644 --- a/java/com/google/gerrit/server/change/ChangeJson.java +++ b/java/com/google/gerrit/server/change/ChangeJson.java @@ -84,6 +84,8 @@ import com.google.gerrit.server.ReviewerStatusUpdate; import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.account.AccountInfoComparator; import com.google.gerrit.server.account.AccountLoader; +import com.google.gerrit.server.cache.PerThreadCache; +import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.TrackingFooters; import com.google.gerrit.server.index.change.ChangeField; @@ -489,7 +491,8 @@ public class ChangeJson { private <I extends ChangeInfo> I toChangeInfo( ChangeData cd, Optional<PatchSet.Id> limitToPsId, Supplier<I> changeInfoSupplier) throws PatchListNotAvailableException, GpgException, PermissionBackendException, IOException { - try (Timer0.Context ignored = metrics.toChangeInfoLatency.start()) { + try (Timer0.Context ignored = metrics.toChangeInfoLatency.start(); + ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { return toChangeInfoImpl(cd, limitToPsId, changeInfoSupplier); } } diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java index f6f1d57850..5826dc7e39 100644 --- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java +++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java @@ -47,6 +47,8 @@ import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.cache.PerThreadCache; +import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow; import com.google.gerrit.server.data.AccountAttribute; import com.google.gerrit.server.data.ApprovalAttribute; import com.google.gerrit.server.data.ChangeAttribute; @@ -273,14 +275,18 @@ public class StreamEventsApiListener @Override public void onRevisionCreated(RevisionCreatedListener.Event ev) { try { - ChangeNotes notes = getNotes(ev.getChange()); - Change change = notes.getChange(); - PatchSet patchSet = getPatchSet(notes, ev.getRevision()); - PatchSetCreatedEvent event = new PatchSetCreatedEvent(change); - - event.change = changeAttributeSupplier(change, notes); - event.patchSet = patchSetAttributeSupplier(change, patchSet); - event.uploader = accountAttributeSupplier(ev.getWho()); + Change change; + PatchSetCreatedEvent event; + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + ChangeNotes notes = getNotes(ev.getChange()); + change = notes.getChange(); + PatchSet patchSet = getPatchSet(notes, ev.getRevision()); + event = new PatchSetCreatedEvent(change); + + event.change = changeAttributeSupplier(change, notes); + event.patchSet = patchSetAttributeSupplier(change, patchSet); + event.uploader = accountAttributeSupplier(ev.getWho()); + } dispatcher.run(d -> d.postEvent(change, event)); } catch (StorageException e) { @@ -374,17 +380,24 @@ public class StreamEventsApiListener @Override public void onCommentAdded(CommentAddedListener.Event ev) { - try { - ChangeNotes notes = getNotes(ev.getChange()); - Change change = notes.getChange(); - PatchSet ps = getPatchSet(notes, ev.getRevision()); - CommentAddedEvent event = new CommentAddedEvent(change); + Change change; - event.change = changeAttributeSupplier(change, notes); - event.author = accountAttributeSupplier(ev.getWho()); - event.patchSet = patchSetAttributeSupplier(change, ps); - event.comment = ev.getComment(); - event.approvals = approvalsAttributeSupplier(change, ev.getApprovals(), ev.getOldApprovals()); + try { + CommentAddedEvent event; + + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + ChangeNotes notes = getNotes(ev.getChange()); + change = notes.getChange(); + PatchSet ps = getPatchSet(notes, ev.getRevision()); + event = new CommentAddedEvent(change); + + event.change = changeAttributeSupplier(change, notes); + event.author = accountAttributeSupplier(ev.getWho()); + event.patchSet = patchSetAttributeSupplier(change, ps); + event.comment = ev.getComment(); + event.approvals = + approvalsAttributeSupplier(change, ev.getApprovals(), ev.getOldApprovals()); + } dispatcher.run(d -> d.postEvent(change, event)); } catch (StorageException e) { @@ -413,14 +426,18 @@ public class StreamEventsApiListener @Override public void onChangeMerged(ChangeMergedListener.Event ev) { try { - ChangeNotes notes = getNotes(ev.getChange()); - Change change = notes.getChange(); - ChangeMergedEvent event = new ChangeMergedEvent(change); - - event.change = changeAttributeSupplier(change, notes); - event.submitter = accountAttributeSupplier(ev.getWho()); - event.patchSet = patchSetAttributeSupplier(change, psUtil.current(notes)); - event.newRev = ev.getNewRevisionId(); + ChangeMergedEvent event; + Change change; + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + ChangeNotes notes = getNotes(ev.getChange()); + change = notes.getChange(); + event = new ChangeMergedEvent(change); + + event.change = changeAttributeSupplier(change, notes); + event.submitter = accountAttributeSupplier(ev.getWho()); + event.patchSet = patchSetAttributeSupplier(change, psUtil.current(notes)); + event.newRev = ev.getNewRevisionId(); + } dispatcher.run(d -> d.postEvent(change, event)); } catch (StorageException e) { @@ -431,14 +448,18 @@ public class StreamEventsApiListener @Override public void onChangeAbandoned(ChangeAbandonedListener.Event ev) { try { - ChangeNotes notes = getNotes(ev.getChange()); - Change change = notes.getChange(); - ChangeAbandonedEvent event = new ChangeAbandonedEvent(change); - - event.change = changeAttributeSupplier(change, notes); - event.abandoner = accountAttributeSupplier(ev.getWho()); - event.patchSet = patchSetAttributeSupplier(change, psUtil.current(notes)); - event.reason = ev.getReason(); + ChangeAbandonedEvent event; + Change change; + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + ChangeNotes notes = getNotes(ev.getChange()); + change = notes.getChange(); + event = new ChangeAbandonedEvent(change); + + event.change = changeAttributeSupplier(change, notes); + event.abandoner = accountAttributeSupplier(ev.getWho()); + event.patchSet = patchSetAttributeSupplier(change, psUtil.current(notes)); + event.reason = ev.getReason(); + } dispatcher.run(d -> d.postEvent(change, event)); } catch (StorageException e) { @@ -505,12 +526,17 @@ public class StreamEventsApiListener @Override public void onChangeDeleted(ChangeDeletedListener.Event ev) { try { - ChangeNotes notes = getNotes(ev.getChange()); - Change change = notes.getChange(); - ChangeDeletedEvent event = new ChangeDeletedEvent(change); + Change change; + ChangeDeletedEvent event; - event.change = changeAttributeSupplier(change, notes); - event.deleter = accountAttributeSupplier(ev.getWho()); + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + ChangeNotes notes = getNotes(ev.getChange()); + change = notes.getChange(); + event = new ChangeDeletedEvent(change); + + event.change = changeAttributeSupplier(change, notes); + event.deleter = accountAttributeSupplier(ev.getWho()); + } dispatcher.run(d -> d.postEvent(change, event)); } catch (StorageException e) { diff --git a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java index a8c08b991f..252129b1e9 100644 --- a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java +++ b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java @@ -25,6 +25,8 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.server.GpgException; import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.cache.PerThreadCache; +import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow; import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gerrit.server.patch.PatchListObjectTooLargeException; import com.google.gerrit.server.permissions.PermissionBackendException; @@ -58,14 +60,17 @@ public class ChangeAbandoned { return; } try { - Event event = - new Event( - util.changeInfo(change), - util.revisionInfo(change.getProject(), ps), - util.accountInfo(abandoner), - reason, - when, - notifyHandling); + Event event; + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + event = + new Event( + util.changeInfo(change), + util.revisionInfo(change.getProject(), ps), + util.accountInfo(abandoner), + reason, + when, + notifyHandling); + } listeners.runEach(l -> l.onChangeAbandoned(event)); } catch (PatchListObjectTooLargeException e) { logger.atWarning().log("Couldn't fire event: %s", e.getMessage()); diff --git a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java index 9e3e979196..acb212acb5 100644 --- a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java +++ b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java @@ -22,6 +22,8 @@ import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.events.ChangeDeletedListener; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.cache.PerThreadCache; +import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow; import com.google.gerrit.server.plugincontext.PluginSetContext; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -45,7 +47,10 @@ public class ChangeDeleted { return; } try { - Event event = new Event(util.changeInfo(change), util.accountInfo(deleter), when); + Event event; + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + event = new Event(util.changeInfo(change), util.accountInfo(deleter), when); + } listeners.runEach(l -> l.onChangeDeleted(event)); } catch (StorageException e) { logger.atSevere().withCause(e).log("Couldn't fire event"); diff --git a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java index 756f3832ff..1645333661 100644 --- a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java +++ b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java @@ -25,6 +25,8 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.server.GpgException; import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.cache.PerThreadCache; +import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow; import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gerrit.server.patch.PatchListObjectTooLargeException; import com.google.gerrit.server.permissions.PermissionBackendException; @@ -52,14 +54,18 @@ public class ChangeMerged { if (listeners.isEmpty()) { return; } + try { - Event event = - new Event( - util.changeInfo(change), - util.revisionInfo(change.getProject(), ps), - util.accountInfo(merger), - newRevisionId, - when); + Event event; + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + event = + new Event( + util.changeInfo(change), + util.revisionInfo(change.getProject(), ps), + util.accountInfo(merger), + newRevisionId, + when); + } listeners.runEach(l -> l.onChangeMerged(event)); } catch (PatchListObjectTooLargeException e) { logger.atWarning().log("Couldn't fire event: %s", e.getMessage()); diff --git a/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/java/com/google/gerrit/server/extensions/events/CommentAdded.java index ea9ae312a4..e5d626f16c 100644 --- a/java/com/google/gerrit/server/extensions/events/CommentAdded.java +++ b/java/com/google/gerrit/server/extensions/events/CommentAdded.java @@ -26,6 +26,8 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.server.GpgException; import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.cache.PerThreadCache; +import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow; import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gerrit.server.patch.PatchListObjectTooLargeException; import com.google.gerrit.server.permissions.PermissionBackendException; @@ -61,15 +63,18 @@ public class CommentAdded { return; } try { - Event event = - new Event( - util.changeInfo(change), - util.revisionInfo(change.getProject(), ps), - util.accountInfo(author), - comment, - util.approvals(author, approvals, when), - util.approvals(author, oldApprovals, when), - when); + Event event; + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + event = + new Event( + util.changeInfo(change), + util.revisionInfo(change.getProject(), ps), + util.accountInfo(author), + comment, + util.approvals(author, approvals, when), + util.approvals(author, oldApprovals, when), + when); + } listeners.runEach(l -> l.onCommentAdded(event)); } catch (PatchListObjectTooLargeException e) { logger.atWarning().log("Couldn't fire event: %s", e.getMessage()); diff --git a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java index 6fddcfe4ad..f34e0ac608 100644 --- a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java +++ b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java @@ -25,6 +25,8 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.server.GpgException; import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.cache.PerThreadCache; +import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow; import com.google.gerrit.server.change.NotifyResolver; import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gerrit.server.patch.PatchListObjectTooLargeException; @@ -74,13 +76,16 @@ public class RevisionCreated { return; } try { - Event event = - new Event( - util.changeInfo(change), - util.revisionInfo(change.getProject(), patchSet), - util.accountInfo(uploader), - when, - notify.handling()); + Event event; + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + event = + new Event( + util.changeInfo(change), + util.revisionInfo(change.getProject(), patchSet), + util.accountInfo(uploader), + when, + notify.handling()); + } listeners.runEach(l -> l.onRevisionCreated(event)); } catch (PatchListObjectTooLargeException e) { logger.atWarning().log("Couldn't fire event: %s", e.getMessage()); diff --git a/java/com/google/gerrit/server/git/RefCache.java b/java/com/google/gerrit/server/git/RefCache.java index 5a5cae9332..2dee427bf7 100644 --- a/java/com/google/gerrit/server/git/RefCache.java +++ b/java/com/google/gerrit/server/git/RefCache.java @@ -37,4 +37,7 @@ public interface RefCache { * present with a value of {@link ObjectId#zeroId()}. */ Optional<ObjectId> get(String refName) throws IOException; + + /** Closes this cache, releasing the references to any underlying resources. */ + void close(); } diff --git a/java/com/google/gerrit/server/git/RepoRefCache.java b/java/com/google/gerrit/server/git/RepoRefCache.java index 9086da7fb5..c813b8375b 100644 --- a/java/com/google/gerrit/server/git/RepoRefCache.java +++ b/java/com/google/gerrit/server/git/RepoRefCache.java @@ -14,36 +14,66 @@ package com.google.gerrit.server.git; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.flogger.FluentLogger; import com.google.gerrit.server.cache.PerThreadCache; import java.io.IOException; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.Repository; -/** {@link RefCache} backed directly by a repository. */ +/** + * {@link RefCache} backed directly by a repository. TODO: DO NOT MERGE + * PerThreadCache.CacheStalenessCheck into stable-3.2 onwards. + */ public class RepoRefCache implements RefCache { + + /** + * System property for enabling the check for stale cache entries. TODO: DO NOT MERGE into + * stable-3.2 onwards. + */ + public static final String REPO_REF_CACHE_CHECK_STALE_ENTRIES_PROPERTY = + "RepoRefCache_checkStaleEntries"; + + private static FluentLogger log = FluentLogger.forEnclosingClass(); + private final RefDatabase refdb; private final Map<String, Optional<ObjectId>> ids; + private final Repository repo; + private final boolean checkStaleEntries; + private final AtomicBoolean closed; public static Optional<RefCache> getOptional(Repository repo) { PerThreadCache cache = PerThreadCache.get(); - if (cache != null && cache.hasReadonlyRequest()) { - return Optional.of( - cache.get( - PerThreadCache.Key.create(RepoRefCache.class, repo), () -> new RepoRefCache(repo))); + if (cache != null && cache.allowRefCache()) { + return cache + .getWithLoader( + PerThreadCache.Key.create(RepoRefCache.class, repo), + () -> new RepoRefCache(repo), + RepoRefCache::close) + .map(c -> (RefCache) c); } return Optional.empty(); } public RepoRefCache(Repository repo) { + checkStaleEntries = + Boolean.valueOf(System.getProperty(REPO_REF_CACHE_CHECK_STALE_ENTRIES_PROPERTY, "false")); + + repo.incrementOpen(); + this.repo = repo; this.refdb = repo.getRefDatabase(); this.ids = new HashMap<>(); + this.closed = new AtomicBoolean(); } @Override @@ -62,4 +92,69 @@ public class RepoRefCache implements RefCache { public Map<String, Optional<ObjectId>> getCachedRefs() { return Collections.unmodifiableMap(ids); } + + @Override + public void close() { + if (closed.getAndSet(true)) { + log.atWarning().log("RepoRefCache of {} closed more than once", repo.getDirectory()); + return; + } + + if (checkStaleEntries) { + checkStaleness(); + } + + repo.close(); + } + + /** TODO: DO NOT MERGE into stable-3.2 onwards. */ + @VisibleForTesting + void checkStaleness() { + List<String> staleRefs = staleRefs(); + if (staleRefs.size() > 0) { + throw new IllegalStateException( + "Repository " + + repo + + " had modifications on refs " + + staleRefs + + " during a readonly window"); + } + } + + private List<String> staleRefs() { + return ids.entrySet().stream() + .filter(this::isStale) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + + private boolean isStale(Map.Entry<String, Optional<ObjectId>> refEntry) { + String refName = refEntry.getKey(); + Optional<ObjectId> id = ids.get(refName); + if (id == null) { + return false; + } + + try { + ObjectId diskId = refdb.exactRef(refName).getObjectId(); + boolean isStale = !Optional.ofNullable(diskId).equals(id); + if (isStale) { + log.atSevere().log( + "Repository " + + repo + + " has a stale ref " + + refName + + " (cache=" + + id + + ", disk=" + + diskId + + ")"); + } + return isStale; + } catch (IOException e) { + log.atSevere().withCause(e).log( + "Unable to check if ref={} from repository={} is stale", refName, repo); + return true; + } + } } diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java index 55e2fd483d..beb179c2a3 100644 --- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java +++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java @@ -85,7 +85,7 @@ public abstract class ChangeEmail extends NotificationEmail { protected boolean emailOnlyAuthors; protected ChangeEmail(EmailArguments ea, String mc, ChangeData cd) { - super(ea, mc, cd.change().getDest()); + super(ea, mc, cd); changeData = cd; change = cd.change(); emailOnlyAuthors = false; diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java index 9952ba6ff6..9ef8d872e6 100644 --- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java +++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java @@ -25,7 +25,10 @@ import com.google.gerrit.mail.MailHeader; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.server.account.ProjectWatches.NotifyType; +import com.google.gerrit.server.cache.PerThreadCache; +import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow; import com.google.gerrit.server.mail.send.ProjectWatch.Watchers; +import com.google.gerrit.server.query.change.ChangeData; import java.util.HashMap; import java.util.Map; @@ -35,9 +38,11 @@ public abstract class NotificationEmail extends OutgoingEmail { protected Branch.NameKey branch; - protected NotificationEmail(EmailArguments ea, String mc, Branch.NameKey branch) { + protected NotificationEmail(EmailArguments ea, String mc, ChangeData cd) { super(ea, mc); - this.branch = branch; + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + this.branch = cd.change().getDest(); + } } @Override diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java index 97035f23b4..949348df11 100644 --- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java +++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java @@ -20,8 +20,8 @@ import static java.util.Objects.requireNonNull; import com.google.common.collect.Sets; import com.google.common.flogger.FluentLogger; -import com.google.gerrit.exceptions.EmailException; import com.google.gerrit.common.Nullable; +import com.google.gerrit.exceptions.EmailException; import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat; @@ -32,6 +32,8 @@ import com.google.gerrit.mail.MailHeader; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.UserIdentity; import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.cache.PerThreadCache; +import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow; import com.google.gerrit.server.change.NotifyResolver; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.validators.OutgoingEmailValidationListener; @@ -92,138 +94,141 @@ public abstract class OutgoingEmail { * @throws EmailException */ public void send() throws EmailException { - if (!args.emailSender.isEnabled()) { - // Server has explicitly disabled email sending. - // - logger.atFine().log( - "Not sending '%s': Email sending is disabled by server config", messageClass); - return; - } + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + if (!args.emailSender.isEnabled()) { + // Server has explicitly disabled email sending. + // + logger.atFine().log( + "Not sending '%s': Email sending is disabled by server config", messageClass); + return; + } - if (!notify.shouldNotify()) { - logger.atFine().log("Not sending '%s': Notify handling is NONE", messageClass); - return; - } + if (!notify.shouldNotify()) { + logger.atFine().log("Not sending '%s': Notify handling is NONE", messageClass); + return; + } - init(); - if (useHtml()) { - appendHtml(soyHtmlTemplate("HeaderHtml")); - } - format(); - appendText(textTemplate("Footer")); - if (useHtml()) { - appendHtml(soyHtmlTemplate("FooterHtml")); - } + init(); + if (useHtml()) { + appendHtml(soyHtmlTemplate("HeaderHtml")); + } + format(); + appendText(textTemplate("Footer")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("FooterHtml")); + } - Set<Address> smtpRcptToPlaintextOnly = new HashSet<>(); - if (shouldSendMessage()) { - if (fromId != null) { - Optional<AccountState> fromUser = args.accountCache.get(fromId); - if (fromUser.isPresent()) { - GeneralPreferencesInfo senderPrefs = fromUser.get().getGeneralPreferences(); - if (senderPrefs != null && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) { - // If we are impersonating a user, make sure they receive a CC of - // this message so they can always review and audit what we sent - // on their behalf to others. - // - add(RecipientType.CC, fromId); - } else if (!notify.accounts().containsValue(fromId) && rcptTo.remove(fromId)) { - // If they don't want a copy, but we queued one up anyway, - // drop them from the recipient lists. - // - removeUser(fromUser.get().getAccount()); + Set<Address> smtpRcptToPlaintextOnly = new HashSet<>(); + if (shouldSendMessage()) { + if (fromId != null) { + Optional<AccountState> fromUser = args.accountCache.get(fromId); + if (fromUser.isPresent()) { + GeneralPreferencesInfo senderPrefs = fromUser.get().getGeneralPreferences(); + if (senderPrefs != null && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) { + // If we are impersonating a user, make sure they receive a CC of + // this message so they can always review and audit what we sent + // on their behalf to others. + // + add(RecipientType.CC, fromId); + } else if (!notify.accounts().containsValue(fromId) && rcptTo.remove(fromId)) { + // If they don't want a copy, but we queued one up anyway, + // drop them from the recipient lists. + // + removeUser(fromUser.get().getAccount()); + } } } - } - // Check the preferences of all recipients. If any user has disabled - // his email notifications then drop him from recipients' list. - // In addition, check if users only want to receive plaintext email. - for (Account.Id id : rcptTo) { - Optional<AccountState> thisUser = args.accountCache.get(id); - if (thisUser.isPresent()) { - Account thisUserAccount = thisUser.get().getAccount(); - GeneralPreferencesInfo prefs = thisUser.get().getGeneralPreferences(); - if (prefs == null || prefs.getEmailStrategy() == DISABLED) { - removeUser(thisUserAccount); - } else if (useHtml() && prefs.getEmailFormat() == EmailFormat.PLAINTEXT) { - removeUser(thisUserAccount); - smtpRcptToPlaintextOnly.add( - new Address(thisUserAccount.getFullName(), thisUserAccount.getPreferredEmail())); + // Check the preferences of all recipients. If any user has disabled + // his email notifications then drop him from recipients' list. + // In addition, check if users only want to receive plaintext email. + for (Account.Id id : rcptTo) { + Optional<AccountState> thisUser = args.accountCache.get(id); + if (thisUser.isPresent()) { + Account thisUserAccount = thisUser.get().getAccount(); + GeneralPreferencesInfo prefs = thisUser.get().getGeneralPreferences(); + if (prefs == null || prefs.getEmailStrategy() == DISABLED) { + removeUser(thisUserAccount); + } else if (useHtml() && prefs.getEmailFormat() == EmailFormat.PLAINTEXT) { + removeUser(thisUserAccount); + smtpRcptToPlaintextOnly.add( + new Address(thisUserAccount.getFullName(), thisUserAccount.getPreferredEmail())); + } + } + if (smtpRcptTo.isEmpty() && smtpRcptToPlaintextOnly.isEmpty()) { + logger.atFine().log("Not sending '%s': No SMTP recipients", messageClass); + return; } } - if (smtpRcptTo.isEmpty() && smtpRcptToPlaintextOnly.isEmpty()) { - logger.atFine().log("Not sending '%s': No SMTP recipients", messageClass); - return; - } - } - // Set Reply-To only if it hasn't been set by a child class - // Reply-To will already be populated for the message types where Gerrit supports - // inbound email replies. - if (!headers.containsKey(FieldName.REPLY_TO)) { - StringJoiner j = new StringJoiner(", "); - if (fromId != null) { - Address address = toAddress(fromId); - if (address != null) { - j.add(address.getEmail()); + // Set Reply-To only if it hasn't been set by a child class + // Reply-To will already be populated for the message types where Gerrit supports + // inbound email replies. + if (!headers.containsKey(FieldName.REPLY_TO)) { + StringJoiner j = new StringJoiner(", "); + if (fromId != null) { + Address address = toAddress(fromId); + if (address != null) { + j.add(address.getEmail()); + } } + smtpRcptTo.stream().forEach(a -> j.add(a.getEmail())); + smtpRcptToPlaintextOnly.stream().forEach(a -> j.add(a.getEmail())); + setHeader(FieldName.REPLY_TO, j.toString()); } - smtpRcptTo.stream().forEach(a -> j.add(a.getEmail())); - smtpRcptToPlaintextOnly.stream().forEach(a -> j.add(a.getEmail())); - setHeader(FieldName.REPLY_TO, j.toString()); - } - String textPart = textBody.toString(); - OutgoingEmailValidationListener.Args va = new OutgoingEmailValidationListener.Args(); - va.messageClass = messageClass; - va.smtpFromAddress = smtpFromAddress; - va.smtpRcptTo = smtpRcptTo; - va.headers = headers; - va.body = textPart; - - if (useHtml()) { - va.htmlBody = htmlBody.toString(); - } else { - va.htmlBody = null; - } + String textPart = textBody.toString(); + OutgoingEmailValidationListener.Args va = new OutgoingEmailValidationListener.Args(); + va.messageClass = messageClass; + va.smtpFromAddress = smtpFromAddress; + va.smtpRcptTo = smtpRcptTo; + va.headers = headers; + va.body = textPart; + + if (useHtml()) { + va.htmlBody = htmlBody.toString(); + } else { + va.htmlBody = null; + } - for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) { - try { - validator.validateOutgoingEmail(va); - } catch (ValidationException e) { - logger.atFine().log( - "Not sending '%s': Rejected by outgoing email validator: %s", - messageClass, e.getMessage()); - return; + for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) { + try { + validator.validateOutgoingEmail(va); + } catch (ValidationException e) { + logger.atFine().log( + "Not sending '%s': Rejected by outgoing email validator: %s", + messageClass, e.getMessage()); + return; + } } - } - Set<Address> intersection = Sets.intersection(va.smtpRcptTo, smtpRcptToPlaintextOnly); - if (!intersection.isEmpty()) { - logger.atSevere().log("Email '%s' will be sent twice to %s", messageClass, intersection); - } + Set<Address> intersection = Sets.intersection(va.smtpRcptTo, smtpRcptToPlaintextOnly); + if (!intersection.isEmpty()) { + logger.atSevere().log("Email '%s' will be sent twice to %s", messageClass, intersection); + } - if (!va.smtpRcptTo.isEmpty()) { - // Send multipart message - logger.atFine().log("Sending multipart '%s'", messageClass); - args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body, va.htmlBody); - } + if (!va.smtpRcptTo.isEmpty()) { + // Send multipart message + logger.atFine().log("Sending multipart '%s'", messageClass); + args.emailSender.send( + va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body, va.htmlBody); + } - if (!smtpRcptToPlaintextOnly.isEmpty()) { - logger.atFine().log("Sending plaintext '%s'", messageClass); - // Send plaintext message - Map<String, EmailHeader> shallowCopy = new HashMap<>(); - shallowCopy.putAll(headers); - // Remove To and Cc - shallowCopy.remove(FieldName.TO); - shallowCopy.remove(FieldName.CC); - for (Address a : smtpRcptToPlaintextOnly) { - // Add new To - EmailHeader.AddressList to = new EmailHeader.AddressList(); - to.add(a); - shallowCopy.put(FieldName.TO, to); + if (!smtpRcptToPlaintextOnly.isEmpty()) { + logger.atFine().log("Sending plaintext '%s'", messageClass); + // Send plaintext message + Map<String, EmailHeader> shallowCopy = new HashMap<>(); + shallowCopy.putAll(headers); + // Remove To and Cc + shallowCopy.remove(FieldName.TO); + shallowCopy.remove(FieldName.CC); + for (Address a : smtpRcptToPlaintextOnly) { + // Add new To + EmailHeader.AddressList to = new EmailHeader.AddressList(); + to.add(a); + shallowCopy.put(FieldName.TO, to); + } + args.emailSender.send(va.smtpFromAddress, smtpRcptToPlaintextOnly, shallowCopy, va.body); } - args.emailSender.send(va.smtpFromAddress, smtpRcptToPlaintextOnly, shallowCopy, va.body); } } } diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java index 22ac95a4d5..cda394c1c3 100644 --- a/java/com/google/gerrit/server/notedb/ChangeNotes.java +++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java @@ -303,7 +303,6 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> { } private final boolean shouldExist; - private final RefCache refs; private Change change; private ChangeNotesState state; @@ -326,7 +325,6 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> { super(args, change.getId()); this.change = new Change(change); this.shouldExist = shouldExist; - this.refs = refs; } public Change getChange() { @@ -534,8 +532,7 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> { @Override protected ObjectId readRef(Repository repo) throws IOException { - Optional<RefCache> refsCache = - Optional.ofNullable(refs).map(Optional::of).orElse(RepoRefCache.getOptional(repo)); + Optional<RefCache> refsCache = RepoRefCache.getOptional(repo); return refsCache.isPresent() ? refsCache.get().get(getRefName()).orElse(null) : super.readRef(repo); diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java index 51e2dfbe3c..596769b57e 100644 --- a/java/com/google/gerrit/server/restapi/change/Submit.java +++ b/java/com/google/gerrit/server/restapi/change/Submit.java @@ -43,6 +43,8 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.ProjectUtil; import com.google.gerrit.server.account.AccountResolver; +import com.google.gerrit.server.cache.PerThreadCache; +import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow; import com.google.gerrit.server.change.ChangeJson; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.RevisionResource; @@ -211,7 +213,7 @@ public class Submit // Read the ChangeNotes only after MergeOp is fully done (including MergeOp#close) to be sure // to have the correct state of the repo. - try { + try (ReadonlyRequestWindow readonlyRequestWindow = PerThreadCache.openReadonlyRequestWindow()) { change = changeNotesFactory.createChecked(change.getProject(), change.getId()).getChange(); } catch (NoSuchChangeException e) { throw new ResourceConflictException("change is deleted", e); diff --git a/java/com/google/gerrit/server/update/ChainedReceiveCommands.java b/java/com/google/gerrit/server/update/ChainedReceiveCommands.java index c223aec637..9a395759bf 100644 --- a/java/com/google/gerrit/server/update/ChainedReceiveCommands.java +++ b/java/com/google/gerrit/server/update/ChainedReceiveCommands.java @@ -39,13 +39,19 @@ import org.eclipse.jgit.transport.ReceiveCommand; public class ChainedReceiveCommands implements RefCache { private final Map<String, ReceiveCommand> commands = new LinkedHashMap<>(); private final RepoRefCache refCache; + private final boolean closeRefCache; public ChainedReceiveCommands(Repository repo) { - this(new RepoRefCache(repo)); + this(new RepoRefCache(repo), true); } public ChainedReceiveCommands(RepoRefCache refCache) { + this(refCache, false); + } + + private ChainedReceiveCommands(RepoRefCache refCache, boolean closeRefCache) { this.refCache = requireNonNull(refCache); + this.closeRefCache = closeRefCache; } public RepoRefCache getRepoRefCache() { @@ -122,4 +128,11 @@ public class ChainedReceiveCommands implements RefCache { public Map<String, ReceiveCommand> getCommands() { return Collections.unmodifiableMap(commands); } + + @Override + public void close() { + if (closeRefCache) { + refCache.close(); + } + } } diff --git a/javatests/com/google/gerrit/server/cache/BUILD b/javatests/com/google/gerrit/server/cache/BUILD index bb6ea30739..0c3bf3ed40 100644 --- a/javatests/com/google/gerrit/server/cache/BUILD +++ b/javatests/com/google/gerrit/server/cache/BUILD @@ -10,6 +10,7 @@ junit_tests( "//lib:junit", "//lib/truth", "//lib/truth:truth-java8-extension", + "@jgit-lib//jar", "@servlet-api//jar", ], ) diff --git a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java index 65d9979f56..2e7d8eb75a 100644 --- a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java +++ b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java @@ -16,12 +16,20 @@ package com.google.gerrit.server.cache; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; +import com.google.gerrit.server.cache.PerThreadCache.Key; +import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow; +import com.google.gerrit.server.git.RefCache; import com.google.gerrit.testing.GerritBaseTests; import com.google.gerrit.util.http.testutil.FakeHttpServletRequest; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; +import org.eclipse.jgit.lib.ObjectId; import org.junit.Test; public class PerThreadCacheTest extends GerritBaseTests { @@ -47,14 +55,13 @@ public class PerThreadCacheTest extends GerritBaseTests { PerThreadCache cache = PerThreadCache.get(); PerThreadCache.Key<String> key1 = PerThreadCache.Key.create(String.class); - String value1 = cache.get(key1, () -> "value1"); - assertThat(value1).isEqualTo("value1"); + assertThat(cache.getWithLoader(key1, () -> "value1", null)).hasValue("value1"); Supplier<String> neverCalled = () -> { throw new IllegalStateException("this method must not be called"); }; - assertThat(cache.get(key1, neverCalled)).isEqualTo("value1"); + assertThat(cache.getWithLoader(key1, neverCalled, null)).hasValue("value1"); } } @@ -63,20 +70,30 @@ public class PerThreadCacheTest extends GerritBaseTests { PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class); try (PerThreadCache ignored = PerThreadCache.create(null)) { PerThreadCache cache = PerThreadCache.get(); - String value1 = cache.get(key, () -> "value1"); - assertThat(value1).isEqualTo("value1"); + assertThat(cache.getWithLoader(key, () -> "value1", null)).hasValue("value1"); } // Create a second cache and assert that it is not connected to the first one. // This ensures that the cleanup is actually working. try (PerThreadCache ignored = PerThreadCache.create(null)) { PerThreadCache cache = PerThreadCache.get(); - String value1 = cache.get(key, () -> "value2"); - assertThat(value1).isEqualTo("value2"); + assertThat(cache.getWithLoader(key, () -> "value2", null)).hasValue("value2"); } } @Test + public void unloaderCalledUponCleanup() { + AtomicBoolean unloaderCalled = new AtomicBoolean(); + PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class); + try (PerThreadCache ignored = PerThreadCache.create(null)) { + PerThreadCache cache = PerThreadCache.get(); + cache.getWithLoader(key, () -> "value1", v -> unloaderCalled.set(true)); + assertThat(unloaderCalled.get()).isFalse(); + } + assertThat(unloaderCalled.get()).isTrue(); + } + + @Test public void doubleInstantiationFails() { try (PerThreadCache ignored = PerThreadCache.create(null)) { exception.expect(IllegalStateException.class); @@ -89,8 +106,7 @@ public class PerThreadCacheTest extends GerritBaseTests { public void isAssociatedWithHttpReadonlyRequest() { HttpServletRequest getRequest = new FakeHttpServletRequest(); try (PerThreadCache cache = PerThreadCache.create(getRequest)) { - assertThat(cache.getHttpRequest()).hasValue(getRequest); - assertThat(cache.hasReadonlyRequest()).isTrue(); + assertThat(cache.allowRefCache()).isTrue(); } } @@ -104,32 +120,211 @@ public class PerThreadCacheTest extends GerritBaseTests { } }; try (PerThreadCache cache = PerThreadCache.create(putRequest)) { - assertThat(cache.getHttpRequest()).hasValue(putRequest); - assertThat(cache.hasReadonlyRequest()).isFalse(); + assertThat(cache.allowRefCache()).isFalse(); } } @Test public void isNotAssociatedWithHttpRequest() { try (PerThreadCache cache = PerThreadCache.create(null)) { - assertThat(cache.getHttpRequest()).isEmpty(); - assertThat(cache.hasReadonlyRequest()).isFalse(); + assertThat(cache.allowRefCache()).isFalse(); } } @Test - public void enforceMaxSize() { + public void isAssociatedWithReadonlyRequest() { + try (PerThreadCache cache = PerThreadCache.createReadOnly()) { + assertThat(cache.allowRefCache()).isTrue(); + } + } + + @Test + public void openNestedTemporaryReadonlyWindows() throws Exception { + + try (PerThreadCache cache = PerThreadCache.create(null)) { + assertThat(cache.allowRefCache()).isFalse(); + + try (ReadonlyRequestWindow outerWindow = PerThreadCache.openReadonlyRequestWindow()) { + assertThat(cache.allowRefCache()).isTrue(); + + try (ReadonlyRequestWindow innerWindow = PerThreadCache.openReadonlyRequestWindow()) { + assertThat(cache.allowRefCache()).isTrue(); + } + + assertThat(cache.allowRefCache()).isTrue(); + } + + assertThat(cache.allowRefCache()).isFalse(); + } + } + + static class TestCacheValue implements RefCache { + public static final String STALENESS_FAILED_MESSAGE = "Staleness check failed"; + + private final String value; + + public TestCacheValue(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object obj) { + return obj != null + && (obj instanceof TestCacheValue) + && ((TestCacheValue) obj).value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + public void checkStaleness() throws IllegalStateException { + throw new IllegalStateException(STALENESS_FAILED_MESSAGE); + } + + @Override + public Optional<ObjectId> get(String refName) throws IOException { + return Optional.empty(); + } + + @Override + public void close() {} + } + + @Test + public void clearOutStaleEntriesAfterReadonlyWindow() throws Exception { + Key<TestCacheValue> key = PerThreadCache.Key.create(TestCacheValue.class, "key"); + TestCacheValue cachedValue = new TestCacheValue("cached value"); + TestCacheValue updatedValue = new TestCacheValue("updated value"); + + try (PerThreadCache cache = PerThreadCache.create(null)) { + try (ReadonlyRequestWindow outerWindow = PerThreadCache.openReadonlyRequestWindow()) { + assertThat(PerThreadCache.get(key, () -> cachedValue, v -> {})).hasValue(cachedValue); + assertThat(PerThreadCache.get(key, () -> updatedValue, v -> {})).hasValue(cachedValue); + } + + assertThat(PerThreadCache.get(key, () -> updatedValue, v -> {})).hasValue(updatedValue); + } + } + + @Test + public void checkForStaleEntriesAfterReadonlyWindow() { + Key<TestCacheValue> key = PerThreadCache.Key.create(TestCacheValue.class, "key"); + try (PerThreadCache cache = PerThreadCache.create(null)) { - // Fill the cache - for (int i = 0; i < 50; i++) { - PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, i); - cache.get(key, () -> "cached value"); + IllegalStateException thrown = + assertThrows( + IllegalStateException.class, + () -> { + try (ReadonlyRequestWindow outerWindow = + PerThreadCache.openReadonlyRequestWindow()) { + PerThreadCache.get( + key, () -> new TestCacheValue(""), TestCacheValue::checkStaleness); + } + }); + assertThat(thrown).hasMessageThat().isEqualTo(TestCacheValue.STALENESS_FAILED_MESSAGE); + } + } + + @Test + public void allowStaleEntriesAfterReadonlyWindow() { + Key<TestCacheValue> key = PerThreadCache.Key.create(TestCacheValue.class, "key"); + TestCacheValue value = new TestCacheValue(""); + Optional<TestCacheValue> cachedValue; + + try (PerThreadCache cache = PerThreadCache.create(null)) { + try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) { + + cachedValue = PerThreadCache.get(key, () -> value, null); } + } + + assertThat(cachedValue).hasValue(value); + } + + @SuppressWarnings("deprecation") + @Test + public void disableCachingForSpecificTypes() { + System.setProperty( + PerThreadCache.PER_THREAD_CACHE_DISABLED_TYPES_PROPERTY, + String.class.getCanonicalName() + "," + Integer.class.getCanonicalName()); + try { + try (PerThreadCache cache = PerThreadCache.createReadOnly()) { + assertThat( + PerThreadCache.get( + PerThreadCache.Key.create(String.class, "key"), () -> "oldValue", null)) + .isEmpty(); + assertThat( + PerThreadCache.getOrCompute( + PerThreadCache.Key.create(String.class, "key"), () -> "newValue")) + .isEqualTo("newValue"); + + assertThat( + PerThreadCache.get(PerThreadCache.Key.create(Integer.class, "key"), () -> 1, null)) + .isEmpty(); + assertThat( + PerThreadCache.getOrCompute( + PerThreadCache.Key.create(Integer.class, "key"), () -> 2)) + .isEqualTo(2); + + assertThat(PerThreadCache.get(PerThreadCache.Key.create(Long.class, "key"), () -> 1L, null)) + .hasValue(1L); + assertThat( + PerThreadCache.getOrCompute(PerThreadCache.Key.create(Long.class, "key"), () -> 2L)) + .isEqualTo(1L); + } + } finally { + System.setProperty(PerThreadCache.PER_THREAD_CACHE_DISABLED_TYPES_PROPERTY, ""); + } + } + + @SuppressWarnings("deprecation") + @Test + public void enforceMaxSize() { + try (PerThreadCache cache = PerThreadCache.create(null)) { + fillTheCache(cache); + // Assert that the value was not persisted PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, 1000); - cache.get(key, () -> "new value"); + assertThat(cache.getWithLoader(key, () -> "new value", null)).isEmpty(); String value = cache.get(key, () -> "directly served"); assertThat(value).isEqualTo("directly served"); } } + + @Test + public void returnEmptyWhenCacheIsFull() { + try (PerThreadCache cache = PerThreadCache.create(null)) { + fillTheCache(cache); + + PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, 1000); + assertThat(cache.getWithLoader(key, () -> "new value", null)).isEmpty(); + } + } + + @Test + public void unloaderNotCalledUponCleanupIfCacheWasFull() { + AtomicBoolean unloaderCalled = new AtomicBoolean(); + PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class); + try (PerThreadCache ignored = PerThreadCache.create(null)) { + PerThreadCache cache = PerThreadCache.get(); + fillTheCache(cache); + + cache.getWithLoader(key, () -> "value1", v -> unloaderCalled.set(true)); + } + assertThat(unloaderCalled.get()).isFalse(); + } + + private void fillTheCache(PerThreadCache cache) { + for (int i = 0; i < 50; i++) { + PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, i); + cache.getWithLoader(key, () -> "cached value", null); + } + } } diff --git a/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java b/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java new file mode 100644 index 0000000000..9ad8ffbf58 --- /dev/null +++ b/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java @@ -0,0 +1,329 @@ +// 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.git; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.google.gerrit.server.cache.PerThreadCache; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.eclipse.jgit.attributes.AttributesNodeProvider; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository.Builder; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectDatabase; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefRename; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.ReflogReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.util.FS; +import org.junit.Test; + +public class RepoRefCacheTest { + private static final String TEST_BRANCH = "main"; + + @Test + @SuppressWarnings("resource") + public void repositoryUseShouldBeTrackedByRepoRefCache() throws Exception { + RefCache cache; + TestRepositoryWithRefCounting repoWithRefCounting; + + try (TestRepositoryWithRefCounting repo = + TestRepositoryWithRefCounting.createWithBranch(TEST_BRANCH)) { + assertThat(repo.refCounter()).isEqualTo(1); + repoWithRefCounting = repo; + cache = new RepoRefCache(repo); + } + + assertThat(repoWithRefCounting.refCounter()).isEqualTo(1); + assertThat(cache.get(Constants.R_HEADS + TEST_BRANCH)).isNotNull(); + } + + @Test + @SuppressWarnings("resource") + public void shouldNotKeepReferenceToReposWhenCacheIsFull() throws Exception { + TestRepositoryWithRefCounting repoPointedFromCache; + + try (PerThreadCache threadCache = PerThreadCache.createReadOnly()) { + fillUpAllThreadCache(threadCache); + + try (TestRepositoryWithRefCounting repo = + TestRepositoryWithRefCounting.createWithBranch(TEST_BRANCH)) { + repoPointedFromCache = repo; + assertThat(repo.refCounter()).isEqualTo(1); + Optional<RefCache> refCache = RepoRefCache.getOptional(repo); + assertThat(refCache).isEqualTo(Optional.empty()); + assertThat(repo.refCounter()).isEqualTo(1); + } + + assertThat(repoPointedFromCache.refCounter()).isEqualTo(0); + } + } + + @Test + public void shouldCheckForStaleness() throws Exception { + String refName = "refs/heads/foo"; + + try (TestRepositoryWithRefCounting repo = + TestRepositoryWithRefCounting.createWithBranch(TEST_BRANCH)) { + RepoRefCache refCache = new RepoRefCache(repo); + TestRepository<Repository> testRepo = new TestRepository<>(repo); + + Optional<ObjectId> cachedObjId = refCache.get(refName); + + assertThat(cachedObjId).isEqualTo(Optional.empty()); + + RefUpdate refUpdate = repo.getRefDatabase().newUpdate(refName, true); + refUpdate.setNewObjectId(testRepo.commit().create().getId()); + + assertThat(refUpdate.forceUpdate()).isEqualTo(Result.NEW); + + IllegalStateException thrown = + assertThrows(IllegalStateException.class, () -> refCache.checkStaleness()); + assertThat(thrown).hasMessageThat().contains(refName); + } + } + + private void fillUpAllThreadCache(PerThreadCache cache) { + + // Fill the cache + for (int i = 0; i < 50; i++) { + PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, i); + cache.getWithLoader(key, () -> "cached value", null); + } + } + + private static class TestRepositoryWithRefCounting extends Repository { + private int refCounter; + + static TestRepositoryWithRefCounting createWithBranch(String branchName) throws Exception { + Builder builder = + new InMemoryRepository.Builder() + .setRepositoryDescription(new DfsRepositoryDescription("")) + .setFS(FS.detect().setUserHome(null)); + TestRepositoryWithRefCounting testRepo = new TestRepositoryWithRefCounting(builder); + new TestRepository<>(testRepo).branch(branchName).commit().message("").create(); + return testRepo; + } + + private final Repository repo; + + private TestRepositoryWithRefCounting(InMemoryRepository.Builder builder) throws IOException { + super(builder); + + repo = builder.build(); + refCounter = 1; + } + + public int refCounter() { + return refCounter; + } + + @Override + public void incrementOpen() { + repo.incrementOpen(); + refCounter++; + } + + @Override + public void close() { + repo.close(); + refCounter--; + } + + @Override + public void create(boolean bare) throws IOException {} + + @Override + public ObjectDatabase getObjectDatabase() { + checkIsOpen(); + return repo.getObjectDatabase(); + } + + @Override + public RefDatabase getRefDatabase() { + RefDatabase refDatabase = repo.getRefDatabase(); + return new RefDatabase() { + + @Override + public int hashCode() { + return refDatabase.hashCode(); + } + + @Override + public void create() throws IOException { + refDatabase.create(); + } + + @Override + public void close() { + checkIsOpen(); + refDatabase.close(); + } + + @Override + public boolean isNameConflicting(String name) throws IOException { + checkIsOpen(); + return refDatabase.isNameConflicting(name); + } + + @Override + public boolean equals(Object obj) { + return refDatabase.equals(obj); + } + + @Override + public Collection<String> getConflictingNames(String name) throws IOException { + checkIsOpen(); + return refDatabase.getConflictingNames(name); + } + + @Override + public RefUpdate newUpdate(String name, boolean detach) throws IOException { + checkIsOpen(); + return refDatabase.newUpdate(name, detach); + } + + @Override + public RefRename newRename(String fromName, String toName) throws IOException { + checkIsOpen(); + return refDatabase.newRename(fromName, toName); + } + + @Override + public BatchRefUpdate newBatchUpdate() { + checkIsOpen(); + return refDatabase.newBatchUpdate(); + } + + @Override + public boolean performsAtomicTransactions() { + checkIsOpen(); + return refDatabase.performsAtomicTransactions(); + } + + @Override + public Ref exactRef(String name) throws IOException { + checkIsOpen(); + return refDatabase.exactRef(name); + } + + @Override + public String toString() { + return refDatabase.toString(); + } + + @Override + public Map<String, Ref> exactRef(String... refs) throws IOException { + checkIsOpen(); + return refDatabase.exactRef(refs); + } + + @Override + public Ref firstExactRef(String... refs) throws IOException { + checkIsOpen(); + return refDatabase.firstExactRef(refs); + } + + @Override + public List<Ref> getRefs() throws IOException { + checkIsOpen(); + return refDatabase.getRefs(); + } + + @Override + public Map<String, Ref> getRefs(String prefix) throws IOException { + checkIsOpen(); + return refDatabase.getRefs(prefix); + } + + @Override + public List<Ref> getRefsByPrefix(String prefix) throws IOException { + checkIsOpen(); + return refDatabase.getRefsByPrefix(prefix); + } + + @Override + public boolean hasRefs() throws IOException { + checkIsOpen(); + return refDatabase.hasRefs(); + } + + @Override + public List<Ref> getAdditionalRefs() throws IOException { + checkIsOpen(); + return refDatabase.getAdditionalRefs(); + } + + @Override + public Ref peel(Ref ref) throws IOException { + checkIsOpen(); + return refDatabase.peel(ref); + } + + @Override + public void refresh() { + checkIsOpen(); + refDatabase.refresh(); + } + }; + } + + @Override + public StoredConfig getConfig() { + return repo.getConfig(); + } + + @Override + public AttributesNodeProvider createAttributesNodeProvider() { + checkIsOpen(); + return repo.createAttributesNodeProvider(); + } + + @Override + public void scanForRepoChanges() throws IOException { + checkIsOpen(); + } + + @Override + public void notifyIndexChanged(boolean internal) { + checkIsOpen(); + } + + @Override + public ReflogReader getReflogReader(String refName) throws IOException { + checkIsOpen(); + return repo.getReflogReader(refName); + } + + private void checkIsOpen() { + if (refCounter <= 0) { + throw new IllegalStateException("Repository is not open (refCounter=" + refCounter + ")"); + } + } + } +} |