/* * Copyright 2015 Ben Manes. All Rights Reserved. * * 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.github.benmanes.caffeine.cache; import static java.util.Objects.requireNonNull; import java.io.Serializable; import java.lang.reflect.Method; import java.util.AbstractCollection; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nonnull; import com.github.benmanes.caffeine.cache.stats.CacheStats; /** * This class provides a skeletal implementation of the {@link AsyncLoadingCache} interface to * minimize the effort required to implement a {@link LocalCache}. * * @author ben.manes@gmail.com (Ben Manes) */ abstract class LocalAsyncLoadingCache<C extends LocalCache<K, CompletableFuture<V>>, K, V> implements AsyncLoadingCache<K, V> { static final Logger logger = Logger.getLogger(LocalAsyncLoadingCache.class.getName()); final C cache; final boolean canBulkLoad; final AsyncCacheLoader<K, V> loader; LoadingCacheView localCacheView; @SuppressWarnings("unchecked") LocalAsyncLoadingCache(C cache, AsyncCacheLoader<? super K, V> loader) { this.loader = (AsyncCacheLoader<K, V>) loader; this.canBulkLoad = canBulkLoad(loader); this.cache = cache; } /** Returns the policy supported by this implementation and its configuration. */ protected abstract Policy<K, V> policy(); /** Returns whether the supplied cache loader has bulk load functionality. */ private static boolean canBulkLoad(AsyncCacheLoader<?, ?> loader) { try { Class<?> defaultLoaderClass = AsyncCacheLoader.class; if (loader instanceof CacheLoader<?, ?>) { defaultLoaderClass = CacheLoader.class; Method classLoadAll = loader.getClass().getMethod("loadAll", Iterable.class); Method defaultLoadAll = CacheLoader.class.getMethod("loadAll", Iterable.class); if (!classLoadAll.equals(defaultLoadAll)) { return true; } } Method classAsyncLoadAll = loader.getClass().getMethod( "asyncLoadAll", Iterable.class, Executor.class); Method defaultAsyncLoadAll = defaultLoaderClass.getMethod( "asyncLoadAll", Iterable.class, Executor.class); return !classAsyncLoadAll.equals(defaultAsyncLoadAll); } catch (NoSuchMethodException | SecurityException e) { logger.log(Level.WARNING, "Cannot determine if CacheLoader can bulk load", e); return false; } } @Override public CompletableFuture<V> getIfPresent(@Nonnull Object key) { return cache.getIfPresent(key, /* recordStats */ true); } @Override public CompletableFuture<V> get(@Nonnull K key, @Nonnull Function<? super K, ? extends V> mappingFunction) { requireNonNull(mappingFunction); return get(key, (k1, executor) -> CompletableFuture.supplyAsync( () -> mappingFunction.apply(key), executor)); } @Override public CompletableFuture<V> get(K key, BiFunction<? super K, Executor, CompletableFuture<V>> mappingFunction) { return get(key, mappingFunction, /* recordStats */ true); } @SuppressWarnings("FutureReturnValueIgnored") CompletableFuture<V> get(K key, BiFunction<? super K, Executor, CompletableFuture<V>> mappingFunction, boolean recordStats) { long startTime = cache.statsTicker().read(); @SuppressWarnings({"unchecked", "rawtypes"}) CompletableFuture<V>[] result = new CompletableFuture[1]; CompletableFuture<V> future = cache.computeIfAbsent(key, k -> { result[0] = mappingFunction.apply(key, cache.executor()); return requireNonNull(result[0]); }, recordStats, /* recordLoad */ false); if (result[0] != null) { AtomicBoolean completed = new AtomicBoolean(); result[0].whenComplete((value, error) -> { if (!completed.compareAndSet(false, true)) { // Ignore multiple invocations due to ForkJoinPool retrying on delays return; } long loadTime = cache.statsTicker().read() - startTime; if (value == null) { if (error != null) { logger.log(Level.WARNING, "Exception thrown during asynchronous load", error); } cache.statsCounter().recordLoadFailure(loadTime); cache.remove(key, result[0]); } else { // update the weight and expiration timestamps cache.replace(key, result[0], result[0]); cache.statsCounter().recordLoadSuccess(loadTime); } }); } return future; } @Override public CompletableFuture<V> get(K key) { CompletableFuture<V> future = get(key, loader::asyncLoad); return requireNonNull(future); } @Override public CompletableFuture<Map<K, V>> getAll(Iterable<? extends K> keys) { if (canBulkLoad) { return getAllBulk(keys); } Map<K, CompletableFuture<V>> result = new HashMap<>(); Function<K, CompletableFuture<V>> mappingFunction = this::get; for (K key : keys) { CompletableFuture<V> future = result.computeIfAbsent(key, mappingFunction); requireNonNull(future); } return composeResult(result); } /** Computes all of the missing entries in a single {@link CacheLoader#asyncLoadAll} call. */ @SuppressWarnings("FutureReturnValueIgnored") private CompletableFuture<Map<K, V>> getAllBulk(Iterable<? extends K> keys) { Map<K, CompletableFuture<V>> futures = new HashMap<>(); Map<K, CompletableFuture<V>> proxies = new HashMap<>(); for (K key : keys) { if (futures.containsKey(key)) { continue; } CompletableFuture<V> future = cache.getIfPresent(key, /* recordStats */ false); if (future == null) { CompletableFuture<V> proxy = new CompletableFuture<>(); future = cache.putIfAbsent(key, proxy); if (future == null) { future = proxy; proxies.put(key, proxy); } } futures.put(key, future); } cache.statsCounter().recordMisses(proxies.size()); cache.statsCounter().recordHits(futures.size() - proxies.size()); if (proxies.isEmpty()) { return composeResult(futures); } AsyncBulkCompleter completer = new AsyncBulkCompleter(proxies); try { loader.asyncLoadAll(proxies.keySet(), cache.executor()).whenComplete(completer); return composeResult(futures); } catch (Throwable t) { completer.accept(/* result */ null, t); throw t; } } /** * Returns a future that waits for all of the dependent futures to complete and returns the * combined mapping if successful. If any future fails then it is automatically removed from * the cache if still present. */ private CompletableFuture<Map<K, V>> composeResult(Map<K, CompletableFuture<V>> futures) { if (futures.isEmpty()) { return CompletableFuture.completedFuture(Collections.emptyMap()); } @SuppressWarnings("rawtypes") CompletableFuture<?>[] array = futures.values().toArray(new CompletableFuture[0]); return CompletableFuture.allOf(array).thenApply(ignored -> { Map<K, V> result = new HashMap<>(futures.size()); futures.forEach((key, future) -> { V value = future.getNow(null); if (value != null) { result.put(key, value); } }); return Collections.unmodifiableMap(result); }); } @Override @SuppressWarnings("FutureReturnValueIgnored") public void put(K key, CompletableFuture<V> valueFuture) { if (valueFuture.isCompletedExceptionally() || (valueFuture.isDone() && (valueFuture.join() == null))) { cache.statsCounter().recordLoadFailure(0L); cache.remove(key); return; } AtomicBoolean completed = new AtomicBoolean(); long startTime = cache.statsTicker().read(); cache.put(key, valueFuture); valueFuture.whenComplete((value, error) -> { if (!completed.compareAndSet(false, true)) { // Ignore multiple invocations due to ForkJoinPool retrying on delays return; } long loadTime = cache.statsTicker().read() - startTime; if (value == null) { if (error != null) { logger.log(Level.WARNING, "Exception thrown during asynchronous load", error); } cache.remove(key, valueFuture); cache.statsCounter().recordLoadFailure(loadTime); } else { // update the weight and expiration timestamps cache.replace(key, valueFuture, valueFuture); cache.statsCounter().recordLoadSuccess(loadTime); } }); } @Override public LoadingCache<K, V> synchronous() { return (localCacheView == null) ? (localCacheView = new LoadingCacheView()) : localCacheView; } /** A function executed asynchronously after a bulk load completes. */ private final class AsyncBulkCompleter implements BiConsumer<Map<K, V>, Throwable> { private final Map<K, CompletableFuture<V>> proxies; private final long startTime; AsyncBulkCompleter(Map<K, CompletableFuture<V>> proxies) { this.startTime = cache.statsTicker().read(); this.proxies = proxies; } @Override public void accept(Map<K, V> result, Throwable error) { long loadTime = cache.statsTicker().read() - startTime; if (result == null) { if (error == null) { error = new CompletionException("null map", null); } for (Entry<K, CompletableFuture<V>> entry : proxies.entrySet()) { cache.remove(entry.getKey(), entry.getValue()); entry.getValue().obtrudeException(error); } cache.statsCounter().recordLoadFailure(loadTime); logger.log(Level.WARNING, "Exception thrown during asynchronous load", error); } else { fillProxies(result); addNewEntries(result); cache.statsCounter().recordLoadSuccess(result.size()); } } /** Populates the proxies with the computed result. */ private void fillProxies(Map<K, V> result) { proxies.forEach((key, future) -> { V value = result.get(key); future.obtrudeValue(value); if (value == null) { cache.remove(key, future); } else { // update the weight and expiration timestamps cache.replace(key, future, future); } }); } /** Adds to the cache any extra entries computed that were not requested. */ private void addNewEntries(Map<K, V> result) { if (proxies.size() == result.size()) { return; } result.forEach((key, value) -> { if (!proxies.containsKey(key)) { cache.put(key, CompletableFuture.completedFuture(value)); } }); } } /* ---------------- Synchronous views -------------- */ final class LoadingCacheView implements LoadingCache<K, V>, Serializable { private static final long serialVersionUID = 1L; transient AsMapView<K, V> asMapView; /** A test-only method for validation. */ LocalAsyncLoadingCache<C, K, V> getOuter() { return LocalAsyncLoadingCache.this; } @Override public V getIfPresent(Object key) { CompletableFuture<V> future = cache.getIfPresent(key, /* recordStats */ true); return Async.getIfReady(future); } @Override public Map<K, V> getAllPresent(Iterable<?> keys) { Map<Object, Object> result = new HashMap<>(); for (Object key : keys) { result.put(key, null); } int misses = 0; for (Iterator<Entry<Object, Object>> iter = result.entrySet().iterator(); iter.hasNext();) { Entry<Object, Object> entry = iter.next(); CompletableFuture<V> future = cache.get(entry.getKey()); Object value = Async.getIfReady(future); if (value == null) { iter.remove(); misses++; } else { entry.setValue(value); } } cache.statsCounter().recordMisses(misses); cache.statsCounter().recordHits(result.size()); @SuppressWarnings("unchecked") Map<K, V> castResult = (Map<K, V>) result; return Collections.unmodifiableMap(castResult); } @Override @SuppressWarnings("PMD.PreserveStackTrace") public V get(K key, Function<? super K, ? extends V> mappingFunction) { requireNonNull(mappingFunction); CompletableFuture<V> future = LocalAsyncLoadingCache.this.get(key, (k, executor) -> CompletableFuture.supplyAsync(() -> mappingFunction.apply(key), executor)); try { return future.get(); } catch (ExecutionException e) { if (e.getCause() instanceof RuntimeException) { throw (RuntimeException) e.getCause(); } else if (e.getCause() instanceof Error) { throw (Error) e.getCause(); } throw new CompletionException(e.getCause()); } catch (InterruptedException e) { throw new CompletionException(e); } } @Override @SuppressWarnings("PMD.PreserveStackTrace") public V get(K key) { try { return LocalAsyncLoadingCache.this.get(key).get(); } catch (ExecutionException e) { if (e.getCause() instanceof RuntimeException) { throw (RuntimeException) e.getCause(); } else if (e.getCause() instanceof Error) { throw (Error) e.getCause(); } throw new CompletionException(e.getCause()); } catch (InterruptedException e) { throw new CompletionException(e); } } @Override @SuppressWarnings("PMD.PreserveStackTrace") public Map<K, V> getAll(Iterable<? extends K> keys) { try { return LocalAsyncLoadingCache.this.getAll(keys).get(); } catch (ExecutionException e) { if (e.getCause() instanceof RuntimeException) { throw (RuntimeException) e.getCause(); } else if (e.getCause() instanceof Error) { throw (Error) e.getCause(); } throw new CompletionException(e.getCause()); } catch (InterruptedException e) { throw new CompletionException(e); } } @Override public void put(K key, V value) { requireNonNull(value); cache.put(key, CompletableFuture.completedFuture(value)); } @Override public void putAll(Map<? extends K, ? extends V> map) { map.forEach(this::put); } @Override public void invalidate(Object key) { cache.remove(key); } @Override public void invalidateAll(Iterable<?> keys) { cache.invalidateAll(keys); } @Override public void invalidateAll() { cache.clear(); } @Override public long estimatedSize() { return cache.size(); } @Override public CacheStats stats() { return cache.statsCounter().snapshot(); } @Override public void cleanUp() { cache.cleanUp(); } @Override @SuppressWarnings("FutureReturnValueIgnored") public void refresh(K key) { requireNonNull(key); long[] writeTime = new long[1]; CompletableFuture<V> oldValueFuture = cache.getIfPresentQuietly(key, writeTime); if ((oldValueFuture == null) || (oldValueFuture.isDone() && oldValueFuture.isCompletedExceptionally())) { LocalAsyncLoadingCache.this.get(key, loader::asyncLoad, /* recordStats */ false); return; } else if (!oldValueFuture.isDone()) { // no-op if load is pending return; } oldValueFuture.thenAccept(oldValue -> { long now = cache.statsTicker().read(); CompletableFuture<V> refreshFuture = (oldValue == null) ? loader.asyncLoad(key, cache.executor()) : loader.asyncReload(key, oldValue, cache.executor()); refreshFuture.whenComplete((newValue, error) -> { long loadTime = cache.statsTicker().read() - now; if (error != null) { cache.statsCounter().recordLoadFailure(loadTime); logger.log(Level.WARNING, "Exception thrown during refresh", error); return; } boolean[] discard = new boolean[1]; cache.compute(key, (k, currentValue) -> { if (currentValue == null) { return (newValue == null) ? null : refreshFuture; } else if (currentValue == oldValueFuture) { long expectedWriteTime = writeTime[0]; if (cache.hasWriteTime()) { cache.getIfPresentQuietly(key, writeTime); } if (writeTime[0] == expectedWriteTime) { return (newValue == null) ? null : refreshFuture; } } discard[0] = true; return currentValue; }, /* recordMiss */ false, /* recordLoad */ false); if (discard[0] && cache.hasRemovalListener()) { cache.notifyRemoval(key, refreshFuture, RemovalCause.REPLACED); } if (newValue == null) { cache.statsCounter().recordLoadFailure(loadTime); } else { cache.statsCounter().recordLoadSuccess(loadTime); } }); }); } @Override public Policy<K, V> policy() { return getOuter().policy(); } @Override public ConcurrentMap<K, V> asMap() { if (asMapView == null) { asMapView = new AsMapView<>(cache); } return asMapView; } } static final class AsMapView<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> { final LocalCache<K, CompletableFuture<V>> delegate; Collection<V> values; Set<Entry<K, V>> entries; AsMapView(LocalCache<K, CompletableFuture<V>> delegate) { this.delegate = delegate; } @Override public boolean isEmpty() { return delegate.isEmpty(); } @Override public int size() { return delegate.size(); } @Override public boolean containsKey(Object key) { return delegate.containsKey(key); } @Override public boolean containsValue(Object value) { requireNonNull(value); for (CompletableFuture<V> valueFuture : delegate.values()) { if (value.equals(Async.getIfReady(valueFuture))) { return true; } } return false; } @Override public V get(Object key) { return Async.getIfReady(delegate.get(key)); } @Override public V putIfAbsent(K key, V value) { requireNonNull(value); CompletableFuture<V> valueFuture = delegate.putIfAbsent(key, CompletableFuture.completedFuture(value)); return Async.getWhenSuccessful(valueFuture); } @Override public V put(K key, V value) { requireNonNull(value); CompletableFuture<V> oldValueFuture = delegate.put(key, CompletableFuture.completedFuture(value)); return Async.getWhenSuccessful(oldValueFuture); } @Override public V remove(Object key) { CompletableFuture<V> oldValueFuture = delegate.remove(key); return Async.getWhenSuccessful(oldValueFuture); } @Override public V replace(K key, V value) { requireNonNull(value); CompletableFuture<V> oldValueFuture = delegate.replace(key, CompletableFuture.completedFuture(value)); return Async.getWhenSuccessful(oldValueFuture); } @Override public boolean replace(K key, V oldValue, V newValue) { requireNonNull(oldValue); requireNonNull(newValue); CompletableFuture<V> oldValueFuture = delegate.get(key); return oldValue.equals(Async.getIfReady(oldValueFuture)) && delegate.replace(key, oldValueFuture, CompletableFuture.completedFuture(newValue)); } @Override public boolean remove(Object key, Object value) { requireNonNull(key); if (value == null) { return false; } CompletableFuture<V> oldValueFuture = delegate.get(key); return value.equals(Async.getIfReady(oldValueFuture)) && delegate.remove(key, oldValueFuture); } @Override public void clear() { delegate.clear(); } @Override public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) { requireNonNull(mappingFunction); CompletableFuture<V> valueFuture = delegate.computeIfAbsent(key, k -> { V newValue = mappingFunction.apply(key); return (newValue == null) ? null : CompletableFuture.completedFuture(newValue); }); return Async.getWhenSuccessful(valueFuture); } @Override public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) { requireNonNull(remappingFunction); CompletableFuture<V> valueFuture = delegate.computeIfPresent(key, (k, oldValueFuture) -> { V oldValue = Async.getWhenSuccessful(oldValueFuture); if (oldValue == null) { return null; } V newValue = remappingFunction.apply(key, oldValue); return (newValue == null) ? null : CompletableFuture.completedFuture(newValue); }); return Async.getWhenSuccessful(valueFuture); } @Override public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) { requireNonNull(remappingFunction); long startTime = delegate.statsTicker().read(); CompletableFuture<V> valueFuture = delegate.compute(key, (k, oldValueFuture) -> { V oldValue = Async.getWhenSuccessful(oldValueFuture); V newValue = remappingFunction.apply(key, oldValue); long loadTime = delegate.statsTicker().read() - startTime; if (newValue == null) { delegate.statsCounter().recordLoadFailure(loadTime); return null; } delegate.statsCounter().recordLoadSuccess(loadTime); return CompletableFuture.completedFuture(newValue); }, /* recordMiss */ false, /* recordLoad */ false); return Async.getWhenSuccessful(valueFuture); } @Override public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) { requireNonNull(value); requireNonNull(remappingFunction); CompletableFuture<V> mergedValueFuture = delegate.merge( key, CompletableFuture.completedFuture(value), (oldValueFuture, valueFuture) -> { V oldValue = Async.getWhenSuccessful(oldValueFuture); if (oldValue == null) { return valueFuture; } V newValue = remappingFunction.apply(oldValue, value); return (newValue == null) ? null : CompletableFuture.completedFuture(newValue); }); return Async.getWhenSuccessful(mergedValueFuture); } @Override public Set<K> keySet() { return delegate.keySet(); } @Override public Collection<V> values() { return (values == null) ? (values = new Values()) : values; } @Override public Set<Entry<K, V>> entrySet() { return (entries == null) ? (entries = new EntrySet()) : entries; } private final class Values extends AbstractCollection<V> { @Override public boolean isEmpty() { return AsMapView.this.isEmpty(); } @Override public int size() { return AsMapView.this.size(); } @Override public boolean contains(Object o) { return AsMapView.this.containsValue(o); } @Override public void clear() { AsMapView.this.clear(); } @Override public Iterator<V> iterator() { return new Iterator<V>() { Iterator<Entry<K, V>> iterator = entrySet().iterator(); @Override public boolean hasNext() { return iterator.hasNext(); } @Override public V next() { return iterator.next().getValue(); } @Override public void remove() { iterator.remove(); } }; } } private final class EntrySet extends AbstractSet<Entry<K, V>> { @Override public boolean isEmpty() { return AsMapView.this.isEmpty(); } @Override public int size() { return AsMapView.this.size(); } @Override public boolean contains(Object o) { if (!(o instanceof Entry<?, ?>)) { return false; } Entry<?, ?> entry = (Entry<?, ?>) o; V value = AsMapView.this.get(entry.getKey()); return (value != null) && value.equals(entry.getValue()); } @Override public boolean remove(Object obj) { if (!(obj instanceof Entry<?, ?>)) { return false; } Entry<?, ?> entry = (Entry<?, ?>) obj; return AsMapView.this.remove(entry.getKey(), entry.getValue()); } @Override public void clear() { AsMapView.this.clear(); } @Override public Iterator<Entry<K, V>> iterator() { return new Iterator<Entry<K, V>>() { Iterator<Entry<K, CompletableFuture<V>>> iterator = delegate.entrySet().iterator(); Entry<K, V> cursor; K removalKey; @Override public boolean hasNext() { while ((cursor == null) && iterator.hasNext()) { Entry<K, CompletableFuture<V>> entry = iterator.next(); V value = Async.getIfReady(entry.getValue()); if (value != null) { cursor = new WriteThroughEntry<>(AsMapView.this, entry.getKey(), value); } } return (cursor != null); } @Override public Entry<K, V> next() { if (!hasNext()) { throw new NoSuchElementException(); } removalKey = cursor.getKey(); Entry<K, V> entry = cursor; cursor = null; return entry; } @Override public void remove() { Caffeine.requireState(removalKey != null); delegate.remove(removalKey); removalKey = null; } }; } } } }