From eb44628f9b59d2f786f697b57da304be3d52170b Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sun, 28 May 2017 16:47:04 -0700 Subject: [PATCH] Fix variable expiration with async cache (fixes #159) An in-flight future was mistakenly given the maximum expiry allowed, causing it to not honor an expire-after-create setting. Instead it was supposed to be beyond the maximum to signal adaption on the completion update. The calculations for fixed expiration was made more robust to the time rolling over. This now complies with System.nanoTime() warnings. Strengthened the remove and replace operations to be more predictably linearizable. This removed optimizations to avoid unnecessary work by checking if the entry was present in a lock-free manner. Since the hash table supresses loads until complete, that might mean that a call to remove a loading entry was not performed. The contract allows either, so the optimization is left to user code and gives preference to those who need the linearizable behavior. (See #156) --- .../caffeine/cache/BoundedLocalCache.java | 40 +++--- .../cache/LocalAsyncLoadingCache.java | 52 +++++-- .../caffeine/cache/UnboundedLocalCache.java | 10 +- .../caffeine/cache/ExpirationTest.java | 136 +++++++++++------- .../caffeine/cache/ExpireAfterVarTest.java | 124 ++++++++++++++++ .../caffeine/cache/testing/CacheSpec.java | 13 +- gradle/dependencies.gradle | 10 +- .../cache/LocalCacheMapComputeTest.java | 86 +++++++++++ 8 files changed, 365 insertions(+), 106 deletions(-) create mode 100644 guava/src/test/java/com/google/common/cache/LocalCacheMapComputeTest.java 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 84ed3afd8c..aaa8af7d35 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 @@ -678,21 +678,20 @@ void expireAfterAccessEntries(long now) { return; } - long expirationTime = (now - expiresAfterAccessNanos()); - expireAfterAccessEntries(accessOrderEdenDeque(), expirationTime, now); + expireAfterAccessEntries(accessOrderEdenDeque(), now); if (evicts()) { - expireAfterAccessEntries(accessOrderProbationDeque(), expirationTime, now); - expireAfterAccessEntries(accessOrderProtectedDeque(), expirationTime, now); + expireAfterAccessEntries(accessOrderProbationDeque(), now); + expireAfterAccessEntries(accessOrderProtectedDeque(), now); } } /** Expires entries in an access-order queue. */ @GuardedBy("evictionLock") - void expireAfterAccessEntries(AccessOrderDeque> accessOrderDeque, - long expirationTime, long now) { + void expireAfterAccessEntries(AccessOrderDeque> accessOrderDeque, long now) { + long duration = expiresAfterAccessNanos(); for (;;) { Node node = accessOrderDeque.peekFirst(); - if ((node == null) || (node.getAccessTime() > expirationTime)) { + if ((node == null) || ((now - node.getAccessTime()) < duration)) { return; } evictEntry(node, RemovalCause.EXPIRED, now); @@ -705,10 +704,10 @@ void expireAfterWriteEntries(long now) { if (!expiresAfterWrite()) { return; } - long expirationTime = now - expiresAfterWriteNanos(); + long duration = expiresAfterWriteNanos(); for (;;) { final Node node = writeOrderDeque().peekFirst(); - if ((node == null) || (node.getWriteTime() > expirationTime)) { + if ((node == null) || ((now - node.getWriteTime()) < duration)) { break; } evictEntry(node, RemovalCause.EXPIRED, now); @@ -762,12 +761,10 @@ boolean evictEntry(Node node, RemovalCause cause, long now) { if (actualCause[0] == RemovalCause.EXPIRED) { boolean expired = false; if (expiresAfterAccess()) { - long expirationTime = now - expiresAfterAccessNanos(); - expired |= (n.getAccessTime() <= expirationTime); + expired |= ((now - n.getAccessTime()) >= expiresAfterAccessNanos()); } if (expiresAfterWrite()) { - long expirationTime = now - expiresAfterWriteNanos(); - expired |= (n.getWriteTime() <= expirationTime); + expired |= ((now - n.getWriteTime()) >= expiresAfterWriteNanos()); } if (expiresVariable()) { expired |= (n.getVariableTime() <= now); @@ -1333,10 +1330,10 @@ public void run() { if (isComputingAsync(node)) { synchronized (node) { if (!Async.isReady((CompletableFuture) node.getValue())) { - long expirationTime = expirationTicker().read() + Async.MAXIMUM_EXPIRY; - setWriteTime(node, expirationTime); - setAccessTime(node, expirationTime); + long expirationTime = expirationTicker().read() + Long.MAX_VALUE; setVariableTime(node, expirationTime); + setAccessTime(node, expirationTime); + setWriteTime(node, expirationTime); } } } @@ -1745,9 +1742,8 @@ public V remove(Object key) { * @return the removed value or null if no mapping was found */ V removeNoWriter(Object key) { - Node node; - Object lookupKey = nodeFactory.newLookupKey(key); - if (!data.containsKey(lookupKey) || ((node = data.remove(lookupKey)) == null)) { + Node node = data.remove(nodeFactory.newLookupKey(key)); + if (node == null) { return null; } @@ -1822,9 +1818,7 @@ V removeWithWriter(Object key) { @Override public boolean remove(Object key, Object value) { requireNonNull(key); - - Object lookupKey = nodeFactory.newLookupKey(key); - if ((value == null) || !data.containsKey(lookupKey)) { + if (value == null) { return false; } @@ -1837,7 +1831,7 @@ public boolean remove(Object key, Object value) { RemovalCause[] cause = new RemovalCause[1]; long now = expirationTicker().read(); - data.computeIfPresent(lookupKey, (kR, node) -> { + data.computeIfPresent(nodeFactory.newLookupKey(key), (kR, node) -> { synchronized (node) { oldKey[0] = node.getKey(); oldValue[0] = node.getValue(); 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 e8d57947bb..7b90743285 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 @@ -559,6 +559,11 @@ public int size() { return delegate.size(); } + @Override + public void clear() { + delegate.clear(); + } + @Override public boolean containsKey(Object key) { return delegate.containsKey(key); @@ -603,6 +608,28 @@ public V remove(Object key) { return Async.getWhenSuccessful(oldValueFuture); } + @Override + public boolean remove(Object key, Object value) { + requireNonNull(key); + if (value == null) { + return false; + } + CompletableFuture oldValueFuture = delegate.get(key); + if ((oldValueFuture != null) && !value.equals(Async.getWhenSuccessful(oldValueFuture))) { + // Optimistically check if the current value is equal, but don't skip if it may be loading + return false; + } + + @SuppressWarnings("unchecked") + K castedKey = (K) key; + boolean[] removed = { false }; + delegate.compute(castedKey, (k, oldValue) -> { + removed[0] = value.equals(Async.getWhenSuccessful(oldValue)); + return removed[0] ? null : oldValue; + }, /* recordStats */ false, /* recordLoad */ false); + return removed[0]; + } + @Override public V replace(K key, V value) { requireNonNull(value); @@ -616,24 +643,19 @@ public boolean replace(K key, V oldValue, V newValue) { requireNonNull(oldValue); requireNonNull(newValue); CompletableFuture 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) { + if ((oldValueFuture != null) && !oldValue.equals(Async.getWhenSuccessful(oldValueFuture))) { + // Optimistically check if the current value is equal, but don't skip if it may be loading return false; } - CompletableFuture oldValueFuture = delegate.get(key); - return value.equals(Async.getIfReady(oldValueFuture)) - && delegate.remove(key, oldValueFuture); - } - @Override - public void clear() { - delegate.clear(); + @SuppressWarnings("unchecked") + K castedKey = key; + boolean[] replaced = { false }; + delegate.compute(castedKey, (k, value) -> { + replaced[0] = oldValue.equals(Async.getWhenSuccessful(value)); + return replaced[0] ? CompletableFuture.completedFuture(newValue) : value; + }, /* recordStats */ false, /* recordLoad */ false); + return replaced[0]; } @Override 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 b08975518b..d45e4e5a09 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 @@ -327,6 +327,11 @@ public boolean isEmpty() { return data.isEmpty(); } + @Override + public int size() { + return data.size(); + } + @Override public void clear() { if (!hasRemovalListener() && (writer == CacheWriter.disabledWriter())) { @@ -338,11 +343,6 @@ public void clear() { } } - @Override - public int size() { - return data.size(); - } - @Override public boolean containsKey(Object key) { return data.containsKey(key); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java index b0efae5ad8..57b68ed4e7 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpirationTest.java @@ -71,7 +71,7 @@ public final class ExpirationTest { @Test(dataProvider = "caches") @CacheSpec(mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.IMMEDIATELY}, expireAfterWrite = {Expire.DISABLED, Expire.IMMEDIATELY}, expiryTime = Expire.IMMEDIATELY, population = Population.EMPTY) @@ -97,7 +97,7 @@ public void expire_zero(Cache cache, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, writer = Writer.EXCEPTIONAL, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expiryTime = Expire.ONE_MINUTE, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, executorFailure = ExecutorFailure.EXPECTED, removalListener = Listener.REJECTING) @@ -116,7 +116,7 @@ public void getIfPresent_writerFails(Cache cache, CacheContext @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -134,7 +134,7 @@ public void get_writerFails(Cache cache, CacheContext context) @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void put_insert(Cache cache, CacheContext context) { @@ -180,7 +180,7 @@ public void put_replace(Cache cache, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -198,7 +198,7 @@ public void put_writerFails(Cache cache, CacheContext context) @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void putAll_insert(Cache cache, CacheContext context) { @@ -245,7 +245,7 @@ public void putAll_replace(Cache cache, CacheContext context) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -263,7 +263,7 @@ public void putAll_writerFails(Cache cache, CacheContext conte @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void invalidate(Cache cache, CacheContext context) { @@ -279,7 +279,7 @@ public void invalidate(Cache cache, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -297,7 +297,7 @@ public void invalidate_writerFails(Cache cache, CacheContext c @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void invalidateAll(Cache cache, CacheContext context) { @@ -313,7 +313,7 @@ public void invalidateAll(Cache cache, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -331,7 +331,7 @@ public void invalidateAll_writerFails(Cache cache, CacheContex @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void invalidateAll_full(Cache cache, CacheContext context) { @@ -347,7 +347,7 @@ public void invalidateAll_full(Cache cache, CacheContext conte @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -365,7 +365,7 @@ public void invalidateAll_full_writerFails(Cache cache, CacheC @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, expiryTime = Expire.ONE_MINUTE) @@ -377,7 +377,7 @@ public void estimatedSize(Cache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, expiryTime = Expire.ONE_MINUTE) @@ -395,7 +395,7 @@ public void cleanUp(Cache cache, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -413,7 +413,7 @@ public void cleanUp_writerFails(Cache cache, CacheContext cont @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -432,7 +432,7 @@ public void get_writerFails(LoadingCache cache, CacheContext c @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -450,7 +450,7 @@ public void getAll_writerFails(LoadingCache cache, CacheContex @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, loader = Loader.IDENTITY, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void refresh(LoadingCache cache, CacheContext context) { @@ -469,7 +469,7 @@ public void refresh(LoadingCache cache, CacheContext context) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -487,7 +487,7 @@ public void refresh_writerFails(LoadingCache cache, CacheConte @CacheSpec(population = Population.FULL, loader = Loader.IDENTITY, removalListener = Listener.CONSUMING, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) @SuppressWarnings("FutureReturnValueIgnored") @@ -506,7 +506,7 @@ public void get(AsyncLoadingCache cache, CacheContext context) @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, removalListener = Listener.CONSUMING, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expiryTime = Expire.ONE_MINUTE, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) @SuppressWarnings("FutureReturnValueIgnored") @@ -530,12 +530,12 @@ public void get_async(AsyncLoadingCache cache, CacheContext co } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, removalListener = Listener.CONSUMING, + @CacheSpec(population = Population.SINGLETON, removalListener = Listener.CONSUMING, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, - expiryTime = Expire.ONE_MINUTE, loader = {Loader.IDENTITY, Loader.BULK_IDENTITY}) + expiryTime = Expire.ONE_MINUTE, loader = {Loader.BULK_IDENTITY}) @SuppressWarnings("FutureReturnValueIgnored") public void getAll(AsyncLoadingCache cache, CacheContext context) { Set keys = context.firstMiddleLastKeys(); @@ -550,7 +550,7 @@ public void getAll(AsyncLoadingCache cache, CacheContext conte @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expiryTime = Expire.ONE_MINUTE, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void put_insert(AsyncLoadingCache cache, CacheContext context) { @@ -563,6 +563,32 @@ public void put_insert(AsyncLoadingCache cache, CacheContext c assertThat(cache, hasRemovalNotifications(context, count, RemovalCause.EXPIRED)); } + @Test(dataProvider = "caches") + @CacheSpec(population = Population.EMPTY, removalListener = Listener.CONSUMING, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expiryTime = Expire.ONE_MINUTE, + expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) + @SuppressWarnings("FutureReturnValueIgnored") + public void put_insert_async(AsyncLoadingCache cache, CacheContext context) { + CompletableFuture future = new CompletableFuture(); + cache.put(context.absentKey(), future); + context.ticker().advance(2, TimeUnit.MINUTES); + cache.synchronous().cleanUp(); + + assertThat(cache, hasRemovalNotifications(context, 0, RemovalCause.EXPIRED)); + future.complete(context.absentValue()); + context.ticker().advance(30, TimeUnit.SECONDS); + assertThat(cache.getIfPresent(context.absentKey()), is(future)); + + context.ticker().advance(1, TimeUnit.MINUTES); + assertThat(cache.getIfPresent(context.absentKey()), is(nullValue())); + + cache.synchronous().cleanUp(); + assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.EXPIRED)); + verifyWriter(context, (verifier, writer) -> verifier.deletions(1, RemovalCause.EXPIRED)); + } + @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, @@ -593,7 +619,7 @@ public void put_replace(AsyncLoadingCache cache, CacheContext @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void isEmpty(Map map, CacheContext context) { @@ -604,7 +630,7 @@ public void isEmpty(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void size(Map map, CacheContext context) { @@ -615,7 +641,7 @@ public void size(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void containsKey(Map map, CacheContext context) { @@ -626,7 +652,7 @@ public void containsKey(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void containsValue(Map map, CacheContext context) { @@ -637,7 +663,7 @@ public void containsValue(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void clear(Map map, CacheContext context) { @@ -653,7 +679,7 @@ public void clear(Map map, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -672,7 +698,7 @@ public void clear_writerFails(Map map, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -690,7 +716,7 @@ public void putIfAbsent_writerFails(Map map, CacheContext cont @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void put_insert(Map map, CacheContext context) { @@ -735,7 +761,7 @@ public void put_replace(Map map, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -753,7 +779,7 @@ public void put_writerFails(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void replace(Map map, CacheContext context) { @@ -791,7 +817,7 @@ public void replace_updated(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void replaceConditionally(Map map, CacheContext context) { @@ -831,7 +857,7 @@ public void replaceConditionally_updated(Map map, CacheContext @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void remove(Map map, CacheContext context) { @@ -847,7 +873,7 @@ public void remove(Map map, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, compute = Compute.SYNC, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -865,7 +891,7 @@ public void remove_writerFails(Map map, CacheContext context) @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void removeConditionally(Map map, CacheContext context) { @@ -882,7 +908,7 @@ public void removeConditionally(Map map, CacheContext context) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -901,7 +927,7 @@ public void removeConditionally_writerFails(Map map, CacheCont @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void computeIfAbsent(Map map, CacheContext context) { @@ -919,7 +945,7 @@ public void computeIfAbsent(Map map, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -938,7 +964,7 @@ public void computeIfAbsent_writerFails(Map map, CacheContext @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void computeIfPresent(Map map, CacheContext context) { @@ -961,7 +987,7 @@ public void computeIfPresent(Map map, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, @@ -981,7 +1007,7 @@ public void computeIfPresent_writerFails(Map map, CacheContext @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void compute(Map map, CacheContext context) { @@ -1002,7 +1028,7 @@ public void compute(Map map, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -1021,7 +1047,7 @@ public void compute_writerFails(Map map, CacheContext context) @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void merge(Map map, CacheContext context) { @@ -1041,7 +1067,7 @@ public void merge(Map map, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -1061,7 +1087,7 @@ public void merge_writerFails(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void iterators(Map map, CacheContext context) { @@ -1077,7 +1103,7 @@ public void iterators(Map map, CacheContext context) { @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void putIfAbsent_weighted(Cache> cache, CacheContext context) { @@ -1092,7 +1118,7 @@ public void putIfAbsent_weighted(Cache> cache, CacheConte @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void put_weighted(Cache> cache, CacheContext context) { @@ -1107,7 +1133,7 @@ public void put_weighted(Cache> cache, CacheContext conte @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void computeIfAbsent_weighted(Cache> cache, CacheContext context) { @@ -1122,7 +1148,7 @@ public void computeIfAbsent_weighted(Cache> cache, CacheC @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void compute_weighted(Cache> cache, CacheContext context) { @@ -1137,7 +1163,7 @@ public void compute_weighted(Cache> cache, CacheContext c @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, expiryTime = Expire.ONE_MINUTE, mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, - expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.CREATE, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void merge_weighted(Cache> cache, CacheContext context) { diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java index fc92235d7b..5c49f8349b 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterVarTest.java @@ -15,13 +15,18 @@ */ package com.github.benmanes.caffeine.cache; +import static com.github.benmanes.caffeine.cache.testing.CacheWriterVerifier.verifyWriter; +import static com.github.benmanes.caffeine.cache.testing.HasRemovalNotifications.hasRemovalNotifications; import static com.github.benmanes.caffeine.testing.IsEmptyMap.emptyMap; +import static com.github.benmanes.caffeine.testing.IsFutureValue.futureOf; import static java.util.concurrent.TimeUnit.NANOSECONDS; import static java.util.function.Function.identity; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; @@ -34,6 +39,7 @@ import java.util.Optional; import java.util.OptionalLong; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import org.testng.annotations.Listeners; @@ -51,6 +57,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheSpec.Writer; import com.github.benmanes.caffeine.cache.testing.CacheValidationListener; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; /** @@ -62,6 +69,123 @@ @Test(dataProviderClass = CacheProvider.class) public final class ExpireAfterVarTest { + /* ---------------- Create -------------- */ + + @Test(dataProvider = "caches") + @CacheSpec(population = Population.FULL, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.CREATE) + public void put_replace(Cache cache, CacheContext context) { + context.ticker().advance(30, TimeUnit.SECONDS); + + cache.put(context.firstKey(), context.absentValue()); + cache.put(context.absentKey(), context.absentValue()); + context.consumedNotifications().clear(); // Ignore replacement notification + + context.ticker().advance(45, TimeUnit.SECONDS); + assertThat(cache.getIfPresent(context.firstKey()), is(nullValue())); + assertThat(cache.getIfPresent(context.middleKey()), is(nullValue())); + assertThat(cache.getIfPresent(context.absentKey()), is(context.absentValue())); + assertThat(cache.estimatedSize(), is(1L)); + + long count = context.initialSize(); + assertThat(cache, hasRemovalNotifications(context, count, RemovalCause.EXPIRED)); + verifyWriter(context, (verifier, writer) -> verifier.deletions(count, RemovalCause.EXPIRED)); + } + + @Test(dataProvider = "caches") + @CacheSpec(population = Population.FULL, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.CREATE) + public void put_replace(AsyncLoadingCache cache, CacheContext context) { + CompletableFuture future = CompletableFuture.completedFuture(context.absentValue()); + context.ticker().advance(30, TimeUnit.SECONDS); + + cache.put(context.firstKey(), future); + cache.put(context.absentKey(), future); + context.consumedNotifications().clear(); // Ignore replacement notification + + context.ticker().advance(45, TimeUnit.SECONDS); + assertThat(cache.getIfPresent(context.firstKey()), is(nullValue())); + assertThat(cache.getIfPresent(context.middleKey()), is(nullValue())); + assertThat(cache.getIfPresent(context.absentKey()), is(futureOf(context.absentValue()))); + assertThat(cache.synchronous().estimatedSize(), is(1L)); + + long count = context.initialSize(); + assertThat(cache, hasRemovalNotifications(context, count, RemovalCause.EXPIRED)); + verifyWriter(context, (verifier, writer) -> verifier.deletions(count, RemovalCause.EXPIRED)); + } + + @Test(dataProvider = "caches") + @CacheSpec(population = Population.FULL, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.CREATE) + public void put_replace(Map map, CacheContext context) { + context.ticker().advance(30, TimeUnit.SECONDS); + + assertThat(map.put(context.firstKey(), context.absentValue()), is(not(nullValue()))); + assertThat(map.put(context.absentKey(), context.absentValue()), is(nullValue())); + context.consumedNotifications().clear(); // Ignore replacement notification + + context.ticker().advance(45, TimeUnit.SECONDS); + assertThat(map.get(context.firstKey()), is(nullValue())); + assertThat(map.get(context.middleKey()), is(nullValue())); + assertThat(map.get(context.absentKey()), is(context.absentValue())); + assertThat(map.size(), is(1)); + + long count = context.initialSize(); + assertThat(map, hasRemovalNotifications(context, count, RemovalCause.EXPIRED)); + verifyWriter(context, (verifier, writer) -> verifier.deletions(count, RemovalCause.EXPIRED)); + } + + @Test(dataProvider = "caches") + @CacheSpec(population = Population.FULL, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.CREATE) + public void putAll_replace(Cache cache, CacheContext context) { + context.ticker().advance(30, TimeUnit.SECONDS); + + cache.putAll(ImmutableMap.of( + context.firstKey(), context.absentValue(), + context.absentKey(), context.absentValue())); + context.consumedNotifications().clear(); // Ignore replacement notification + + context.ticker().advance(45, TimeUnit.SECONDS); + assertThat(cache.getIfPresent(context.firstKey()), is(nullValue())); + assertThat(cache.getIfPresent(context.middleKey()), is(nullValue())); + assertThat(cache.getIfPresent(context.absentKey()), is(context.absentValue())); + assertThat(cache.estimatedSize(), is(1L)); + + long count = context.initialSize(); + assertThat(cache, hasRemovalNotifications(context, count, RemovalCause.EXPIRED)); + verifyWriter(context, (verifier, writer) -> verifier.deletions(count, RemovalCause.EXPIRED)); + } + + @Test(dataProvider = "caches") + @CacheSpec(population = Population.FULL, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.CREATE) + public void replace_updated(Map map, CacheContext context) { + context.ticker().advance(30, TimeUnit.SECONDS); + assertThat(map.replace(context.firstKey(), context.absentValue()), is(not(nullValue()))); + context.ticker().advance(30, TimeUnit.SECONDS); + + context.cleanUp(); + assertThat(map.size(), is(0)); + long count = context.initialSize(); + verifyWriter(context, (verifier, writer) -> verifier.deletions(count)); + } + + @Test(dataProvider = "caches") + @CacheSpec(population = Population.FULL, + expiryTime = Expire.ONE_MINUTE, expiry = CacheExpiry.CREATE) + public void replaceConditionally_updated(Map map, CacheContext context) { + Integer key = context.firstKey(); + context.ticker().advance(30, TimeUnit.SECONDS); + assertThat(map.replace(key, context.original().get(key), context.absentValue()), is(true)); + context.ticker().advance(30, TimeUnit.SECONDS); + + context.cleanUp(); + assertThat(map, is(emptyMap())); + long count = context.initialSize(); + verifyWriter(context, (verifier, writer) -> verifier.deletions(count)); + } + /* ---------------- Exceptional -------------- */ @CacheSpec(implementation = Implementation.Caffeine, 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 ecb6969a71..64df141dab 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 @@ -286,12 +286,10 @@ enum CacheExpiry { return mock; } }, - ACCESS { + CREATE { @Override public Expiry createExpiry(Expire expiryTime) { return ExpiryBuilder .expiringAfterCreate(expiryTime.timeNanos()) - .expiringAfterUpdate(expiryTime.timeNanos()) - .expiringAfterRead(expiryTime.timeNanos()) .build(); } }, @@ -302,6 +300,15 @@ enum CacheExpiry { .expiringAfterUpdate(expiryTime.timeNanos()) .build(); } + }, + ACCESS { + @Override public Expiry createExpiry(Expire expiryTime) { + return ExpiryBuilder + .expiringAfterCreate(expiryTime.timeNanos()) + .expiringAfterUpdate(expiryTime.timeNanos()) + .expiringAfterRead(expiryTime.timeNanos()) + .build(); + } }; public abstract Expiry createExpiry(Expire expiryTime); diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 4fdc6defda..5f344319c5 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -25,14 +25,14 @@ */ ext { versions = [ - akka: '2.5.1', + akka: '2.5.2', commons_compress: '1.14', commons_lang3: '3.5', config: '1.3.1', error_prone_annotations: '2.0.19', fastutil: '7.2.0', flip_tables: '1.0.2', - guava: '21.0', + guava: '22.0', javapoet: '1.9.0', jcache: '1.0.0', jsr305: '3.0.2', @@ -69,10 +69,10 @@ ext { ohc: '0.6.1', rapidoid: '5.3.4', slf4j: '1.7.25', - tcache: '1.0.1', + tcache: '1.0.3', ] plugin_versions = [ - buildscan: '1.7.1', + buildscan: '1.7.3', buildscan_recipes: '0.2.0', checkstyle: '7.7', coveralls: '2.8.1', @@ -129,7 +129,7 @@ ext { exclude group: 'org.hamcrest' }, osgi_compile: [ - 'org.apache.felix:org.apache.felix.framework:5.6.2', + 'org.apache.felix:org.apache.felix.framework:5.6.4', "org.ops4j.pax.exam:pax-exam-junit4:${test_versions.pax_exam}", ], osgi_runtime: [ diff --git a/guava/src/test/java/com/google/common/cache/LocalCacheMapComputeTest.java b/guava/src/test/java/com/google/common/cache/LocalCacheMapComputeTest.java new file mode 100644 index 0000000000..620503754f --- /dev/null +++ b/guava/src/test/java/com/google/common/cache/LocalCacheMapComputeTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2017 The Guava Authors + * + * 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.common.cache; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.concurrent.TimeUnit; +import java.util.function.IntConsumer; +import java.util.stream.IntStream; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.guava.CaffeinatedGuava; + +import junit.framework.TestCase; + +/** + * Test Java8 map.compute in concurrent cache context. + */ +public class LocalCacheMapComputeTest extends TestCase { + final int count = 10000; + final String delimiter = "-"; + final String key = "key"; + Cache cache; + + // helper + private static void doParallelCacheOp(int count, IntConsumer consumer) { + IntStream.range(0, count).parallel().forEach(consumer); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + this.cache = CaffeinatedGuava.build(Caffeine.newBuilder() + .expireAfterAccess(500000, TimeUnit.MILLISECONDS) + .maximumSize(count)); + } + + public void testComputeIfAbsent() { + // simultaneous insertion for same key, expect 1 winner + doParallelCacheOp(count, n -> { + cache.asMap().computeIfAbsent(key, k -> "value" + n); + }); + assertEquals(1, cache.size()); + } + + public void testComputeIfPresent() { + cache.put(key, "1"); + // simultaneous update for same key, expect count successful updates + doParallelCacheOp(count, n -> { + cache.asMap().computeIfPresent(key, (k, v) -> v + delimiter + n); + }); + assertEquals(1, cache.size()); + assertThat(cache.getIfPresent(key).split(delimiter)).hasLength(count + 1); + } + + public void testUpdates() { + cache.put(key, "1"); + // simultaneous update for same key, some null, some non-null + doParallelCacheOp(count, n -> { + cache.asMap().compute(key, (k, v) -> n % 2 == 0 ? v + delimiter + n : null); + }); + assertTrue(1 >= cache.size()); + } + + public void testCompute() { + cache.put(key, "1"); + // simultaneous deletion + doParallelCacheOp(count, n -> { + cache.asMap().compute(key, (k, v) -> null); + }); + assertEquals(0, cache.size()); + } +}