| // Copyright (C) 2018 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.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; |
| |
| /** |
| * Caches object instances for a request as {@link ThreadLocal} in the serving thread. |
| * |
| * <p>This class is intended to cache objects that have a high instantiation cost, are specific to |
| * the current request and potentially need to be instantiated multiple times while serving a |
| * request. |
| * |
| * <p>This is different from the key-value storage in {@code CurrentUser}: {@code CurrentUser} |
| * offers a key-value storage by providing thread-safe {@code get} and {@code put} methods. Once the |
| * value is retrieved through {@code get} there is not thread-safety anymore - apart from the |
| * retrieved object guarantees. Depending on the implementation of {@code CurrentUser}, it might be |
| * shared between the request serving thread as well as sub- or background treads. |
| * |
| * <p>In comparison to that, this class guarantees thread safety even on non-thread-safe objects as |
| * its cache is tied to the serving thread only. While allowing to cache non-thread-safe objects, it |
| * has the downside of not sharing any objects with background threads or executors. |
| * |
| * <p>Lastly, this class offers a cache, that requires callers to also provide a {@code Supplier} in |
| * case the object is not present in the cache, while {@code CurrentUser} provides a storage where |
| * just retrieving stored values is a valid operation. |
| * |
| * <p>To prevent OOM errors on requests that would cache a lot of objects, this class enforces an |
| * internal limit after which no new elements are cached. All {@code get} calls are served by |
| * invoking the {@code Supplier} after that. |
| */ |
| public class PerThreadCache implements AutoCloseable { |
| private static final ThreadLocal<PerThreadCache> CACHE = new ThreadLocal<>(); |
| /** |
| * Cache at maximum 25 values per thread. This value was chosen arbitrarily. Some endpoints (like |
| * ListProjects) break the assumption that the data cached in a request is limited. To prevent |
| * this class from accumulating an unbound number of objects, we enforce this limit. |
| */ |
| private static final int PER_THREAD_CACHE_SIZE = 25; |
| |
| /** |
| * System property for disabling caching specific key types. TODO: DO NOT MERGE into stable-3.2 |
| * onwards. |
| */ |
| 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 |
| * class and a list of identifiers that in combination uniquely set the object apart form others |
| * of the same class. |
| */ |
| public static final class Key<T> { |
| private final Class<T> clazz; |
| private final ImmutableList<Object> identifiers; |
| |
| /** |
| * Returns a key based on the value's class and an identifier that uniquely identify the value. |
| * The identifier needs to implement {@code equals()} and {@hashCode()}. |
| */ |
| public static <T> Key<T> create(Class<T> clazz, Object identifier) { |
| return new Key<>(clazz, ImmutableList.of(identifier)); |
| } |
| |
| /** |
| * Returns a key based on the value's class and a set of identifiers that uniquely identify the |
| * value. Identifiers need to implement {@code equals()} and {@hashCode()}. |
| */ |
| public static <T> Key<T> create(Class<T> clazz, Object... identifiers) { |
| return new Key<>(clazz, ImmutableList.copyOf(identifiers)); |
| } |
| |
| private Key(Class<T> clazz, ImmutableList<Object> identifiers) { |
| this.clazz = clazz; |
| this.identifiers = identifiers; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hashCode(clazz, identifiers); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (!(o instanceof Key)) { |
| return false; |
| } |
| Key<?> other = (Key<?>) o; |
| return this.clazz == other.clazz && this.identifiers.equals(other.identifiers); |
| } |
| } |
| |
| /** |
| * 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, 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; |
| } |
| |
| @Nullable |
| public static PerThreadCache get() { |
| 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, 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"))); |
| } |
| |
| /** |
| * 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) { |
| 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 && cache.size() < PER_THREAD_CACHE_SIZE) { |
| value = loader.get(); |
| cache.put(key, value); |
| if (unloader != null) { |
| unloaders.put(key, (Consumer<Object>) unloader); |
| } |
| } |
| return Optional.ofNullable(value); |
| } |
| |
| /** 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 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())); |
| } |
| } |
| } |