diff --git a/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/ComputeBenchmark.java b/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/ComputeBenchmark.java index 3ac48c95e3..2fd35e0d66 100644 --- a/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/ComputeBenchmark.java +++ b/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/ComputeBenchmark.java @@ -31,7 +31,7 @@ import com.google.common.base.Throwables; import com.google.common.cache.CacheBuilder; -import com.yahoo.ycsb.generator.IntegerGenerator; +import com.yahoo.ycsb.generator.NumberGenerator; import com.yahoo.ycsb.generator.ScrambledZipfianGenerator; /** @@ -60,9 +60,9 @@ public static class ThreadState { public ComputeBenchmark() { ints = new Integer[SIZE]; - IntegerGenerator generator = new ScrambledZipfianGenerator(ITEMS); + NumberGenerator generator = new ScrambledZipfianGenerator(ITEMS); for (int i = 0; i < SIZE; i++) { - ints[i] = generator.nextInt(); + ints[i] = generator.nextValue().intValue(); } } diff --git a/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/FrequencySketchBenchmark.java b/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/FrequencySketchBenchmark.java index 4e112659a1..07c043ede2 100644 --- a/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/FrequencySketchBenchmark.java +++ b/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/FrequencySketchBenchmark.java @@ -20,7 +20,7 @@ import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; -import com.yahoo.ycsb.generator.IntegerGenerator; +import com.yahoo.ycsb.generator.NumberGenerator; import com.yahoo.ycsb.generator.ScrambledZipfianGenerator; /** @@ -46,9 +46,9 @@ public void setup() { sketch = new FrequencySketch<>(); sketch.ensureCapacity(ITEMS); - IntegerGenerator generator = new ScrambledZipfianGenerator(ITEMS); + NumberGenerator generator = new ScrambledZipfianGenerator(ITEMS); for (int i = 0; i < SIZE; i++) { - ints[i] = generator.nextInt(); + ints[i] = generator.nextValue().intValue(); sketch.increment(i); } } diff --git a/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/GetPutBenchmark.java b/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/GetPutBenchmark.java index 7dfefbe32f..f658a424ba 100644 --- a/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/GetPutBenchmark.java +++ b/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/GetPutBenchmark.java @@ -27,7 +27,7 @@ import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.TearDown; -import com.yahoo.ycsb.generator.IntegerGenerator; +import com.yahoo.ycsb.generator.NumberGenerator; import com.yahoo.ycsb.generator.ScrambledZipfianGenerator; /** @@ -85,9 +85,9 @@ public void setup() { cache.cleanUp(); // Populate with a realistic access distribution - IntegerGenerator generator = new ScrambledZipfianGenerator(ITEMS); + NumberGenerator generator = new ScrambledZipfianGenerator(ITEMS); for (int i = 0; i < SIZE; i++) { - ints[i] = generator.nextInt(); + ints[i] = generator.nextValue().intValue(); cache.put(ints[i], Boolean.TRUE); } } diff --git a/caffeine/src/jmh/java/com/github/benmanes/caffeine/profiler/CacheProfiler.java b/caffeine/src/jmh/java/com/github/benmanes/caffeine/profiler/CacheProfiler.java index 7af43e43bb..c643ae05c4 100644 --- a/caffeine/src/jmh/java/com/github/benmanes/caffeine/profiler/CacheProfiler.java +++ b/caffeine/src/jmh/java/com/github/benmanes/caffeine/profiler/CacheProfiler.java @@ -19,7 +19,7 @@ import com.github.benmanes.caffeine.cache.BasicCache; import com.github.benmanes.caffeine.cache.CacheType; -import com.yahoo.ycsb.generator.IntegerGenerator; +import com.yahoo.ycsb.generator.NumberGenerator; import com.yahoo.ycsb.generator.ScrambledZipfianGenerator; /** @@ -42,9 +42,9 @@ public final class CacheProfiler extends ProfilerHook { CacheProfiler() { ints = new Integer[SIZE]; cache = cacheType.create(2 * SIZE); - IntegerGenerator generator = new ScrambledZipfianGenerator(ITEMS); + NumberGenerator generator = new ScrambledZipfianGenerator(ITEMS); for (int i = 0; i < SIZE; i++) { - ints[i] = generator.nextInt(); + ints[i] = generator.nextValue().intValue(); cache.put(ints[i], Boolean.TRUE); } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncCacheLoader.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncCacheLoader.java new file mode 100644 index 0000000000..c7775d9334 --- /dev/null +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncCacheLoader.java @@ -0,0 +1,98 @@ +/* + * Copyright 2016 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 java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import javax.annotation.Nonnull; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Computes or retrieves values asynchronously, based on a key, for use in populating a + * {@link AsyncLoadingCache}. + *

+ * Most implementations will only need to implement {@link #asyncLoad}. Other methods may be + * overridden as desired. + *

+ * Usage example: + *

{@code
+ *   AsyncCacheLoader loader = (key, executor) ->
+ *       createExpensiveGraphAsync(key, executor);
+ *   AsyncLoadingCache cache = Caffeine.newBuilder().build(loader);
+ * }
+ * + * @author ben.manes@gmail.com (Ben Manes) + */ +@ThreadSafe +@FunctionalInterface +public interface AsyncCacheLoader { + + /** + * Asynchronously computes or retrieves the value corresponding to {@code key}. + * + * @param key the non-null key whose value should be loaded + * @param executor the executor with which the entry is asynchronously loaded + * @return the future value associated with {@code key} + */ + @Nonnull + CompletableFuture asyncLoad(@Nonnull K key, @Nonnull Executor executor); + + /** + * Asynchronously computes or retrieves the values corresponding to {@code keys}. This method is + * called by {@link AsyncLoadingCache#getAll}. + *

+ * If the returned map doesn't contain all requested {@code keys} then the entries it does contain + * will be cached and {@code getAll} will return the partial results. If the returned map contains + * extra keys not present in {@code keys} then all returned entries will be cached, but only the + * entries for {@code keys} will be returned from {@code getAll}. + *

+ * This method should be overridden when bulk retrieval is significantly more efficient than many + * individual lookups. Note that {@link AsyncLoadingCache#getAll} will defer to individual calls + * to {@link AsyncLoadingCache#get} if this method is not overridden. + * + * @param keys the unique, non-null keys whose values should be loaded + * @param executor the executor with which the entries are asynchronously loaded + * @return a future containing the map from each key in {@code keys} to the value associated with + * that key; may not contain null values + */ + @Nonnull + default CompletableFuture> asyncLoadAll( + @Nonnull Iterable keys, @Nonnull Executor executor) { + throw new UnsupportedOperationException(); + } + + /** + * Asynchronously computes or retrieves a replacement value corresponding to an already-cached + * {@code key}. If the replacement value is not found then the mapping will be removed if + * {@code null} is computed. This method is called when an existing cache entry is refreshed by + * {@link Caffeine#refreshAfterWrite}, or through a call to {@link LoadingCache#refresh}. + *

+ * Note: all exceptions thrown by this method will be logged and then swallowed. + * + * @param key the non-null key whose value should be loaded + * @param oldValue the non-null old value corresponding to {@code key} + * @param executor the executor with which the entry is asynchronously loaded + * @return a future containing the new value associated with {@code key}, or containing + * {@code null} if the mapping is to be removed + */ + @Nonnull + default CompletableFuture asyncReload( + @Nonnull K key, @Nonnull V oldValue, @Nonnull Executor executor) { + return asyncLoad(key, executor); + } +} diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncLoadingCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncLoadingCache.java index bc3b478c9f..747e0aea97 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncLoadingCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncLoadingCache.java @@ -93,7 +93,7 @@ CompletableFuture get(@Nonnull K key, * @throws RuntimeException or Error if the mappingFunction does when constructing the future, * in which case the mapping is left unestablished */ - @CheckForNull + @Nonnull CompletableFuture get(@Nonnull K key, @Nonnull BiFunction> mappingFunction); diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java index 86cfb30390..33dc8f045f 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java @@ -2675,14 +2675,14 @@ static final class BoundedLocalAsyncLoadingCache Policy policy; @SuppressWarnings("unchecked") - BoundedLocalAsyncLoadingCache(Caffeine builder, CacheLoader loader) { + BoundedLocalAsyncLoadingCache(Caffeine builder, AsyncCacheLoader loader) { super(LocalCacheFactory.newBoundedLocalCache((Caffeine>) builder, asyncLoader(loader, builder), true), loader); isWeighted = builder.isWeighted(); } private static CacheLoader> asyncLoader( - CacheLoader loader, Caffeine builder) { + AsyncCacheLoader loader, Caffeine builder) { Executor executor = builder.getExecutor(); return key -> loader.asyncLoad(key, executor); } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheLoader.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheLoader.java index 80a2808f5d..ecd033ba03 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheLoader.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheLoader.java @@ -44,7 +44,7 @@ @ThreadSafe @FunctionalInterface @SuppressWarnings("PMD.SignatureDeclareThrowsException") -public interface CacheLoader { +public interface CacheLoader extends AsyncCacheLoader { /** * Computes or retrieves the value corresponding to {@code key}. @@ -96,7 +96,7 @@ default Map loadAll(@Nonnull Iterable keys) throws Exception * @param executor the executor that asynchronously loads the entry * @return the future value associated with {@code key} */ - @Nonnull + @Override @Nonnull default CompletableFuture asyncLoad(@Nonnull K key, @Nonnull Executor executor) { requireNonNull(key); requireNonNull(executor); @@ -129,9 +129,9 @@ default CompletableFuture asyncLoad(@Nonnull K key, @Nonnull Executor executo * @return a future containing the map from each key in {@code keys} to the value associated with * that key; may not contain null values */ - @Nonnull + @Override @Nonnull default CompletableFuture> asyncLoadAll( - Iterable keys, @Nonnull Executor executor) { + @Nonnull Iterable keys, @Nonnull Executor executor) { requireNonNull(keys); requireNonNull(executor); return CompletableFuture.supplyAsync(() -> { @@ -166,4 +166,34 @@ default CompletableFuture> asyncLoadAll( default V reload(@Nonnull K key, @Nonnull V oldValue) throws Exception { return load(key); } + + /** + * Asynchronously computes or retrieves a replacement value corresponding to an already-cached + * {@code key}. If the replacement value is not found then the mapping will be removed if + * {@code null} is computed. This method is called when an existing cache entry is refreshed by + * {@link Caffeine#refreshAfterWrite}, or through a call to {@link LoadingCache#refresh}. + *

+ * Note: all exceptions thrown by this method will be logged and then swallowed. + * + * @param key the non-null key whose value should be loaded + * @param oldValue the non-null old value corresponding to {@code key} + * @param executor the executor with which the entry is asynchronously loaded + * @return a future containing the new value associated with {@code key}, or containing + * {@code null} if the mapping is to be removed + */ + @Override @Nonnull + default CompletableFuture asyncReload( + @Nonnull K key, @Nonnull V oldValue, @Nonnull Executor executor) { + requireNonNull(key); + requireNonNull(executor); + return CompletableFuture.supplyAsync(() -> { + try { + return reload(key, oldValue); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new CompletionException(e); + } + }, executor); + } } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java index 5efcaa6701..ee2c83d3bb 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java @@ -833,9 +833,9 @@ public LoadingCache build( /** * Builds a cache, which either returns a {@link CompletableFuture} already loaded or currently * computing the value for a given key or atomically computes the value asynchronously through a - * supplied mapping function or the supplied {@code CacheLoader}. If the asynchronous - * computation fails then the entry will be automatically removed. Note that multiple threads can - * concurrently load values for distinct keys. + * supplied mapping function or the supplied {@code CacheLoader}. If the asynchronous computation + * fails or computes a {@code null} value then the entry will be automatically removed. Note that + * multiple threads can concurrently load values for distinct keys. *

* This method does not alter the state of this {@code Caffeine} instance, so it can be invoked * again to create multiple independent caches. @@ -850,6 +850,29 @@ public LoadingCache build( @Nonnull public AsyncLoadingCache buildAsync( @Nonnull CacheLoader loader) { + return buildAsync((AsyncCacheLoader) loader); + } + + /** + * Builds a cache, which either returns a {@link CompletableFuture} already loaded or currently + * computing the value for a given key or atomically computes the value asynchronously through a + * supplied mapping function or the supplied {@code AsyncCacheLoader}. If the asynchronous + * computation fails or computes a {@code null} value then the entry will be automatically + * removed. Note that multiple threads can concurrently load values for distinct keys. + *

+ * This method does not alter the state of this {@code Caffeine} instance, so it can be invoked + * again to create multiple independent caches. + * + * @param loader the cache loader used to obtain new values + * @param the key type of the loader + * @param the value type of the loader + * @return a cache having the requested features + * @throws IllegalStateException if the value strength is weak or soft + * @throws NullPointerException if the specified cache loader is null + */ + @Nonnull + public AsyncLoadingCache buildAsync( + @Nonnull AsyncCacheLoader loader) { requireState(valueStrength == null); requireState(writer == null); requireWeightWithWeigher(); diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java index 8beb4fde14..9fc20ddb66 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java @@ -59,12 +59,13 @@ abstract class LocalAsyncLoadingCache loader; + final AsyncCacheLoader loader; + LoadingCacheView localCacheView; @SuppressWarnings("unchecked") - LocalAsyncLoadingCache(C cache, CacheLoader loader) { - this.loader = (CacheLoader) loader; + LocalAsyncLoadingCache(C cache, AsyncCacheLoader loader) { + this.loader = (AsyncCacheLoader) loader; this.canBulkLoad = canBulkLoad(loader); this.cache = cache; } @@ -73,13 +74,24 @@ abstract class LocalAsyncLoadingCache policy(); /** Returns whether the supplied cache loader has bulk load functionality. */ - private static boolean canBulkLoad(CacheLoader loader) { + private static boolean canBulkLoad(AsyncCacheLoader loader) { try { - Method loadAll = loader.getClass().getMethod( - "loadAll", Iterable.class); - Method asyncLoadAll = loader.getClass().getMethod( + 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); - return !loadAll.isDefault() || !asyncLoadAll.isDefault(); + 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; @@ -436,12 +448,26 @@ public void refresh(K key) { BiFunction, CompletableFuture> refreshFunction = (k, oldValueFuture) -> { try { - V oldValue = (oldValueFuture == null) ? null : oldValueFuture.join(); - V newValue = (oldValue == null) ? loader.load(key) : loader.reload(key, oldValue); - return (newValue == null) ? null : CompletableFuture.completedFuture(newValue); + V oldValue = Async.getWhenSuccessful(oldValueFuture); + if (loader instanceof CacheLoader) { + CacheLoader cacheLoader = (CacheLoader) loader; + V newValue = (oldValue == null) + ? cacheLoader.load(key) + : cacheLoader.reload(key, oldValue); + return (newValue == null) ? null : CompletableFuture.completedFuture(newValue); + } else { + // Hint that the async task should be run on this async task's thread + CompletableFuture newValueFuture = (oldValue == null) + ? loader.asyncLoad(key, Runnable::run) + : loader.asyncReload(key, oldValue, Runnable::run); + V newValue = newValueFuture.get(); + return (newValue == null) ? null : newValueFuture; + } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return LocalCache.throwUnchecked(e); + } catch (ExecutionException e) { + return LocalCache.throwUnchecked(e.getCause()); } catch (Exception e) { return LocalCache.throwUnchecked(e); } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalCache.java index 8112a89cab..1a6c1a1668 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalCache.java @@ -179,7 +179,7 @@ default void invalidateAll(Iterable keys) { } @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - static V throwUnchecked(Exception e) throws T { - throw (T) e; + static V throwUnchecked(Throwable t) throws T { + throw (T) t; } } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java index 3bc134197a..5164455d48 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java @@ -17,6 +17,7 @@ import static java.util.Objects.requireNonNull; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -52,7 +53,9 @@ interface LocalLoadingCache, K, V> /** Returns whether the supplied cache loader has bulk load functionality. */ default boolean hasLoadAll(CacheLoader loader) { try { - return !loader.getClass().getMethod("loadAll", Iterable.class).isDefault(); + Method classLoadAll = loader.getClass().getMethod("loadAll", Iterable.class); + Method defaultLoadAll = CacheLoader.class.getMethod("loadAll", Iterable.class); + return !classLoadAll.equals(defaultLoadAll); } catch (NoSuchMethodException | SecurityException e) { logger.log(Level.WARNING, "Cannot determine if CacheLoader can bulk load", e); return false; diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/SerializationProxy.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/SerializationProxy.java index 03204b2a8b..66b62f1aa9 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/SerializationProxy.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/SerializationProxy.java @@ -34,12 +34,12 @@ final class SerializationProxy implements Serializable { boolean weakValues; boolean softValues; Weigher weigher; - CacheLoader loader; CacheWriter writer; boolean isRecordingStats; long expiresAfterWriteNanos; long expiresAfterAccessNanos; long refreshAfterWriteNanos; + AsyncCacheLoader loader; RemovalListener removalListener; long maximumSize = Caffeine.UNSET_INT; long maximumWeight = Caffeine.UNSET_INT; @@ -92,7 +92,9 @@ Object readResolve() { if (async) { return builder.buildAsync(loader); } else if (loader != null) { - return builder.build(loader); + @SuppressWarnings("unchecked") + CacheLoader cacheLoader = (CacheLoader) loader; + return builder.build(cacheLoader); } return builder.build(); } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java index 34cf780bb0..dce03d7819 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java @@ -889,7 +889,7 @@ static final class UnboundedLocalAsyncLoadingCache Policy policy; @SuppressWarnings("unchecked") - UnboundedLocalAsyncLoadingCache(Caffeine builder, CacheLoader loader) { + UnboundedLocalAsyncLoadingCache(Caffeine builder, AsyncCacheLoader loader) { super(new UnboundedLocalCache<>((Caffeine>) builder, true), loader); } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CaffeineTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CaffeineTest.java index cb575fdda1..6edae6fd5d 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CaffeineTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/CaffeineTest.java @@ -115,9 +115,22 @@ public void loading_nullLoader() { /* ---------------- async -------------- */ - @Test(expectedExceptions = NullPointerException.class) + @Test public void async_nullLoader() { - Caffeine.newBuilder().buildAsync(null); + try { + Caffeine.newBuilder().buildAsync((CacheLoader) null); + Assert.fail(); + } catch (NullPointerException expected) {} + + try { + Caffeine.newBuilder().buildAsync((AsyncCacheLoader) null); + Assert.fail(); + } catch (NullPointerException expected) {} + } + + @Test + public void async_asyncLoader() { + Caffeine.newBuilder().buildAsync(loader::asyncLoad); } @Test(expectedExceptions = IllegalStateException.class) diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContext.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContext.java index 078bcdfb74..b6122d844d 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContext.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheContext.java @@ -59,18 +59,22 @@ * @author ben.manes@gmail.com (Ben Manes) */ public final class CacheContext { + final RemovalListener removalListener; + final CacheWriter cacheWriter; final InitialCapacity initialCapacity; + final Map original; final Implementation implementation; final Listener removalListenerType; final CacheExecutor cacheExecutor; final ReferenceType valueStrength; final ReferenceType keyStrength; final TrackingExecutor executor; - final Maximum maximumSize; final Population population; final CacheWeigher weigher; + final Maximum maximumSize; final Expire afterAccess; final Expire afterWrite; + final FakeTicker ticker; final Compute compute; final Advance advance; final Expire refresh; @@ -78,10 +82,7 @@ public final class CacheContext { final Writer writer; final Stats stats; - final FakeTicker ticker; - final Map original; - final CacheWriter cacheWriter; - final RemovalListener removalListener; + final boolean isAsyncLoading; Cache cache; AsyncLoadingCache asyncCache; @@ -101,7 +102,7 @@ public CacheContext(InitialCapacity initialCapacity, Stats stats, CacheWeigher w Maximum maximumSize, Expire afterAccess, Expire afterWrite, Expire refresh, Advance advance, ReferenceType keyStrength, ReferenceType valueStrength, CacheExecutor cacheExecutor, Listener removalListenerType, Population population, - boolean isLoading, Compute compute, Loader loader, Writer writer, + boolean isLoading, boolean isAsyncLoading, Compute compute, Loader loader, Writer writer, Implementation implementation) { this.initialCapacity = requireNonNull(initialCapacity); this.stats = requireNonNull(stats); @@ -119,6 +120,7 @@ public CacheContext(InitialCapacity initialCapacity, Stats stats, CacheWeigher w this.removalListener = removalListenerType.create(); this.population = requireNonNull(population); this.loader = isLoading ? requireNonNull(loader) : null; + this.isAsyncLoading = isAsyncLoading; this.writer = requireNonNull(writer); this.cacheWriter = writer.get(); this.ticker = new FakeTicker(); @@ -287,6 +289,10 @@ public boolean isLoading() { return (loader != null); } + public boolean isAsyncLoading() { + return isAsyncLoading; + } + public Loader loader() { return loader; } @@ -382,6 +388,7 @@ public String toString() { .add("valueStrength", valueStrength) .add("compute", compute) .add("loader", loader) + .add("isAsyncLoading", isAsyncLoading) .add("writer", writer) .add("cacheExecutor", cacheExecutor) .add("removalListener", removalListenerType) diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheGenerator.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheGenerator.java index 23280a7673..9514e1fd52 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheGenerator.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheGenerator.java @@ -78,6 +78,7 @@ public Stream>> generate() { /** Returns the Cartesian set of the possible cache configurations. */ @SuppressWarnings("unchecked") private Set> combinations() { + Set asyncLoading = ImmutableSet.of(true, false); Set statistics = filterTypes(options.stats(), cacheSpec.stats()); Set keys = filterTypes(options.keys(), cacheSpec.keys()); Set values = filterTypes(options.values(), cacheSpec.values()); @@ -96,6 +97,9 @@ private Set> combinations() { ? ImmutableSet.of(Implementation.Caffeine) : ImmutableSet.of(); } + if (computations.equals(ImmutableSet.of(Compute.SYNC))) { + asyncLoading = ImmutableSet.of(false); + } if (computations.isEmpty() || implementations.isEmpty() || keys.isEmpty() || values.isEmpty()) { return ImmutableSet.of(); @@ -115,6 +119,7 @@ private Set> combinations() { ImmutableSet.copyOf(cacheSpec.removalListener()), ImmutableSet.copyOf(cacheSpec.population()), ImmutableSet.of(true, isLoadingOnly), + ImmutableSet.copyOf(asyncLoading), ImmutableSet.copyOf(computations), ImmutableSet.copyOf(cacheSpec.loader()), ImmutableSet.copyOf(cacheSpec.writer()), @@ -149,6 +154,7 @@ private CacheContext newCacheContext(List combination) { (Listener) combination.get(index++), (Population) combination.get(index++), (Boolean) combination.get(index++), + (Boolean) combination.get(index++), (Compute) combination.get(index++), (Loader) combination.get(index++), (Writer) combination.get(index++), @@ -159,14 +165,16 @@ private CacheContext newCacheContext(List combination) { private boolean isCompatible(CacheContext context) { boolean asyncIncompatible = context.isAsync() && ((context.implementation() != Implementation.Caffeine) - || !context.isStrongValues() || !context.isLoading()); + || !context.isStrongValues() || !context.isLoading()); + boolean asyncLoaderIncompatible = context.isAsyncLoading() + && (!context.isAsync() || !context.isLoading()); boolean refreshIncompatible = context.refreshes() && !context.isLoading(); boolean weigherIncompatible = context.isUnbounded() && context.isWeighted(); boolean expirationIncompatible = cacheSpec.requiresExpiration() && !context.expires(); boolean referenceIncompatible = cacheSpec.requiresWeakOrSoft() && (context.isWeakKeys() || context.isWeakValues() || context.isSoftValues()); - boolean skip = asyncIncompatible + boolean skip = asyncIncompatible || asyncLoaderIncompatible || refreshIncompatible || weigherIncompatible || expirationIncompatible || referenceIncompatible; return !skip; diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java index 4f9ce7e7e8..6e9dcd8beb 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java @@ -18,6 +18,8 @@ import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.io.ObjectStreamException; +import java.io.Serializable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.ArrayList; @@ -25,6 +27,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; @@ -32,6 +36,7 @@ import org.mockito.Mockito; +import com.github.benmanes.caffeine.cache.AsyncCacheLoader; import com.github.benmanes.caffeine.cache.CacheLoader; import com.github.benmanes.caffeine.cache.CacheWriter; import com.github.benmanes.caffeine.cache.LoadingCache; @@ -354,32 +359,32 @@ Loader[] loader() default { /** The {@link CacheLoader} for constructing the {@link LoadingCache}. */ enum Loader implements CacheLoader { /** A loader that always returns null (no mapping). */ - NULL(false) { + NULL { @Override public Integer load(Integer key) { return null; } }, /** A loader that returns the key. */ - IDENTITY(false) { + IDENTITY { @Override public Integer load(Integer key) { return key; } }, /** A loader that returns the key's negation. */ - NEGATIVE(false) { + NEGATIVE { @Override public Integer load(Integer key) { return interner.get().intern(-key); } }, /** A loader that always throws an exception. */ - EXCEPTIONAL(false) { + EXCEPTIONAL { @Override public Integer load(Integer key) { throw new IllegalStateException(); } }, /** A loader that always returns null (no mapping). */ - BULK_NULL(true) { + BULK_NULL { @Override public Integer load(Integer key) { throw new UnsupportedOperationException(); } @@ -387,7 +392,7 @@ enum Loader implements CacheLoader { return null; } }, - BULK_IDENTITY(true) { + BULK_IDENTITY { @Override public Integer load(Integer key) { throw new UnsupportedOperationException(); } @@ -397,7 +402,7 @@ enum Loader implements CacheLoader { return result; } }, - BULK_NEGATIVE(true) { + BULK_NEGATIVE { @Override public Integer load(Integer key) { throw new UnsupportedOperationException(); } @@ -408,7 +413,7 @@ enum Loader implements CacheLoader { } }, /** A bulk-only loader that loads more than requested. */ - BULK_NEGATIVE_EXCEEDS(true) { + BULK_NEGATIVE_EXCEEDS { @Override public Integer load(Integer key) { throw new UnsupportedOperationException(); } @@ -422,7 +427,7 @@ enum Loader implements CacheLoader { } }, /** A bulk-only loader that always throws an exception. */ - BULK_EXCEPTIONAL(true) { + BULK_EXCEPTIONAL { @Override public Integer load(Integer key) { throw new UnsupportedOperationException(); } @@ -432,14 +437,55 @@ enum Loader implements CacheLoader { }; private final boolean bulk; + private final AsyncCacheLoader asyncLoader; - private Loader(boolean bulk) { - this.bulk = bulk; + private Loader() { + bulk = name().startsWith("BULK"); + asyncLoader = bulk + ? new BulkSeriazableAsyncCacheLoader(this) + : new SeriazableAsyncCacheLoader(this); } public boolean isBulk() { return bulk; } + + /** Returns a serializable view restricted to the {@link AsyncCacheLoader} interface. */ + public AsyncCacheLoader async() { + return asyncLoader; + } + + private static class SeriazableAsyncCacheLoader + implements AsyncCacheLoader, Serializable { + private static final long serialVersionUID = 1L; + + final Loader loader; + + SeriazableAsyncCacheLoader(Loader loader) { + this.loader = loader; + } + @Override public CompletableFuture asyncLoad(Integer key, Executor executor) { + return loader.asyncLoad(key, executor); + } + private Object readResolve() throws ObjectStreamException { + return loader.asyncLoader; + } + } + + private static final class BulkSeriazableAsyncCacheLoader extends SeriazableAsyncCacheLoader { + private static final long serialVersionUID = 1L; + + BulkSeriazableAsyncCacheLoader(Loader loader) { + super(loader); + } + @Override public CompletableFuture asyncLoad(Integer key, Executor executor) { + throw new IllegalStateException(); + } + @Override public CompletableFuture> asyncLoadAll( + Iterable keys, Executor executor) { + return loader.asyncLoadAll(keys, executor); + } + } } /* ---------------- CacheWriter -------------- */ diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java index 36f9538855..28ef41e788 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheValidationListener.java @@ -71,6 +71,8 @@ public void afterInvocation(IInvokedMethod method, ITestResult testResult) { checkWriter(testResult, context); checkNoStats(testResult, context); checkExecutor(testResult, context); + } else { + testResult.setThrowable(new AssertionError(getTestName(method), testResult.getThrowable())); } } catch (Throwable caught) { testResult.setStatus(ITestResult.FAILURE); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CaffeineCacheFromContext.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CaffeineCacheFromContext.java index 65ff117999..206aefc1fe 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CaffeineCacheFromContext.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CaffeineCacheFromContext.java @@ -85,7 +85,8 @@ public static Cache newCaffeineCache(CacheContext context) { builder.writer(context.cacheWriter()); } if (context.isAsync()) { - context.asyncCache = builder.buildAsync(context.loader); + context.asyncCache = builder.buildAsync( + context.isAsyncLoading ? context.loader.async() : context.loader); context.cache = context.asyncCache.synchronous(); } else if (context.loader == null) { context.cache = builder.build(); diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 235cc463bb..b450b6587c 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -36,8 +36,8 @@ ext { jcache: '1.0.0', jsr305: '3.0.1', stream: '2.9.0', - univocity_parsers: '1.5.6', - ycsb: '0.6.0', + univocity_parsers: '2.0.0', + ycsb: '0.7.0-RC1', ] test_versions = [ awaitility: '1.7.0', @@ -57,7 +57,7 @@ ext { ehcache2: '2.10.1-55', ehcache3: '3.0.0.m4', high_scale_lib: '1.0.6', - infinispan: '8.2.0.Beta1', + infinispan: '8.2.0.Beta2', jackrabbit: '1.3.15', jamm: '0.3.1', java_object_layout: '0.4', diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Synthetic.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Synthetic.java index 0296e1b223..8d492780a0 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Synthetic.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Synthetic.java @@ -22,7 +22,7 @@ import com.yahoo.ycsb.generator.CounterGenerator; import com.yahoo.ycsb.generator.ExponentialGenerator; import com.yahoo.ycsb.generator.HotspotIntegerGenerator; -import com.yahoo.ycsb.generator.IntegerGenerator; +import com.yahoo.ycsb.generator.NumberGenerator; import com.yahoo.ycsb.generator.ScrambledZipfianGenerator; import com.yahoo.ycsb.generator.SkewedLatestGenerator; import com.yahoo.ycsb.generator.UniformIntegerGenerator; @@ -150,7 +150,7 @@ public static LongStream uniform(int lowerBound, int upperBound, int items) { } /** Returns a sequence of items constructed by the generator. */ - private static LongStream generate(IntegerGenerator generator, long count) { - return LongStream.range(0, count).map(ignored -> generator.nextInt()); + private static LongStream generate(NumberGenerator generator, long count) { + return LongStream.range(0, count).map(ignored -> generator.nextValue().intValue()); } }