From d6f0c6d9a18354d8953a06c9c231088c8932f66d Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sun, 30 Apr 2017 16:43:49 -0700 Subject: [PATCH] Variable expiration support (fixes #70, #75, #141) The expiration time can now be customized on a per entry basis to allow them to expire at different rates. This is acheived in O(1) time using a timer wheel and evaluating using the new Expiry interface. This setting can be combined with refreshAfterWrite, but is incompatible with the fixed expiration types (expireAfterAccess, expireAfterWrite). While the test suite was updated to incorporate this new configuration option, there is still remaining work before this should be released. - New tests specific to this feature (such as exceptional conditions) have not yet been written - Incorporate a data integrity check for the timer wheel into the validation listener - Inspection through cache.policy() - JCache integration - Documentation --- README.md | 2 +- .../caffeine/cache/NodeSelectorCode.java | 22 +- .../cache/local/AddExpireAfterAccess.java | 2 +- .../caffeine/cache/node/AddDeques.java | 4 +- .../caffeine/cache/node/AddExpiration.java | 61 +++- .../benmanes/caffeine/cache/node/AddKey.java | 5 +- .../caffeine/cache/node/AddMaximum.java | 6 +- .../caffeine/cache/node/AddToString.java | 11 +- .../caffeine/cache/node/AddValue.java | 5 +- .../caffeine/cache/TimerWheelBenchmark.java | 38 +-- .../github/benmanes/caffeine/cache/Async.java | 53 ++++ .../caffeine/cache/BoundedLocalCache.java | 62 ++-- .../benmanes/caffeine/cache/Caffeine.java | 15 +- .../benmanes/caffeine/cache/Expiry.java | 52 +--- .../github/benmanes/caffeine/cache/Node.java | 41 ++- .../benmanes/caffeine/cache/TimerWheel.java | 105 ++++--- .../caffeine/cache/BoundedLocalCacheTest.java | 5 +- .../caffeine/cache/ExpirationTest.java | 282 +++++++++++++----- .../caffeine/cache/ExpireAfterAccessTest.java | 97 ++++-- .../caffeine/cache/ExpireAfterWriteTest.java | 103 +++++-- .../caffeine/cache/IsCacheReserializable.java | 33 +- .../caffeine/cache/TimerWheelTest.java | 50 ++-- .../caffeine/cache/testing/CacheContext.java | 49 ++- .../cache/testing/CacheGenerator.java | 16 +- .../caffeine/cache/testing/CacheSpec.java | 80 ++++- .../testing/CaffeineCacheFromContext.java | 4 + .../caffeine/cache/testing/ExpiryBuilder.java | 85 ++++++ gradle/dependencies.gradle | 12 +- gradle/jmh.gradle | 2 +- .../jcache/copy/JavaSerializationCopier.java | 2 +- 30 files changed, 925 insertions(+), 379 deletions(-) create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/ExpiryBuilder.java diff --git a/README.md b/README.md index ef6a26c452..9a5e7a93c8 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Snapshots of the development version are available in [solr]: https://issues.apache.org/jira/browse/SOLR-8241 [infinispan]: http://infinispan.org/docs/stable/user_guide/user_guide.html#eviction_strategy [neo4j]: https://github.com/neo4j/neo4j -[ohc]: https://github.com/snazy/ohc/issues/34 +[ohc]: https://github.com/snazy/ohc [go-tinylfu]: https://github.com/dgryski/go-tinylfu [mango-cache]: https://github.com/goburrow/cache [ratpack]: https://github.com/ratpack/ratpack diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeSelectorCode.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeSelectorCode.java index 026d81cc64..bf8f5b5a2c 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeSelectorCode.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeSelectorCode.java @@ -49,11 +49,23 @@ private NodeSelectorCode values() { } private NodeSelectorCode expires() { - block.beginControlFlow("if (builder.expiresAfterAccess() || builder.expiresVariable())") - .addStatement("sb.append('A')") - .endControlFlow() - .beginControlFlow("if (builder.expiresAfterWrite())") - .addStatement("sb.append('W')") + block + .beginControlFlow("if (builder.expiresVariable())") + .beginControlFlow("if (builder.refreshes())") + .addStatement("sb.append('A')") + .beginControlFlow("if (builder.evicts())") + .addStatement("sb.append('W')") + .endControlFlow() + .nextControlFlow("else") + .addStatement("sb.append('W')") + .endControlFlow() + .nextControlFlow("else") + .beginControlFlow("if (builder.expiresAfterAccess())") + .addStatement("sb.append('A')") + .endControlFlow() + .beginControlFlow("if (builder.expiresAfterWrite())") + .addStatement("sb.append('W')") + .endControlFlow() .endControlFlow() .beginControlFlow("if (builder.refreshes())") .addStatement("sb.append('R')") diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/local/AddExpireAfterAccess.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/local/AddExpireAfterAccess.java index 1ac8265916..307421fbb7 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/local/AddExpireAfterAccess.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/local/AddExpireAfterAccess.java @@ -67,7 +67,7 @@ private void variableExpiration() { .returns(boolean.class) .build()); - context.constructor.addStatement("this.expiry = builder.getExpiry()"); + context.constructor.addStatement("this.expiry = builder.getExpiry(isAsync)"); context.cache.addField(FieldSpec.builder(EXPIRY, "expiry", privateFinalModifiers).build()); context.cache.addMethod(MethodSpec.methodBuilder("expiry") .addModifiers(protectedFinalModifiers) diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddDeques.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddDeques.java index fc3b507cc3..a175eea3d2 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddDeques.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddDeques.java @@ -17,8 +17,6 @@ import static com.github.benmanes.caffeine.cache.Specifications.NODE; -import javax.lang.model.element.Modifier; - import com.github.benmanes.caffeine.cache.Feature; /** @@ -49,7 +47,7 @@ protected void execute() { /** Adds a simple field, accessor, and mutator for the variable. */ private void addFieldAndGetter(String varName) { - context.nodeSubtype.addField(NODE, varName, Modifier.PRIVATE) + context.nodeSubtype.addField(NODE, varName) .addMethod(newGetter(Strength.STRONG, NODE, varName, Visibility.IMMEDIATE)) .addMethod(newSetter(NODE, varName, Visibility.IMMEDIATE)); } diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddExpiration.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddExpiration.java index 360c1b59eb..285e311739 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddExpiration.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddExpiration.java @@ -15,9 +15,11 @@ */ package com.github.benmanes.caffeine.cache.node; +import static com.github.benmanes.caffeine.cache.Specifications.NODE; import static com.github.benmanes.caffeine.cache.Specifications.UNSAFE_ACCESS; import static com.github.benmanes.caffeine.cache.Specifications.newFieldOffset; import static com.github.benmanes.caffeine.cache.Specifications.offsetName; +import static org.apache.commons.lang3.StringUtils.capitalize; import javax.lang.model.element.Modifier; @@ -39,19 +41,70 @@ protected boolean applies() { @Override protected void execute() { + addVariableExpiration(); addAccessExpiration(); addWriteExpiration(); addRefreshExpiration(); } + private void addVariableExpiration() { + if (context.generateFeatures.contains(Feature.EXPIRE_ACCESS)) { + addLink("previousInVariableOrder", "previousInAccessOrder"); + addLink("nextInVariableOrder", "nextInAccessOrder"); + addVariableTime("accessTime"); + } else if (context.generateFeatures.contains(Feature.EXPIRE_WRITE)) { + addLink("previousInVariableOrder", "previousInWriteOrder"); + addLink("nextInVariableOrder", "nextInWriteOrder"); + addVariableTime("writeTime"); + } + if (context.parentFeatures.contains(Feature.EXPIRE_ACCESS) + && context.parentFeatures.contains(Feature.EXPIRE_WRITE) + && context.generateFeatures.contains(Feature.REFRESH_WRITE)) { + addLink("previousInVariableOrder", "previousInWriteOrder"); + addLink("nextInVariableOrder", "nextInWriteOrder"); + addVariableTime("accessTime"); + } + } + + private void addLink(String method, String varName) { + MethodSpec getter = MethodSpec.methodBuilder("get" + capitalize(method)) + .addModifiers(Modifier.PUBLIC) + .addStatement("return $N", varName) + .returns(NODE) + .build(); + MethodSpec setter = MethodSpec.methodBuilder("set" + capitalize(method)) + .addModifiers(Modifier.PUBLIC) + .addParameter(NODE, varName) + .addStatement("this.$N = $N", varName, varName) + .build(); + context.nodeSubtype + .addMethod(getter) + .addMethod(setter); + } + + private void addVariableTime(String varName) { + MethodSpec getter = MethodSpec.methodBuilder("getVariableTime") + .addModifiers(Modifier.PUBLIC) + .addStatement("return $N", varName) + .returns(long.class) + .build(); + MethodSpec setter = MethodSpec.methodBuilder("setVariableTime") + .addModifiers(Modifier.PUBLIC) + .addParameter(long.class, varName) + .addStatement("this.$N = $N", varName, varName) + .build(); + context.nodeSubtype + .addMethod(getter) + .addMethod(setter); + } + private void addAccessExpiration() { if (!context.generateFeatures.contains(Feature.EXPIRE_ACCESS)) { return; } context.nodeSubtype.addField(newFieldOffset(context.className, "accessTime")) - .addField(long.class, "accessTime", Modifier.PRIVATE, Modifier.VOLATILE) - .addMethod(newGetter(Strength.STRONG, TypeName.LONG, - "accessTime", Visibility.LAZY)) + .addField(long.class, "accessTime", Modifier.VOLATILE) + .addMethod(newGetter(Strength.STRONG, TypeName.LONG, "accessTime", Visibility.LAZY)) .addMethod(newSetter(TypeName.LONG, "accessTime", Visibility.LAZY)); addTimeConstructorAssignment(context.constructorByKey, "accessTime"); addTimeConstructorAssignment(context.constructorByKeyRef, "accessTime"); @@ -61,7 +114,7 @@ private void addWriteExpiration() { if (!Feature.useWriteTime(context.parentFeatures) && Feature.useWriteTime(context.generateFeatures)) { context.nodeSubtype.addField(newFieldOffset(context.className, "writeTime")) - .addField(long.class, "writeTime", Modifier.PRIVATE, Modifier.VOLATILE) + .addField(long.class, "writeTime", Modifier.VOLATILE) .addMethod(newGetter(Strength.STRONG, TypeName.LONG, "writeTime", Visibility.LAZY)) .addMethod(newSetter(TypeName.LONG, "writeTime", Visibility.LAZY)); addTimeConstructorAssignment(context.constructorByKey, "writeTime"); diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddKey.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddKey.java index a8a63b33ea..d4169eee06 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddKey.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddKey.java @@ -55,10 +55,9 @@ protected void execute() { } private FieldSpec newKeyField() { - Modifier[] modifiers = { Modifier.PRIVATE, Modifier.VOLATILE }; FieldSpec.Builder fieldSpec = isStrongKeys() - ? FieldSpec.builder(kTypeVar, "key", modifiers) - : FieldSpec.builder(keyReferenceType(), "key", modifiers); + ? FieldSpec.builder(kTypeVar, "key", Modifier.VOLATILE) + : FieldSpec.builder(keyReferenceType(), "key", Modifier.VOLATILE); return fieldSpec.build(); } diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddMaximum.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddMaximum.java index 8055aa95d8..7cac4b1fea 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddMaximum.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddMaximum.java @@ -40,7 +40,7 @@ protected void execute() { } private void addQueueFlag() { - context.nodeSubtype.addField(int.class, "queueType", Modifier.PRIVATE); + context.nodeSubtype.addField(int.class, "queueType"); context.nodeSubtype.addMethod(MethodSpec.methodBuilder("getQueueType") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .returns(int.class) @@ -57,13 +57,13 @@ private void addWeight() { if (!context.generateFeatures.contains(Feature.MAXIMUM_WEIGHT)) { return; } - context.nodeSubtype.addField(int.class, "weight", Modifier.PRIVATE) + context.nodeSubtype.addField(int.class, "weight") .addMethod(newGetter(Strength.STRONG, TypeName.INT, "weight", Visibility.IMMEDIATE)) .addMethod(newSetter(TypeName.INT, "weight", Visibility.IMMEDIATE)); context.constructorByKey.addStatement("this.$N = $N", "weight", "weight"); context.constructorByKeyRef.addStatement("this.$N = $N", "weight", "weight"); - context.nodeSubtype.addField(int.class, "policyWeight", Modifier.PRIVATE) + context.nodeSubtype.addField(int.class, "policyWeight") .addMethod(newGetter(Strength.STRONG, TypeName.INT, "policyWeight", Visibility.IMMEDIATE)) .addMethod(newSetter(TypeName.INT, "policyWeight", Visibility.IMMEDIATE)); } diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddToString.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddToString.java index c53a8a2c4c..5d33e3766b 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddToString.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddToString.java @@ -34,11 +34,12 @@ protected boolean applies() { @Override protected void execute() { String statement = "return String.format(\"%s=[key=%s, value=%s, weight=%d, queueType=%,d, " - + "accessTimeNS=%,d, \"\n+ \"writeTimeNS=%,d, prevInAccess=%s, nextInAccess=%s, " - + "prevInWrite=%s, nextInWrite=%s]\",\ngetClass().getSimpleName(), getKey(), getValue(), " - + "getWeight(), getQueueType(), \ngetAccessTime(), getWriteTime(), " - + "getPreviousInAccessOrder() != null,\ngetNextInAccessOrder() != null, " - + "getPreviousInWriteOrder() != null,\ngetNextInWriteOrder() != null)"; + + "accessTimeNS=%,d, \"\n+ \"writeTimeNS=%,d, varTimeNs=%,d, prevInAccess=%s, " + + "nextInAccess=%s, prevInWrite=%s, \"\n+ \"nextInWrite=%s]\", getClass().getSimpleName(), " + + "getKey(), getValue(), getWeight(), \ngetQueueType(), getAccessTime(), getWriteTime(), " + + "getVariableTime(), \ngetPreviousInAccessOrder() != null, " + + "getNextInAccessOrder() != null, \ngetPreviousInWriteOrder() != null, " + + "getNextInWriteOrder() != null)"; context.nodeSubtype.addMethod(MethodSpec.methodBuilder("toString") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddValue.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddValue.java index 4123afccd8..76f8652b83 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddValue.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddValue.java @@ -61,10 +61,9 @@ protected void execute() { } private FieldSpec newValueField() { - Modifier[] modifiers = { Modifier.PRIVATE, Modifier.VOLATILE }; FieldSpec.Builder fieldSpec = isStrongValues() - ? FieldSpec.builder(vTypeVar, "value", modifiers) - : FieldSpec.builder(valueReferenceType(), "value", modifiers); + ? FieldSpec.builder(vTypeVar, "value", Modifier.VOLATILE) + : FieldSpec.builder(valueReferenceType(), "value", Modifier.VOLATILE); return fieldSpec.build(); } diff --git a/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/TimerWheelBenchmark.java b/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/TimerWheelBenchmark.java index adab1b81e2..7ab850c147 100644 --- a/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/TimerWheelBenchmark.java +++ b/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/TimerWheelBenchmark.java @@ -56,7 +56,9 @@ public void setup() { timerWheel = new TimerWheel<>(new MockCache()); for (int i = 0; i < SIZE; i++) { times[i] = ThreadLocalRandom.current().nextLong(UPPERBOUND); + timerWheel.schedule(new Timer(times[i])); } + timerWheel.schedule(timer); } @Benchmark @@ -66,44 +68,44 @@ public void findBucket(ThreadState threadState) { @Benchmark public void reschedule(ThreadState threadState) { - timer.setAccessTime(times[threadState.index++ & MASK]); + timer.setVariableTime(times[threadState.index++ & MASK]); timerWheel.reschedule(timer); } @Benchmark public void expire(ThreadState threadState) { - long accessTime = times[threadState.index++ & MASK]; - timer.setAccessTime(accessTime); - timerWheel.nanos = accessTime - DELTA; + long time = times[threadState.index++ & MASK]; + timer.setVariableTime(time); + timerWheel.nanos = (time - DELTA); + timerWheel.advance(time); timerWheel.schedule(timer); - timerWheel.advance(accessTime); } - private static final class Timer implements Node { + static final class Timer implements Node { Node prev; Node next; - long accessTime; + long time; - Timer(long accessTime) { - setAccessTime(accessTime); + Timer(long time) { + setVariableTime(time); } - @Override public long getAccessTime() { - return accessTime; + @Override public long getVariableTime() { + return time; } - @Override public void setAccessTime(long accessTime) { - this.accessTime = accessTime; + @Override public void setVariableTime(long time) { + this.time = time; } - @Override public Node getPreviousInAccessOrder() { + @Override public Node getPreviousInVariableOrder() { return prev; } - @Override public void setPreviousInAccessOrder(@Nullable Node prev) { + @Override public void setPreviousInVariableOrder(@Nullable Node prev) { this.prev = prev; } - @Override public Node getNextInAccessOrder() { + @Override public Node getNextInVariableOrder() { return next; } - @Override public void setNextInAccessOrder(@Nullable Node next) { + @Override public void setNextInVariableOrder(@Nullable Node next) { this.next = next; } @@ -120,7 +122,7 @@ private static final class Timer implements Node { @Override public void die() {} } - private static final class MockCache extends BoundedLocalCache { + static final class MockCache extends BoundedLocalCache { @SuppressWarnings({"unchecked", "rawtypes"}) protected MockCache() { diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Async.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Async.java index 7f9efc30f5..a09e8baf57 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Async.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Async.java @@ -32,6 +32,7 @@ * @author ben.manes@gmail.com (Ben Manes) */ final class Async { + static final long MAXIMUM_EXPIRY = (Long.MAX_VALUE >> 1); // 150 years private Async() {} @@ -111,4 +112,56 @@ Object writeReplace() { return delegate; } } + + /** + * An expiry for asynchronous computations. When the value is being loaded this expiry returns + * {@code Long.MAX_VALUE} to indicate that the entry should not be evicted due to an expiry + * constraint. If the value is computed successfully the entry must be reinserted so that the + * expiration is updated and the expiration timeouts reflect the value once present. The value + * maximum range is reserved to coordinate the asynchronous life cycle. + */ + static final class AsyncExpiry implements Expiry>, Serializable { + private static final long serialVersionUID = 1L; + + final Expiry delegate; + + AsyncExpiry(Expiry delegate) { + this.delegate = requireNonNull(delegate); + } + + @Override + public long expireAfterCreate(K key, CompletableFuture future, long currentTime) { + if (isReady(future)) { + long duration = delegate.expireAfterCreate(key, future.join(), currentTime); + return Math.min(duration, MAXIMUM_EXPIRY); + } + return Long.MAX_VALUE; + } + + @Override + public long expireAfterUpdate(K key, CompletableFuture future, + long currentTime, long currentDuration) { + if (isReady(future)) { + long duration = (currentDuration > MAXIMUM_EXPIRY) + ? delegate.expireAfterCreate(key, future.join(), currentTime) + : delegate.expireAfterUpdate(key, future.join(), currentDuration, currentTime); + return Math.min(duration, MAXIMUM_EXPIRY); + } + return currentDuration; + } + + @Override + public long expireAfterRead(K key, CompletableFuture future, + long currentTime, long currentDuration) { + if (isReady(future) && (currentDuration > MAXIMUM_EXPIRY)) { + long duration = delegate.expireAfterRead(key, future.join(), currentDuration, currentTime); + return Math.min(duration, MAXIMUM_EXPIRY); + } + return currentDuration; + } + + Object writeReplace() { + return delegate; + } + } } 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 5c32d99fe7..dcd4533534 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 @@ -730,7 +730,7 @@ boolean hasExpired(Node node, long now) { } return (expiresAfterAccess() && (now - node.getAccessTime() >= expiresAfterAccessNanos())) || (expiresAfterWrite() && (now - node.getWriteTime() >= expiresAfterWriteNanos())) - || (expiresVariable() && (now > node.getAccessTime())); + || (expiresVariable() && (now - node.getVariableTime() >= 0)); } /** @@ -763,11 +763,14 @@ boolean evictEntry(Node node, RemovalCause cause, long now) { boolean expired = false; if (expiresAfterAccess()) { long expirationTime = now - expiresAfterAccessNanos(); - expired |= n.getAccessTime() <= expirationTime; + expired |= (n.getAccessTime() <= expirationTime); } if (expiresAfterWrite()) { long expirationTime = now - expiresAfterWriteNanos(); - expired |= n.getWriteTime() <= expirationTime; + expired |= (n.getWriteTime() <= expirationTime); + } + if (expiresVariable()) { + expired |= (n.getVariableTime() <= now); } if (!expired) { resurrect[0] = true; @@ -935,10 +938,10 @@ void expireAfterCreate(Node node, K key, V value, long now) { if (expiresVariable()) { if ((key != null) && (value != null)) { long duration = expiry().expireAfterCreate(key, value, now); - node.setAccessTime(now + duration); + node.setVariableTime(now + duration); } } else { - node.setAccessTime(now); + setAccessTime(node, now); } } @@ -953,12 +956,12 @@ void expireAfterCreate(Node node, K key, V value, long now) { void expireAfterUpdate(Node node, K key, V value, long now) { if (expiresVariable()) { if ((key != null) && (value != null)) { - long currentDuration = Math.min(1, node.getAccessTime() - now); + long currentDuration = Math.max(1, node.getVariableTime() - now); long duration = expiry().expireAfterUpdate(key, value, now, currentDuration); - node.setAccessTime(now + duration); + node.setVariableTime(now + duration); } } else { - node.setAccessTime(now); + setAccessTime(node, now); } } @@ -973,11 +976,23 @@ void expireAfterUpdate(Node node, K key, V value, long now) { void expireAfterRead(Node node, K key, V value, long now) { if (expiresVariable()) { if ((key != null) && (value != null)) { - long currentDuration = Math.min(1, node.getAccessTime() - now); + long currentDuration = Math.max(1, node.getVariableTime() - now); long duration = expiry().expireAfterRead(key, value, now, currentDuration); - node.setAccessTime(now + duration); + node.setVariableTime(now + duration); } } else { + setAccessTime(node, now); + } + } + + void setWriteTime(Node node, long now) { + if (expiresAfterWrite() || refreshAfterWrite()) { + node.setWriteTime(now); + } + } + + void setAccessTime(Node node, long now) { + if (expiresAfterAccess()) { node.setAccessTime(now); } } @@ -991,8 +1006,8 @@ void expireAfterRead(Node node, K key, V value, long now) { */ void afterWrite(@Nullable Node node, Runnable task, long now) { if (node != null) { - node.setAccessTime(now); - node.setWriteTime(now); + setAccessTime(node, now); + setWriteTime(node, now); } if (buffersWrites()) { for (int i = 0; i < WRITE_BUFFER_RETRIES; i++) { @@ -1324,12 +1339,13 @@ public void run() { if (isComputingAsync(node)) { CompletableFuture future = (CompletableFuture) node.getValue(); if (future != null) { - node.setAccessTime(Long.MAX_VALUE); - node.setWriteTime(Long.MAX_VALUE); + node.setVariableTime(Long.MAX_VALUE); + setAccessTime(node, Long.MAX_VALUE); + setWriteTime(node, Long.MAX_VALUE); future.thenRun(() -> { long now = expirationTicker().read(); - node.setAccessTime(now); - node.setWriteTime(now); + setAccessTime(node, now); + setWriteTime(node, now); }); } } @@ -1699,7 +1715,7 @@ V put(K key, V value, boolean notifyWriter, boolean onlyIfAbsent) { if (onlyIfAbsent) { expireAfterRead(prior, key, value, now); } else { - prior.setWriteTime(now); + setWriteTime(prior, now); } afterRead(prior, now, /* recordHit */ false); } @@ -1865,13 +1881,13 @@ public V replace(K key, V value) { return n; } + expireAfterUpdate(n, key, value, now); if (value != oldValue[0]) { - expireAfterUpdate(n, key, value, now); writer.write(nodeKey[0], value); } n.setValue(value, valueReferenceQueue()); - n.setWriteTime(now); n.setWeight(weight); + setWriteTime(n, now); return n; } }); @@ -1920,13 +1936,13 @@ public boolean replace(K key, V oldValue, V newValue) { return n; } + expireAfterUpdate(n, key, newValue, now); if (newValue != prevValue[0]) { - expireAfterUpdate(n, key, newValue, now); writer.write(key, newValue); } n.setValue(newValue, valueReferenceQueue()); n.setWeight(weight); - n.setWriteTime(now); + setWriteTime(n, now); replaced[0] = true; } return n; @@ -2044,7 +2060,7 @@ V doComputeIfAbsent(K key, Object keyRef, expireAfterCreate(n, key, newValue[0], now); n.setValue(newValue[0], valueReferenceQueue()); n.setWeight(weight[1]); - n.setWriteTime(now); + setWriteTime(n, now); return n; } }); @@ -2208,7 +2224,7 @@ V remap(K key, Object keyRef, BiFunction rema } n.setValue(newValue[0], valueReferenceQueue()); n.setWeight(weight[1]); - n.setWriteTime(now); + setWriteTime(n, now); return n; } }); 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 0d25482af2..9768ac7d52 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 @@ -33,6 +33,7 @@ import javax.annotation.Nonnegative; import javax.annotation.Nonnull; +import com.github.benmanes.caffeine.cache.Async.AsyncExpiry; import com.github.benmanes.caffeine.cache.Async.AsyncRemovalListener; import com.github.benmanes.caffeine.cache.Async.AsyncWeigher; import com.github.benmanes.caffeine.cache.stats.CacheStats; @@ -401,7 +402,7 @@ Weigher getWeigher(boolean isAsync) { Weigher delegate = isWeighted() && (weigher != Weigher.singletonWeigher()) ? Weigher.boundedWeigher((Weigher) weigher) : Weigher.singletonWeigher(); - return (Weigher) (isAsync ? new AsyncWeigher<>(delegate) : delegate); + return isAsync ? (Weigher) new AsyncWeigher<>(delegate) : delegate; } /** @@ -591,7 +592,7 @@ boolean expiresAfterAccess() { * @throws IllegalStateException if expiration was already set */ @Nonnull - /* public */ Caffeine expireAfter( + public Caffeine expireAfter( @Nonnull Expiry expiry) { requireNonNull(expiry); requireState(this.expiry == null, "Expiry was already set to %s", this.expiry); @@ -611,8 +612,9 @@ boolean expiresVariable() { } @SuppressWarnings("unchecked") - Expiry getExpiry() { - return (expiry == null) ? Expiry.eternalExpiry() : Expiry.boundedExpiry((Expiry) expiry); + Expiry getExpiry(boolean isAsync) { + Expiry delegate = (expiry == null) ? Expiry.eternalExpiry() : (Expiry) expiry; + return isAsync ? (Expiry) new AsyncExpiry<>(delegate) : delegate; } /** @@ -673,7 +675,9 @@ public Caffeine ticker(@Nonnull Ticker ticker) { @Nonnull Ticker getTicker() { - return expiresAfterAccess() || expiresAfterWrite() || refreshes() || isRecordingStats() + boolean useTicker = expiresVariable() || expiresAfterAccess() + || expiresAfterWrite() || refreshes() || isRecordingStats(); + return useTicker ? (ticker == null) ? Ticker.systemTicker() : ticker : Ticker.disabledTicker(); } @@ -822,6 +826,7 @@ boolean isBounded() { || (maximumWeight != UNSET_INT) || (expireAfterAccessNanos != UNSET_INT) || (expireAfterWriteNanos != UNSET_INT) + || (expiry != null) || (keyStrength != null) || (valueStrength != null); } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java index eb7feae97b..ad558996f1 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java @@ -15,11 +15,6 @@ */ package com.github.benmanes.caffeine.cache; -import static java.util.Objects.requireNonNull; - -import java.io.Serializable; - -import javax.annotation.Nonnull; import javax.annotation.concurrent.ThreadSafe; /** @@ -29,7 +24,7 @@ * @author ben.manes@gmail.com (Ben Manes) */ @ThreadSafe -interface Expiry { +public interface Expiry { /** * Specifies that the entry should be automatically removed from the cache once the duration has @@ -82,19 +77,6 @@ interface Expiry { static Expiry eternalExpiry() { return (Expiry) EternalExpiry.INSTANCE; } - - /** - * Returns an expiry where the minimum duration is non-negative. - * - * @param delegate the expiry to calculating the expiration time with - * @param the type of keys - * @param the type of values - * @return a weigher that enforces that the weight is non-negative - */ - @Nonnull - static Expiry boundedExpiry(@Nonnull Expiry delegate) { - return new BoundedExpiry<>(delegate); - } } enum EternalExpiry implements Expiry { @@ -115,35 +97,3 @@ public long expireAfterRead(Object key, Object value, long currentTime, long cur return Long.MAX_VALUE; } } - -final class BoundedExpiry implements Expiry, Serializable { - static final long serialVersionUID = 1; - final Expiry delegate; - - BoundedExpiry(Expiry delegate) { - this.delegate = requireNonNull(delegate); - } - - @Override - public long expireAfterCreate(K key, V value, long currentTime) { - long duration = delegate.expireAfterCreate(key, value, currentTime); - return (duration < 0) ? 0 : duration; - } - - @Override - public long expireAfterUpdate(K key, V value, long currentTime, long currentDuration) { - long duration = delegate.expireAfterUpdate(key, value, currentTime, currentDuration); - return (duration < 0) ? 0 : duration; - } - - @Override - public long expireAfterRead(K key, V value, long currentTime, long currentDuration) { - long duration = delegate.expireAfterUpdate(key, value, currentTime, currentDuration); - return (duration < 0) ? 0 : duration; - } - - Object writeReplace() { - return delegate; - } -} - diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Node.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Node.java index f76c5ae176..737344ff16 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Node.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Node.java @@ -118,6 +118,39 @@ default void setPolicyWeight(int weight) {} @GuardedBy("this") void die(); + /* ---------------- Variable order -------------- */ + + /** Returns the time that this entry was last accessed, in ns. */ + default long getVariableTime() { + return 0L; + } + + /** + * Sets the variable expiration time in nanoseconds. This update may be set lazily and rely on the + * memory fence when the lock is released. + */ + default void setVariableTime(long time) {} + + @GuardedBy("evictionLock") + default @Nullable Node getPreviousInVariableOrder() { + return null; + } + + @GuardedBy("evictionLock") + default void setPreviousInVariableOrder(@Nullable Node prev) { + throw new UnsupportedOperationException(); + } + + @GuardedBy("evictionLock") + default @Nullable Node getNextInVariableOrder() { + return null; + } + + @GuardedBy("evictionLock") + default void setNextInVariableOrder(@Nullable Node prev) { + throw new UnsupportedOperationException(); + } + /* ---------------- Access order -------------- */ int EDEN = 0; @@ -172,7 +205,7 @@ default void setAccessTime(long time) {} @Override @GuardedBy("evictionLock") - default Node getPreviousInAccessOrder() { + default @Nullable Node getPreviousInAccessOrder() { return null; } @@ -184,7 +217,7 @@ default void setPreviousInAccessOrder(@Nullable Node prev) { @Override @GuardedBy("evictionLock") - default Node getNextInAccessOrder() { + default @Nullable Node getNextInAccessOrder() { return null; } @@ -217,7 +250,7 @@ default boolean casWriteTime(long expect, long update) { @Override @GuardedBy("evictionLock") - default Node getPreviousInWriteOrder() { + default @Nullable Node getPreviousInWriteOrder() { return null; } @@ -229,7 +262,7 @@ default void setPreviousInWriteOrder(@Nullable Node prev) { @Override @GuardedBy("evictionLock") - default Node getNextInWriteOrder() { + default @Nullable Node getNextInWriteOrder() { return null; } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/TimerWheel.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/TimerWheel.java index af79652396..94a57d8877 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/TimerWheel.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/TimerWheel.java @@ -87,15 +87,19 @@ final class TimerWheel { */ public void advance(long currentTimeNanos) { long previousTimeNanos = nanos; - nanos = currentTimeNanos; - - for (int i = 0; i < SHIFT.length; i++) { - int prevTicks = (int) (previousTimeNanos >>> SHIFT[i]); - int currentTicks = (int) (currentTimeNanos >>> SHIFT[i]); - if ((currentTicks - prevTicks) <= 0) { - break; + try { + nanos = currentTimeNanos; + for (int i = 0; i < SHIFT.length; i++) { + long previousTicks = (previousTimeNanos >> SHIFT[i]); + long currentTicks = (currentTimeNanos >> SHIFT[i]); + if ((currentTicks - previousTicks) <= 0) { + break; + } + expire(i, previousTicks, currentTicks, previousTimeNanos, currentTimeNanos); } - expire(i, previousTimeNanos, currentTimeNanos); + } catch (Throwable t) { + nanos = previousTimeNanos; + throw t; } } @@ -103,10 +107,13 @@ public void advance(long currentTimeNanos) { * Expires entries or reschedules into the proper bucket if still active. * * @param index the wheel being operated on + * @param previousTicks the previous number of ticks + * @param currentTicks the current number of ticks * @param previousTimeNanos the previous time, in nanoseconds * @param currentTimeNanos the current time, in nanoseconds */ - void expire(int index, long previousTimeNanos, long currentTimeNanos) { + void expire(int index, long previousTicks, long currentTicks, + long previousTimeNanos, long currentTimeNanos) { Node[] timerWheel = wheel[index]; int start, end; @@ -114,32 +121,38 @@ void expire(int index, long previousTimeNanos, long currentTimeNanos) { end = timerWheel.length; start = 0; } else { - long previousTicks = (previousTimeNanos >>> SHIFT[index]); - long currentTicks = (currentTimeNanos >>> SHIFT[index]) + SPANS[index]; long mask = SPANS[index] - 1; - start = (int) (previousTicks & mask); - end = start + 1 + (int) (currentTicks & mask); + end = 1 + (int) (currentTicks & mask); } int mask = timerWheel.length - 1; for (int i = start; i < end; i++) { Node sentinel = timerWheel[(i & mask)]; - Node node = sentinel.getNextInAccessOrder(); - sentinel.setPreviousInAccessOrder(sentinel); - sentinel.setNextInAccessOrder(sentinel); + Node prev = sentinel.getPreviousInVariableOrder(); + Node node = sentinel.getNextInVariableOrder(); + sentinel.setPreviousInVariableOrder(sentinel); + sentinel.setNextInVariableOrder(sentinel); while (node != sentinel) { - Node next = node.getNextInAccessOrder(); - node.setPreviousInAccessOrder(null); - node.setNextInAccessOrder(null); - - if ((node.getAccessTime() > currentTimeNanos) - || !cache.evictEntry(node, RemovalCause.EXPIRED, nanos)) { - Node newSentinel = findBucket(node.getAccessTime()); - link(newSentinel, node); + Node next = node.getNextInVariableOrder(); + node.setPreviousInVariableOrder(null); + node.setNextInVariableOrder(null); + + try { + if (((node.getVariableTime() - currentTimeNanos) > 0) + || !cache.evictEntry(node, RemovalCause.EXPIRED, nanos)) { + Node newSentinel = findBucket(node.getVariableTime()); + link(newSentinel, node); + } + node = next; + } catch (Throwable t) { + node.setPreviousInVariableOrder(sentinel.getPreviousInVariableOrder()); + node.setNextInVariableOrder(next); + sentinel.getPreviousInVariableOrder().setNextInVariableOrder(node); + sentinel.setPreviousInVariableOrder(prev); + throw t; } - node = next; } } } @@ -150,7 +163,7 @@ void expire(int index, long previousTimeNanos, long currentTimeNanos) { * @param node the entry in the cache */ public void schedule(Node node) { - Node sentinel = findBucket(node.getAccessTime()); + Node sentinel = findBucket(node.getVariableTime()); link(sentinel, node); } @@ -160,7 +173,7 @@ public void schedule(Node node) { * @param node the entry in the cache */ public void reschedule(Node node) { - if (node.getNextInAccessOrder() != null) { + if (node.getNextInVariableOrder() != null) { unlink(node); schedule(node); } @@ -173,8 +186,8 @@ public void reschedule(Node node) { */ public void deschedule(Node node) { unlink(node); - node.setNextInAccessOrder(null); - node.setPreviousInAccessOrder(null); + node.setNextInVariableOrder(null); + node.setPreviousInVariableOrder(null); } /** @@ -187,7 +200,7 @@ Node findBucket(long time) { long duration = time - nanos; for (int i = 0; i < wheel.length; i++) { if (duration < SPANS[i + 1]) { - int ticks = (int) (time >>> SHIFT[i]); + int ticks = (int) (time >> SHIFT[i]); int index = ticks & (wheel[i].length - 1); return wheel[i][index]; } @@ -196,29 +209,27 @@ Node findBucket(long time) { // Add to the last timer bucket int lastWheel = wheel.length - 1; int buckets = wheel[lastWheel].length - 1; - int ticks = (int) (nanos >>> SHIFT[lastWheel]) - 1; + int ticks = (int) (nanos >> SHIFT[lastWheel]) - 1; int index = (ticks & buckets); return wheel[lastWheel][index]; } /** Adds the entry at the tail of the bucket's list. */ void link(Node sentinel, Node node) { - node.setPreviousInAccessOrder(sentinel.getPreviousInAccessOrder()); - node.setNextInAccessOrder(sentinel); + node.setPreviousInVariableOrder(sentinel.getPreviousInVariableOrder()); + node.setNextInVariableOrder(sentinel); - sentinel.getPreviousInAccessOrder().setNextInAccessOrder(node); - sentinel.setPreviousInAccessOrder(node); + sentinel.getPreviousInVariableOrder().setNextInVariableOrder(node); + sentinel.setPreviousInVariableOrder(node); } /** Removes the entry from its bucket, if scheduled. */ void unlink(Node node) { - Node prev = node.getPreviousInAccessOrder(); - Node next = node.getNextInAccessOrder(); + Node prev = node.getPreviousInVariableOrder(); + Node next = node.getNextInVariableOrder(); if (next != null) { - next.setPreviousInAccessOrder(prev); - } - if (prev != null) { - prev.setNextInAccessOrder(next); + next.setPreviousInVariableOrder(prev); + prev.setNextInVariableOrder(next); } } @@ -229,8 +240,8 @@ public String toString() { Map buckets = new TreeMap<>(); for (int j = 0; j < wheel[i].length; j++) { int events = 0; - for (Node node = wheel[i][j].getNextInAccessOrder(); - node != wheel[i][j]; node = node.getNextInAccessOrder()) { + for (Node node = wheel[i][j].getNextInVariableOrder(); + node != wheel[i][j]; node = node.getNextInVariableOrder()) { events++; } if (events > 0) { @@ -251,16 +262,16 @@ static final class Sentinel implements Node { prev = next = this; } - @Override public Node getPreviousInAccessOrder() { + @Override public Node getPreviousInVariableOrder() { return prev; } - @Override public void setPreviousInAccessOrder(@Nullable Node prev) { + @Override public void setPreviousInVariableOrder(@Nullable Node prev) { this.prev = prev; } - @Override public Node getNextInAccessOrder() { + @Override public Node getNextInVariableOrder() { return next; } - @Override public void setNextInAccessOrder(@Nullable Node next) { + @Override public void setNextInVariableOrder(@Nullable Node next) { this.next = next; } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java index 6139158109..be5d3647d4 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java @@ -51,6 +51,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheProvider; import com.github.benmanes.caffeine.cache.testing.CacheSpec; import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExecutor; +import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExpiry; import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheWeigher; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Compute; import com.github.benmanes.caffeine.cache.testing.CacheSpec.ExecutorFailure; @@ -136,7 +137,7 @@ public void scheduleDrainBuffers_rejected(Cache cache, CacheCo @Test public void putWeighted_noOverflow() { Cache cache = Caffeine.newBuilder() - .executor(CacheExecutor.DIRECT.get()) + .executor(CacheExecutor.DIRECT.create()) .weigher(CacheWeigher.MAX_VALUE) .maximumWeight(Long.MAX_VALUE) .build(); @@ -444,7 +445,7 @@ public void exceedsMaximumBufferSize_onWrite(Cache cache, Cach @CacheSpec(compute = Compute.SYNC, implementation = Implementation.Caffeine, population = Population.EMPTY, maximumSize = Maximum.FULL, weigher = CacheWeigher.DEFAULT, expireAfterAccess = Expire.DISABLED, expireAfterWrite = Expire.DISABLED, - keys = ReferenceType.STRONG, values = ReferenceType.STRONG) + expiry = CacheExpiry.DISABLED, keys = ReferenceType.STRONG, values = ReferenceType.STRONG) public void fastpath(Cache cache, CacheContext context) { BoundedLocalCache localCache = asBoundedLocalCache(cache); assertThat(localCache.skipReadBuffer(), is(true)); 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 7322559761..b0efae5ad8 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 @@ -15,6 +15,9 @@ */ package com.github.benmanes.caffeine.cache; +import static com.github.benmanes.caffeine.cache.testing.CacheSpec.Expiration.AFTER_ACCESS; +import static com.github.benmanes.caffeine.cache.testing.CacheSpec.Expiration.AFTER_WRITE; +import static com.github.benmanes.caffeine.cache.testing.CacheSpec.Expiration.VARIABLE; 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.IsFutureValue.futureOf; @@ -37,6 +40,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheContext; import com.github.benmanes.caffeine.cache.testing.CacheProvider; import com.github.benmanes.caffeine.cache.testing.CacheSpec; +import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExpiry; import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheWeigher; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Compute; import com.github.benmanes.caffeine.cache.testing.CacheSpec.ExecutorFailure; @@ -57,7 +61,7 @@ import com.google.common.collect.Maps; /** - * The test cases for caches that support the expire after read or expire after write policy. + * The test cases for caches that support an expiration policy. * * @author ben.manes@gmail.com (Ben Manes) */ @@ -66,8 +70,11 @@ public final class ExpirationTest { @Test(dataProvider = "caches") - @CacheSpec(requiresExpiration = true, expireAfterAccess = {Expire.DISABLED, Expire.IMMEDIATELY}, - expireAfterWrite = {Expire.DISABLED, Expire.IMMEDIATELY}, population = Population.EMPTY) + @CacheSpec(mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expireAfterAccess = {Expire.DISABLED, Expire.IMMEDIATELY}, + expireAfterWrite = {Expire.DISABLED, Expire.IMMEDIATELY}, + expiryTime = Expire.IMMEDIATELY, population = Population.EMPTY) public void expire_zero(Cache cache, CacheContext context) { cache.put(context.absentKey(), context.absentValue()); if (context.isZeroWeighted() && context.isGuava()) { @@ -75,6 +82,7 @@ public void expire_zero(Cache cache, CacheContext context) { assertThat(cache.estimatedSize(), is(1L)); assertThat(cache, hasRemovalNotifications(context, 0, RemovalCause.EXPIRED)); } else { + runVariableExpiration(context); assertThat(cache.estimatedSize(), is(0L)); assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.EXPIRED)); verifyWriter(context, (verifier, writer) -> { @@ -87,8 +95,10 @@ public void expire_zero(Cache cache, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, writer = Writer.EXCEPTIONAL, requiresExpiration = true, - expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, + population = Population.FULL, writer = Writer.EXCEPTIONAL, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) public void getIfPresent_writerFails(Cache cache, CacheContext context) { @@ -104,7 +114,9 @@ public void getIfPresent_writerFails(Cache cache, CacheContext @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -120,13 +132,16 @@ public void get_writerFails(Cache cache, CacheContext context) } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void put_insert(Cache cache, CacheContext context) { context.ticker().advance(1, TimeUnit.MINUTES); cache.put(context.firstKey(), context.absentValue()); + runVariableExpiration(context); long count = context.initialSize(); assertThat(cache.estimatedSize(), is(1L)); assertThat(cache, hasRemovalNotifications(context, count, RemovalCause.EXPIRED)); @@ -134,7 +149,9 @@ public void put_insert(Cache cache, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void put_replace(Cache cache, CacheContext context) { @@ -161,7 +178,9 @@ public void put_replace(Cache cache, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -177,7 +196,9 @@ public void put_writerFails(Cache cache, CacheContext context) } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void putAll_insert(Cache cache, CacheContext context) { @@ -192,7 +213,9 @@ public void putAll_insert(Cache cache, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void putAll_replace(Cache cache, CacheContext context) { @@ -220,7 +243,9 @@ public void putAll_replace(Cache cache, CacheContext context) @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -236,7 +261,9 @@ public void putAll_writerFails(Cache cache, CacheContext conte } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void invalidate(Cache cache, CacheContext context) { @@ -250,7 +277,9 @@ public void invalidate(Cache cache, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -266,7 +295,9 @@ public void invalidate_writerFails(Cache cache, CacheContext c } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void invalidateAll(Cache cache, CacheContext context) { @@ -280,7 +311,9 @@ public void invalidateAll(Cache cache, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -296,7 +329,9 @@ public void invalidateAll_writerFails(Cache cache, CacheContex } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void invalidateAll_full(Cache cache, CacheContext context) { @@ -310,7 +345,9 @@ public void invalidateAll_full(Cache cache, CacheContext conte @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -327,8 +364,11 @@ public void invalidateAll_full_writerFails(Cache cache, CacheC @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, - requiresExpiration = true, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, - expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, + expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, + expiryTime = Expire.ONE_MINUTE) public void estimatedSize(Cache cache, CacheContext context) { context.ticker().advance(1, TimeUnit.MINUTES); assertThat(cache.estimatedSize(), is(context.initialSize())); @@ -336,8 +376,11 @@ public void estimatedSize(Cache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, - requiresExpiration = true, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, - expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, + expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, + expiryTime = Expire.ONE_MINUTE) public void cleanUp(Cache cache, CacheContext context) { context.ticker().advance(1, TimeUnit.MINUTES); cache.cleanUp(); @@ -350,7 +393,9 @@ public void cleanUp(Cache cache, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -366,7 +411,9 @@ public void cleanUp_writerFails(Cache cache, CacheContext cont @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -383,7 +430,9 @@ public void get_writerFails(LoadingCache cache, CacheContext c @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -399,7 +448,9 @@ public void getAll_writerFails(LoadingCache cache, CacheContex } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, loader = Loader.IDENTITY, requiresExpiration = true, + @CacheSpec(population = Population.FULL, loader = Loader.IDENTITY, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void refresh(LoadingCache cache, CacheContext context) { @@ -416,7 +467,9 @@ public void refresh(LoadingCache cache, CacheContext context) @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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 +485,9 @@ public void refresh_writerFails(LoadingCache cache, CacheConte @Test(dataProvider = "caches") @CacheSpec(population = Population.FULL, loader = Loader.IDENTITY, - removalListener = Listener.CONSUMING, requiresExpiration = true, + removalListener = Listener.CONSUMING, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) @SuppressWarnings("FutureReturnValueIgnored") @@ -450,7 +505,9 @@ public void get(AsyncLoadingCache cache, CacheContext context) @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, removalListener = Listener.CONSUMING, - requiresExpiration = true, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expiryTime = Expire.ONE_MINUTE, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) @SuppressWarnings("FutureReturnValueIgnored") public void get_async(AsyncLoadingCache cache, CacheContext context) { @@ -473,11 +530,12 @@ public void get_async(AsyncLoadingCache cache, CacheContext co } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, - removalListener = Listener.CONSUMING, requiresExpiration = true, + @CacheSpec(population = Population.FULL, removalListener = Listener.CONSUMING, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, - loader = {Loader.IDENTITY, Loader.BULK_IDENTITY}) + expiryTime = Expire.ONE_MINUTE, loader = {Loader.IDENTITY, Loader.BULK_IDENTITY}) @SuppressWarnings("FutureReturnValueIgnored") public void getAll(AsyncLoadingCache cache, CacheContext context) { Set keys = context.firstMiddleLastKeys(); @@ -490,20 +548,25 @@ public void getAll(AsyncLoadingCache cache, CacheContext conte } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, - expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, + @CacheSpec(population = Population.FULL, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) { context.ticker().advance(1, TimeUnit.MINUTES); cache.put(context.firstKey(), CompletableFuture.completedFuture(context.absentValue())); + runVariableExpiration(context); long count = context.initialSize(); assertThat(cache.synchronous().estimatedSize(), is(1L)); assertThat(cache, hasRemovalNotifications(context, count, RemovalCause.EXPIRED)); } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void put_replace(AsyncLoadingCache cache, CacheContext context) { @@ -528,7 +591,9 @@ public void put_replace(AsyncLoadingCache cache, CacheContext /* ---------------- Map -------------- */ @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void isEmpty(Map map, CacheContext context) { @@ -537,7 +602,9 @@ public void isEmpty(Map map, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void size(Map map, CacheContext context) { @@ -546,7 +613,9 @@ public void size(Map map, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void containsKey(Map map, CacheContext context) { @@ -555,7 +624,9 @@ public void containsKey(Map map, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void containsValue(Map map, CacheContext context) { @@ -564,7 +635,9 @@ public void containsValue(Map map, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void clear(Map map, CacheContext context) { @@ -578,7 +651,9 @@ public void clear(Map map, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -595,7 +670,9 @@ public void clear_writerFails(Map map, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -611,7 +688,9 @@ public void putIfAbsent_writerFails(Map map, CacheContext cont } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void put_insert(Map map, CacheContext context) { @@ -625,7 +704,9 @@ public void put_insert(Map map, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void put_replace(Map map, CacheContext context) { @@ -652,7 +733,9 @@ public void put_replace(Map map, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -668,7 +751,9 @@ public void put_writerFails(Map map, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void replace(Map map, CacheContext context) { @@ -685,7 +770,9 @@ public void replace(Map map, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void replace_updated(Map map, CacheContext context) { @@ -702,7 +789,9 @@ public void replace_updated(Map map, CacheContext context) { // replace_writerFail: Not needed due to exiting without side-effects @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void replaceConditionally(Map map, CacheContext context) { @@ -720,7 +809,9 @@ public void replaceConditionally(Map map, CacheContext context } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void replaceConditionally_updated(Map map, CacheContext context) { @@ -738,7 +829,9 @@ public void replaceConditionally_updated(Map map, CacheContext // replaceConditionally_writerFail: Not needed due to exiting without side-effects @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void remove(Map map, CacheContext context) { @@ -752,7 +845,9 @@ public void remove(Map map, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, compute = Compute.SYNC, requiresExpiration = true, + population = Population.FULL, compute = Compute.SYNC, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING) @@ -768,7 +863,9 @@ public void remove_writerFails(Map map, CacheContext context) } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void removeConditionally(Map map, CacheContext context) { @@ -783,7 +880,9 @@ public void removeConditionally(Map map, CacheContext context) @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -800,7 +899,9 @@ public void removeConditionally_writerFails(Map map, CacheCont } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void computeIfAbsent(Map map, CacheContext context) { @@ -816,7 +917,9 @@ public void computeIfAbsent(Map map, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -833,7 +936,9 @@ public void computeIfAbsent_writerFails(Map map, CacheContext } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void computeIfPresent(Map map, CacheContext context) { @@ -854,7 +959,9 @@ public void computeIfPresent(Map map, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}, compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, @@ -872,7 +979,9 @@ public void computeIfPresent_writerFails(Map map, CacheContext } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void compute(Map map, CacheContext context) { @@ -891,7 +1000,9 @@ public void compute(Map map, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -908,7 +1019,9 @@ public void compute_writerFails(Map map, CacheContext context) } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void merge(Map map, CacheContext context) { @@ -926,7 +1039,9 @@ public void merge(Map map, CacheContext context) { @Test(dataProvider = "caches", expectedExceptions = DeleteException.class) @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG, - population = Population.FULL, requiresExpiration = true, + population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, 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) @@ -944,7 +1059,9 @@ public void merge_writerFails(Map map, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(population = Population.FULL, requiresExpiration = true, + @CacheSpec(population = Population.FULL, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void iterators(Map map, CacheContext context) { @@ -958,8 +1075,10 @@ public void iterators(Map map, CacheContext context) { @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, - maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, - requiresExpiration = true, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, + maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void putIfAbsent_weighted(Cache> cache, CacheContext context) { cache.put(1, ImmutableList.of(1)); @@ -971,8 +1090,10 @@ public void putIfAbsent_weighted(Cache> cache, CacheConte @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, - maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, - requiresExpiration = true, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, + maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void put_weighted(Cache> cache, CacheContext context) { cache.put(1, ImmutableList.of(1)); @@ -984,8 +1105,10 @@ public void put_weighted(Cache> cache, CacheContext conte @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, - maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, - requiresExpiration = true, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, + maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void computeIfAbsent_weighted(Cache> cache, CacheContext context) { cache.put(1, ImmutableList.of(1)); @@ -997,8 +1120,10 @@ public void computeIfAbsent_weighted(Cache> cache, CacheC @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, - maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, - requiresExpiration = true, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, + maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void compute_weighted(Cache> cache, CacheContext context) { cache.put(1, ImmutableList.of(1)); @@ -1010,8 +1135,10 @@ public void compute_weighted(Cache> cache, CacheContext c @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, - maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, - requiresExpiration = true, expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, + maximumSize = Maximum.FULL, weigher = CacheWeigher.COLLECTION, expiryTime = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, AFTER_WRITE, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE, CacheExpiry.ACCESS }, + expireAfterAccess = {Expire.DISABLED, Expire.ONE_MINUTE}, expireAfterWrite = {Expire.DISABLED, Expire.ONE_MINUTE}) public void merge_weighted(Cache> cache, CacheContext context) { cache.put(1, ImmutableList.of(1)); @@ -1022,4 +1149,15 @@ public void merge_weighted(Cache> cache, CacheContext con assertThat(cache.policy().eviction().get().weightedSize().getAsLong(), is(3L)); } + + /** + * Ensures that variable expiration is run, as it may not have due to expiring in coarse batches. + */ + private static void runVariableExpiration(CacheContext context) { + if (context.expiresVariably()) { + // Variable expires in coarse buckets at a time + context.ticker().advance(2, TimeUnit.SECONDS); + context.cleanUp(); + } + } } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java index 63193ef723..967b713c66 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterAccessTest.java @@ -15,6 +15,8 @@ */ package com.github.benmanes.caffeine.cache; +import static com.github.benmanes.caffeine.cache.testing.CacheSpec.Expiration.AFTER_ACCESS; +import static com.github.benmanes.caffeine.cache.testing.CacheSpec.Expiration.VARIABLE; 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; @@ -40,6 +42,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheContext; import com.github.benmanes.caffeine.cache.testing.CacheProvider; import com.github.benmanes.caffeine.cache.testing.CacheSpec; +import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExpiry; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Expire; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Implementation; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Listener; @@ -62,8 +65,9 @@ public final class ExpireAfterAccessTest { /* ---------------- Cache -------------- */ @Test(dataProvider = "caches") - @CacheSpec(expireAfterAccess = Expire.ONE_MINUTE, - population = { Population.PARTIAL, Population.FULL }) + @CacheSpec(mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }, expiryTime = Expire.ONE_MINUTE, + expireAfterAccess = Expire.ONE_MINUTE, population = { Population.PARTIAL, Population.FULL }) public void getIfPresent(Cache cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); cache.getIfPresent(context.firstKey()); @@ -80,8 +84,9 @@ public void getIfPresent(Cache cache, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(expireAfterAccess = Expire.ONE_MINUTE, - population = { Population.PARTIAL, Population.FULL }) + @CacheSpec(mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }, expiryTime = Expire.ONE_MINUTE, + expireAfterAccess = Expire.ONE_MINUTE, population = { Population.PARTIAL, Population.FULL }) public void get(Cache cache, CacheContext context) { Function mappingFunction = context.original()::get; context.ticker().advance(30, TimeUnit.SECONDS); @@ -99,8 +104,9 @@ public void get(Cache cache, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(expireAfterAccess = Expire.ONE_MINUTE, - population = { Population.PARTIAL, Population.FULL }) + @CacheSpec(mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }, expiryTime = Expire.ONE_MINUTE, + expireAfterAccess = Expire.ONE_MINUTE, population = { Population.PARTIAL, Population.FULL }) public void getAllPresent(Cache cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); cache.getAllPresent(context.firstMiddleLastKeys()); @@ -118,8 +124,9 @@ public void getAllPresent(Cache cache, CacheContext context) { /* ---------------- LoadingCache -------------- */ @Test(dataProvider = "caches") - @CacheSpec(expireAfterAccess = Expire.ONE_MINUTE, loader = Loader.IDENTITY, - population = { Population.PARTIAL, Population.FULL }) + @CacheSpec(mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }, expireAfterAccess = Expire.ONE_MINUTE, + loader = Loader.IDENTITY, population = { Population.PARTIAL, Population.FULL }) public void get(LoadingCache cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); cache.get(context.firstKey()); @@ -138,8 +145,9 @@ public void get(LoadingCache cache, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(expireAfterAccess = Expire.ONE_MINUTE, - loader = {Loader.IDENTITY, Loader.BULK_IDENTITY}, + @CacheSpec(mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }, expiryTime = Expire.ONE_MINUTE, + expireAfterAccess = Expire.ONE_MINUTE, loader = {Loader.IDENTITY, Loader.BULK_IDENTITY}, population = { Population.PARTIAL, Population.FULL }) public void getAll(LoadingCache cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); @@ -168,8 +176,9 @@ public void getAll(LoadingCache cache, CacheContext context) { /* ---------------- AsyncLoadingCache -------------- */ @Test(dataProvider = "caches") - @CacheSpec(expireAfterAccess = Expire.ONE_MINUTE, - population = { Population.PARTIAL, Population.FULL }) + @CacheSpec(mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }, expiryTime = Expire.ONE_MINUTE, + expireAfterAccess = Expire.ONE_MINUTE, population = { Population.PARTIAL, Population.FULL }) @SuppressWarnings("FutureReturnValueIgnored") public void getIfPresent(AsyncLoadingCache cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); @@ -186,7 +195,9 @@ public void getIfPresent(AsyncLoadingCache cache, CacheContext /* ---------------- Map -------------- */ @Test(dataProvider = "caches") - @CacheSpec(expireAfterAccess = Expire.ONE_MINUTE, population = Population.FULL) + @CacheSpec(mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }, expiryTime = Expire.ONE_MINUTE, + expireAfterAccess = Expire.ONE_MINUTE, population = Population.FULL) public void putIfAbsent(Map map, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); assertThat(map.putIfAbsent(context.firstKey(), context.absentValue()), is(not(nullValue()))); @@ -203,14 +214,18 @@ public void putIfAbsent(Map map, CacheContext context) { /* ---------------- Policy -------------- */ @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }) public void getExpiresAfter(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { assertThat(expireAfterAccess.getExpiresAfter(TimeUnit.MINUTES), is(1L)); } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }) public void setExpiresAfter(Cache cache, CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { expireAfterAccess.setExpiresAfter(2, TimeUnit.MINUTES); @@ -223,7 +238,9 @@ public void setExpiresAfter(Cache cache, CacheContext context, @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE, - population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) + population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }) public void ageOf(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { assertThat(expireAfterAccess.ageOf(context.firstKey(), TimeUnit.SECONDS).getAsLong(), is(0L)); @@ -235,14 +252,18 @@ public void ageOf(CacheContext context, /* ---------------- Policy: oldest -------------- */ - @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) public void oldest_unmodifiable(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { expireAfterAccess.oldest(Integer.MAX_VALUE).clear();; } - @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) public void oldest_negative(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { @@ -250,15 +271,18 @@ public void oldest_negative(CacheContext context, } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }) public void oldest_zero(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { assertThat(expireAfterAccess.oldest(0), is(emptyMap())); } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, - population = Population.FULL, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expireAfterAccess = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }, expiryTime = Expire.ONE_MINUTE) public void oldest_partial(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { int count = (int) context.initialSize() / 2; @@ -268,6 +292,8 @@ public void oldest_partial(CacheContext context, @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, population = {Population.PARTIAL, Population.FULL}, expireAfterAccess = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void oldest_order(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { @@ -276,7 +302,9 @@ public void oldest_order(CacheContext context, } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }) public void oldest_snapshot(Cache cache, CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { Map oldest = expireAfterAccess.oldest(Integer.MAX_VALUE); @@ -286,14 +314,18 @@ public void oldest_snapshot(Cache cache, CacheContext context, /* ---------------- Policy: youngest -------------- */ - @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) public void youngest_unmodifiable(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { expireAfterAccess.youngest(Integer.MAX_VALUE).clear();; } - @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) public void youngest_negative(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { @@ -301,15 +333,18 @@ public void youngest_negative(CacheContext context, } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }) public void youngest_zero(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { assertThat(expireAfterAccess.youngest(0), is(emptyMap())); } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, - population = Population.FULL, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }, expireAfterAccess = Expire.ONE_MINUTE) public void youngest_partial(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { int count = (int) context.initialSize() / 2; @@ -318,7 +353,9 @@ public void youngest_partial(CacheContext context, @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, - population = {Population.PARTIAL, Population.FULL}, expireAfterAccess = Expire.ONE_MINUTE, + population = {Population.PARTIAL, Population.FULL}, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expireAfterAccess = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }, expiryTime = Expire.ONE_MINUTE, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void youngest_order(CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { @@ -328,7 +365,9 @@ public void youngest_order(CacheContext context, } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, expireAfterAccess = Expire.ONE_MINUTE, + mustExpiresWithAnyOf = { AFTER_ACCESS, VARIABLE }, expiryTime = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.ACCESS }) public void youngest_snapshot(Cache cache, CacheContext context, @ExpireAfterAccess Expiration expireAfterAccess) { Map youngest = expireAfterAccess.youngest(Integer.MAX_VALUE); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java index e037339ac6..a15fb82cc3 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/ExpireAfterWriteTest.java @@ -15,6 +15,8 @@ */ package com.github.benmanes.caffeine.cache; +import static com.github.benmanes.caffeine.cache.testing.CacheSpec.Expiration.AFTER_WRITE; +import static com.github.benmanes.caffeine.cache.testing.CacheSpec.Expiration.VARIABLE; 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; @@ -38,6 +40,7 @@ import com.github.benmanes.caffeine.cache.testing.CacheContext; import com.github.benmanes.caffeine.cache.testing.CacheProvider; import com.github.benmanes.caffeine.cache.testing.CacheSpec; +import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExpiry; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Expire; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Implementation; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Listener; @@ -61,7 +64,9 @@ public final class ExpireAfterWriteTest { /* ---------------- Cache -------------- */ @Test(dataProvider = "caches") - @CacheSpec(expireAfterWrite = Expire.ONE_MINUTE, + @CacheSpec(mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, + expireAfterWrite = { Expire.DISABLED, Expire.ONE_MINUTE }, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE, population = { Population.PARTIAL, Population.FULL }) public void getIfPresent(Cache cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); @@ -78,8 +83,9 @@ public void getIfPresent(Cache cache, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(expireAfterWrite = Expire.ONE_MINUTE, - population = { Population.PARTIAL, Population.FULL }) + @CacheSpec(population = { Population.PARTIAL, Population.FULL }, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void get(Cache cache, CacheContext context) { Function mappingFunction = context.original()::get; context.ticker().advance(30, TimeUnit.SECONDS); @@ -96,8 +102,9 @@ public void get(Cache cache, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(expireAfterWrite = Expire.ONE_MINUTE, - population = { Population.PARTIAL, Population.FULL }) + @CacheSpec(population = { Population.PARTIAL, Population.FULL }, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void getAllPresent(Cache cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); cache.getAllPresent(context.firstMiddleLastKeys()); @@ -115,8 +122,9 @@ public void getAllPresent(Cache cache, CacheContext context) { /* ---------------- LoadingCache -------------- */ @Test(dataProvider = "caches") - @CacheSpec(expireAfterWrite = Expire.ONE_MINUTE, - population = { Population.PARTIAL, Population.FULL }) + @CacheSpec(population = { Population.PARTIAL, Population.FULL }, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void get(LoadingCache cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); cache.get(context.firstKey()); @@ -132,8 +140,10 @@ public void get(LoadingCache cache, CacheContext context) { } @Test(dataProvider = "caches") - @CacheSpec(expireAfterWrite = Expire.ONE_MINUTE, loader = {Loader.IDENTITY, Loader.BULK_IDENTITY}, - population = { Population.PARTIAL, Population.FULL }) + @CacheSpec(population = { Population.PARTIAL, Population.FULL }, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE, + loader = {Loader.IDENTITY, Loader.BULK_IDENTITY}) public void getAll(LoadingCache cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); assertThat(cache.getAll(ImmutableList.of(context.firstKey(), context.absentKey())), @@ -155,8 +165,9 @@ public void getAll(LoadingCache cache, CacheContext context) { /* ---------------- AsyncLoadingCache -------------- */ @Test(dataProvider = "caches") - @CacheSpec(expireAfterWrite = Expire.ONE_MINUTE, - population = { Population.PARTIAL, Population.FULL }) + @CacheSpec(population = { Population.PARTIAL, Population.FULL }, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) @SuppressWarnings("FutureReturnValueIgnored") public void getIfPresent(AsyncLoadingCache cache, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); @@ -173,7 +184,9 @@ public void getIfPresent(AsyncLoadingCache cache, CacheContext /* ---------------- Map -------------- */ @Test(dataProvider = "caches") - @CacheSpec(expireAfterWrite = Expire.ONE_MINUTE, population = Population.FULL) + @CacheSpec(population = Population.FULL, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void putIfAbsent(Map map, CacheContext context) { context.ticker().advance(30, TimeUnit.SECONDS); assertThat(map.putIfAbsent(context.firstKey(), context.absentValue()), is(not(nullValue()))); @@ -190,14 +203,18 @@ public void putIfAbsent(Map map, CacheContext context) { /* ---------------- Policy -------------- */ @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void getExpiresAfter(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { assertThat(expireAfterWrite.getExpiresAfter(TimeUnit.MINUTES), is(1L)); } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void setExpiresAfter(Cache cache, CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { expireAfterWrite.setExpiresAfter(2, TimeUnit.MINUTES); @@ -209,7 +226,9 @@ public void setExpiresAfter(Cache cache, CacheContext context, } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE, + @CacheSpec(implementation = Implementation.Caffeine, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void ageOf(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { @@ -222,14 +241,18 @@ public void ageOf(CacheContext context, /* ---------------- Policy: oldest -------------- */ - @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) public void oldest_unmodifiable(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { expireAfterWrite.oldest(Integer.MAX_VALUE).clear();; } - @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) public void oldest_negative(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { @@ -237,15 +260,18 @@ public void oldest_negative(CacheContext context, } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void oldest_zero(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { assertThat(expireAfterWrite.oldest(0), is(emptyMap())); } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, - population = Population.FULL, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void oldest_partial(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { int count = (int) context.initialSize() / 2; @@ -254,8 +280,10 @@ public void oldest_partial(CacheContext context, @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, - population = {Population.PARTIAL, Population.FULL}, expireAfterWrite = Expire.ONE_MINUTE, - removalListener = { Listener.DEFAULT, Listener.REJECTING }) + population = {Population.PARTIAL, Population.FULL}, + removalListener = { Listener.DEFAULT, Listener.REJECTING }, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void oldest_order(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { Map oldest = expireAfterWrite.oldest(Integer.MAX_VALUE); @@ -263,7 +291,9 @@ public void oldest_order(CacheContext context, } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void oldest_snapshot(Cache cache, CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { Map oldest = expireAfterWrite.oldest(Integer.MAX_VALUE); @@ -273,14 +303,18 @@ public void oldest_snapshot(Cache cache, CacheContext context, /* ---------------- Policy: youngest -------------- */ - @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) public void youngest_unmodifiable(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { expireAfterWrite.youngest(Integer.MAX_VALUE).clear();; } - @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class) public void youngest_negative(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { @@ -288,15 +322,18 @@ public void youngest_negative(CacheContext context, } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void youngest_zero(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { assertThat(expireAfterWrite.youngest(0), is(emptyMap())); } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, - population = Population.FULL, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void youngest_partial(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { int count = (int) context.initialSize() / 2; @@ -305,8 +342,10 @@ public void youngest_partial(CacheContext context, @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, - population = {Population.PARTIAL, Population.FULL}, expireAfterWrite = Expire.ONE_MINUTE, - removalListener = { Listener.DEFAULT, Listener.REJECTING }) + population = {Population.PARTIAL, Population.FULL}, + removalListener = { Listener.DEFAULT, Listener.REJECTING }, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void youngest_order(CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { Map youngest = expireAfterWrite.youngest(Integer.MAX_VALUE); @@ -315,7 +354,9 @@ public void youngest_order(CacheContext context, } @Test(dataProvider = "caches") - @CacheSpec(implementation = Implementation.Caffeine, expireAfterWrite = Expire.ONE_MINUTE) + @CacheSpec(implementation = Implementation.Caffeine, + mustExpiresWithAnyOf = { AFTER_WRITE, VARIABLE }, expireAfterWrite = Expire.ONE_MINUTE, + expiry = { CacheExpiry.DISABLED, CacheExpiry.WRITE }, expiryTime = Expire.ONE_MINUTE) public void youngest_snapshot(Cache cache, CacheContext context, @ExpireAfterWrite Expiration expireAfterWrite) { Map youngest = expireAfterWrite.youngest(Integer.MAX_VALUE); diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/IsCacheReserializable.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/IsCacheReserializable.java index 4b0d98c2e3..a018701357 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/IsCacheReserializable.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/IsCacheReserializable.java @@ -23,6 +23,7 @@ import org.hamcrest.Matcher; import org.hamcrest.TypeSafeDiagnosingMatcher; +import com.github.benmanes.caffeine.cache.Async.AsyncExpiry; import com.github.benmanes.caffeine.cache.Async.AsyncWeigher; import com.github.benmanes.caffeine.testing.DescriptionBuilder; import com.google.common.testing.SerializableTester; @@ -209,15 +210,17 @@ private static void checkBoundedAsyncLocalLoadingCache( private static void checkBoundedLocalCache(BoundedLocalCache original, BoundedLocalCache copy, DescriptionBuilder desc) { desc.expectThat("empty", copy.estimatedSize(), is(0L)); - desc.expectThat("same weigher", - unwrapWeigher(copy.weigher).getClass(), is(equalTo( - unwrapWeigher(original.weigher).getClass()))); + desc.expectThat("same weigher", unwrapWeigher(copy.weigher).getClass(), + is(equalTo(unwrapWeigher(original.weigher).getClass()))); desc.expectThat("same nodeFactory", copy.nodeFactory, is(original.nodeFactory)); if (original.evicts()) { desc.expectThat("same maximumWeight", copy.maximum(), is(original.maximum())); desc.expectThat("same maximumEdenWeight", copy.edenMaximum(), is(original.edenMaximum())); } + desc.expectThat("same expiry", unwrapExpiry(copy.expiry()).getClass(), + is(equalTo(unwrapExpiry(original.expiry()).getClass()))); + if (original.expiresAfterAccess()) { desc.expectThat("same expiresAfterAccessNanos", copy.expiresAfterAccessNanos(), is(original.expiresAfterAccessNanos())); @@ -248,18 +251,26 @@ private static void checkBoundedLocalCache(BoundedLocalCache origin } } - /* ---------------- Shared -------------- */ - - private static Weigher unwrapWeigher(Weigher weigher) { + @SuppressWarnings("unchecked") + private static Weigher unwrapWeigher(Weigher weigher) { for (;;) { if (weigher instanceof BoundedWeigher) { - weigher = ((BoundedWeigher) weigher).delegate; + weigher = (Weigher) ((BoundedWeigher) weigher).delegate; } else if (weigher instanceof AsyncWeigher) { - weigher = ((AsyncWeigher) weigher).delegate; + weigher = (Weigher) ((AsyncWeigher) weigher).delegate; + } else { + return weigher; + } + } + } + + @SuppressWarnings("unchecked") + private static Expiry unwrapExpiry(Expiry expiry) { + for (;;) { + if (expiry instanceof AsyncExpiry) { + expiry = (Expiry) ((AsyncExpiry) expiry).delegate; } else { - @SuppressWarnings("unchecked") - Weigher castedWeigher = (Weigher) weigher; - return castedWeigher; + return expiry; } } } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java index 5b09553091..f48e12f2c1 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/TimerWheelTest.java @@ -72,7 +72,7 @@ public void schedule(long nanos, int expired) { verify(cache, times(expired)).evictEntry(any(), any(), anyLong()); for (Node node : captor.getAllValues()) { - assertThat(node.getAccessTime(), is(lessThan(nanos))); + assertThat(node.getVariableTime(), is(lessThan(nanos))); } } @@ -101,7 +101,7 @@ public void schedule_fuzzy(long clock, long nanos, long[] times) { verify(cache, times(expired)).evictEntry(any(), any(), anyLong()); for (Node node : captor.getAllValues()) { - assertThat(node.getAccessTime(), is(lessThan(nanos))); + assertThat(node.getVariableTime(), is(lessThan(nanos))); } checkTimerWheel(nanos); } @@ -133,9 +133,9 @@ private void checkTimerWheel(long nanos) { private LongList getTimers(Node sentinel) { LongList timers = new LongArrayList(); - for (Node node = sentinel.getNextInAccessOrder(); - node != sentinel; node = node.getNextInAccessOrder()) { - timers.add(node.getAccessTime()); + for (Node node = sentinel.getNextInVariableOrder(); + node != sentinel; node = node.getNextInVariableOrder()) { + timers.add(node.getVariableTime()); } return timers; } @@ -146,11 +146,11 @@ public void reschedule() { Timer timer = new Timer(TimeUnit.MINUTES.toNanos(15)); timerWheel.schedule(timer); - Node startBucket = timer.getNextInAccessOrder(); + Node startBucket = timer.getNextInVariableOrder(); - timer.setAccessTime(TimeUnit.HOURS.toNanos(2)); + timer.setVariableTime(TimeUnit.HOURS.toNanos(2)); timerWheel.reschedule(timer); - assertThat(timer.getNextInAccessOrder(), is(not(startBucket))); + assertThat(timer.getNextInVariableOrder(), is(not(startBucket))); timerWheel.advance(TimeUnit.DAYS.toNanos(1)); checkEmpty(); @@ -160,8 +160,8 @@ private void checkEmpty() { for (int i = 0; i < timerWheel.wheel.length; i++) { for (int j = 0; j < timerWheel.wheel[i].length; j++) { Node sentinel = timerWheel.wheel[i][j]; - assertThat(sentinel.getNextInAccessOrder(), is(sentinel)); - assertThat(sentinel.getPreviousInAccessOrder(), is(sentinel)); + assertThat(sentinel.getNextInVariableOrder(), is(sentinel)); + assertThat(sentinel.getPreviousInVariableOrder(), is(sentinel)); } } } @@ -171,8 +171,8 @@ public void deschedule() { Timer timer = new Timer(100); timerWheel.schedule(timer); timerWheel.deschedule(timer); - assertThat(timer.getNextInAccessOrder(), is(nullValue())); - assertThat(timer.getPreviousInAccessOrder(), is(nullValue())); + assertThat(timer.getNextInVariableOrder(), is(nullValue())); + assertThat(timer.getPreviousInVariableOrder(), is(nullValue())); } @Test @@ -200,7 +200,7 @@ public void deschedule_fuzzy(long clock, long nanos, long[] times) { public void expire_reschedule() { when(cache.evictEntry(captor.capture(), any(), anyLong())).thenAnswer(invocation -> { Timer timer = (Timer) invocation.getArgument(0); - timer.setAccessTime(timerWheel.nanos + 100); + timer.setVariableTime(timerWheel.nanos + 100); return false; }); @@ -208,8 +208,8 @@ public void expire_reschedule() { timerWheel.advance(TimerWheel.SPANS[0]); verify(cache).evictEntry(any(), any(), anyLong()); - assertThat(captor.getValue().getNextInAccessOrder(), is(not(nullValue()))); - assertThat(captor.getValue().getPreviousInAccessOrder(), is(not(nullValue()))); + assertThat(captor.getValue().getNextInVariableOrder(), is(not(nullValue()))); + assertThat(captor.getValue().getPreviousInVariableOrder(), is(not(nullValue()))); } @Test(dataProvider = "cascade") @@ -241,28 +241,28 @@ public Iterator providesCascade() { private static final class Timer implements Node { Node prev; Node next; - long accessTime; + long variableTime; Timer(long accessTime) { - setAccessTime(accessTime); + setVariableTime(accessTime); } - @Override public long getAccessTime() { - return accessTime; + @Override public long getVariableTime() { + return variableTime; } - @Override public void setAccessTime(long accessTime) { - this.accessTime = accessTime; + @Override public void setVariableTime(long variableTime) { + this.variableTime = variableTime; } - @Override public Node getPreviousInAccessOrder() { + @Override public Node getPreviousInVariableOrder() { return prev; } - @Override public void setPreviousInAccessOrder(@Nullable Node prev) { + @Override public void setPreviousInVariableOrder(@Nullable Node prev) { this.prev = prev; } - @Override public Node getNextInAccessOrder() { + @Override public Node getNextInVariableOrder() { return next; } - @Override public void setNextInAccessOrder(@Nullable Node next) { + @Override public void setNextInVariableOrder(@Nullable Node next) { this.next = next; } 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 5daf004338..e802456107 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 @@ -39,13 +39,16 @@ import com.github.benmanes.caffeine.cache.CacheLoader; import com.github.benmanes.caffeine.cache.CacheWriter; import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Expiry; import com.github.benmanes.caffeine.cache.LoadingCache; import com.github.benmanes.caffeine.cache.RemovalListener; import com.github.benmanes.caffeine.cache.stats.CacheStats; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Advance; import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExecutor; +import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExpiry; import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheWeigher; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Compute; +import com.github.benmanes.caffeine.cache.testing.CacheSpec.Expiration; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Expire; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Implementation; import com.github.benmanes.caffeine.cache.testing.CacheSpec.InitialCapacity; @@ -73,17 +76,20 @@ public final class CacheContext { final RemovalListener removalListener; final CacheWriter cacheWriter; final InitialCapacity initialCapacity; + final Expiry expiry; final Map original; final Implementation implementation; final Listener removalListenerType; final CacheExecutor cacheExecutor; final ReferenceType valueStrength; final ReferenceType keyStrength; + final CacheExpiry expiryType; final Population population; final CacheWeigher weigher; final Maximum maximumSize; final Expire afterAccess; final Expire afterWrite; + final Expire expiryTime; final Executor executor; final FakeTicker ticker; final Compute compute; @@ -112,11 +118,11 @@ public final class CacheContext { Map absent; public CacheContext(InitialCapacity initialCapacity, Stats stats, CacheWeigher weigher, - Maximum maximumSize, Expire afterAccess, Expire afterWrite, Expire refresh, - Advance advance, ReferenceType keyStrength, ReferenceType valueStrength, + Maximum maximumSize, CacheExpiry expiryType, Expire afterAccess, Expire afterWrite, + Expire refresh, Advance advance, ReferenceType keyStrength, ReferenceType valueStrength, CacheExecutor cacheExecutor, Listener removalListenerType, Population population, boolean isLoading, boolean isAsyncLoading, Compute compute, Loader loader, Writer writer, - Implementation implementation) { + Implementation implementation, CacheSpec cacheSpec) { this.initialCapacity = requireNonNull(initialCapacity); this.stats = requireNonNull(stats); this.weigher = requireNonNull(weigher); @@ -128,19 +134,22 @@ public CacheContext(InitialCapacity initialCapacity, Stats stats, CacheWeigher w this.keyStrength = requireNonNull(keyStrength); this.valueStrength = requireNonNull(valueStrength); this.cacheExecutor = requireNonNull(cacheExecutor); - this.executor = cacheExecutor.get(); + this.executor = cacheExecutor.create(); this.removalListenerType = removalListenerType; 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.cacheWriter = writer.create(); this.ticker = new SerializableFakeTicker(); this.implementation = requireNonNull(implementation); this.original = new LinkedHashMap<>(); this.initialSize = -1; this.compute = compute; + this.expiryType = expiryType; + this.expiryTime = cacheSpec.expiryTime(); + this.expiry = expiryType.createExpiry(expiryTime); } public InitialCapacity initialCapacity() { @@ -355,8 +364,34 @@ public CacheStats stats() { return cache.stats(); } + public boolean expires(Expiration expiration) { + return (expiresAfterAccess() && (expiration == Expiration.AFTER_ACCESS)) + || (expiresAfterWrite() && (expiration == Expiration.AFTER_WRITE)) + || (expiresVariably() && (expiration == Expiration.VARIABLE)); + } + public boolean expires() { - return (afterAccess != Expire.DISABLED) || (afterWrite != Expire.DISABLED); + return expiresVariably() || expiresAfterAccess() || expiresAfterWrite(); + } + + public boolean expiresAfterAccess() { + return (afterAccess != Expire.DISABLED); + } + + public boolean expiresAfterWrite() { + return (afterWrite != Expire.DISABLED); + } + + public boolean expiresVariably() { + return (expiryType != CacheExpiry.DISABLED); + } + + public CacheExpiry expiryType() { + return expiryType; + } + + public Expire expiryTime() { + return expiryTime; } public Expire expireAfterAccess() { @@ -424,6 +459,8 @@ public String toString() { .add("population", population) .add("maximumSize", maximumSize) .add("weigher", weigher) + .add("expiry", expiryType) + .add("expiryTime", expiryTime) .add("afterAccess", afterAccess) .add("afterWrite", afterWrite) .add("refreshAfterWrite", refresh) 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 ec1836f5dc..3c92f3caf5 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 @@ -30,6 +30,7 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Advance; import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExecutor; +import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExpiry; import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheWeigher; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Compute; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Expire; @@ -115,6 +116,7 @@ private Set> combinations() { ImmutableSet.copyOf(statistics), ImmutableSet.copyOf(cacheSpec.weigher()), ImmutableSet.copyOf(cacheSpec.maximumSize()), + ImmutableSet.copyOf(cacheSpec.expiry()), ImmutableSet.copyOf(cacheSpec.expireAfterAccess()), ImmutableSet.copyOf(cacheSpec.expireAfterWrite()), ImmutableSet.copyOf(cacheSpec.refreshAfterWrite()), @@ -150,6 +152,7 @@ private CacheContext newCacheContext(List combination) { (Stats) combination.get(index++), (CacheWeigher) combination.get(index++), (Maximum) combination.get(index++), + (CacheExpiry) combination.get(index++), (Expire) combination.get(index++), (Expire) combination.get(index++), (Expire) combination.get(index++), @@ -164,7 +167,8 @@ private CacheContext newCacheContext(List combination) { (Compute) combination.get(index++), (Loader) combination.get(index++), (Writer) combination.get(index++), - (Implementation) combination.get(index++)); + (Implementation) combination.get(index++), + cacheSpec); } /** Returns if the context is a viable configuration. */ @@ -176,13 +180,19 @@ private boolean isCompatible(CacheContext context) { && (!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 expiryIncompatible = (context.expiryType() != CacheExpiry.DISABLED) + && ((context.implementation() != Implementation.Caffeine) + || (context.expireAfterAccess() != Expire.DISABLED) + || (context.expireAfterWrite() != Expire.DISABLED)); + boolean expirationIncompatible = (cacheSpec.mustExpiresWithAnyOf().length > 0) + && !Arrays.stream(cacheSpec.mustExpiresWithAnyOf()).anyMatch(context::expires); boolean skip = asyncIncompatible || asyncLoaderIncompatible || refreshIncompatible || weigherIncompatible - || expirationIncompatible || referenceIncompatible; + || expiryIncompatible || 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 e4230639c1..e7631af164 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 @@ -35,13 +35,13 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; 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.Expiry; import com.github.benmanes.caffeine.cache.LoadingCache; import com.github.benmanes.caffeine.cache.RemovalListener; import com.github.benmanes.caffeine.cache.Weigher; @@ -225,8 +225,12 @@ public int unitsPerEntry() { /* ---------------- Expiration -------------- */ - /** Indicates that the combination must have an expiration setting. */ - boolean requiresExpiration() default false; + /** Indicates that the combination must have any of the expiration settings. */ + Expiration[] mustExpiresWithAnyOf() default {}; + + enum Expiration { + AFTER_WRITE, AFTER_ACCESS, VARIABLE + } /** The expiration time-to-idle setting, each resulting in a new combination. */ Expire[] expireAfterAccess() default { @@ -246,11 +250,54 @@ Expire[] refreshAfterWrite() default { Expire.FOREVER }; + /** The variable expiration setting, each resulting in a new combination. */ + CacheExpiry[] expiry() default { + CacheExpiry.DISABLED, + CacheExpiry.ACCESS + }; + + /** The fixed duration for the expiry. */ + Expire expiryTime() default Expire.FOREVER; + /** Indicates if the amount of time that should be auto-advance for each entry when populating. */ Advance[] advanceOnPopulation() default { Advance.ZERO }; + enum CacheExpiry { + DISABLED { + @Override public Expiry createExpiry(Expire expiryTime) { + return null; + } + }, + MOCKITO { + @Override public Expiry createExpiry(Expire expiryTime) { + @SuppressWarnings("unchecked") + Expiry mock = Mockito.mock(Expiry.class); + return mock; + } + }, + ACCESS { + @Override public Expiry createExpiry(Expire expiryTime) { + return ExpiryBuilder + .expiringAfterCreate(expiryTime.timeNanos()) + .expiringAfterUpdate(expiryTime.timeNanos()) + .expiringAfterRead(expiryTime.timeNanos()) + .build(); + } + }, + WRITE { + @Override public Expiry createExpiry(Expire expiryTime) { + return ExpiryBuilder + .expiringAfterCreate(expiryTime.timeNanos()) + .expiringAfterUpdate(expiryTime.timeNanos()) + .build(); + } + }; + + public abstract Expiry createExpiry(Expire expiryTime); + } + enum Expire { /** A flag indicating that entries are not evicted due to expiration. */ DISABLED(Long.MIN_VALUE), @@ -504,27 +551,29 @@ Writer[] writer() default { }; /** The {@link CacheWriter} for the external resource. */ - enum Writer implements Supplier> { + enum Writer { /** A writer that does nothing. */ DISABLED { - @Override public CacheWriter get() { + @Override public CacheWriter create() { return CacheWriter.disabledWriter(); } }, /** A writer that records interactions. */ MOCKITO { - @Override public CacheWriter get() { + @Override public CacheWriter create() { @SuppressWarnings("unchecked") - CacheWriter mock = Mockito.mock(CacheWriter.class); + CacheWriter mock = Mockito.mock(CacheWriter.class); return mock; } }, /** A writer that always throws an exception. */ EXCEPTIONAL { - @Override public CacheWriter get() { - return new RejectingCacheWriter(); + @Override public CacheWriter create() { + return new RejectingCacheWriter(); } }; + + public abstract CacheWriter create(); } /* ---------------- Executor -------------- */ @@ -545,26 +594,26 @@ enum ExecutorFailure { new ThreadFactoryBuilder().setDaemon(true).build()); /** The executors that the cache can be configured with. */ - enum CacheExecutor implements Supplier { + enum CacheExecutor { DEFAULT { // fork-join common pool - @Override public Executor get() { + @Override public Executor create() { // Use with caution as may be unpredictable during tests if awaiting completion return null; } }, DIRECT { - @Override public Executor get() { + @Override public Executor create() { // Cache implementations must avoid deadlocks by incorrectly assuming async execution return new TrackingExecutor(MoreExecutors.newDirectExecutorService()); } }, THREADED { - @Override public Executor get() { + @Override public Executor create() { return new TrackingExecutor(cachedExecutorService); } }, REJECTING { - @Override public Executor get() { + @Override public Executor create() { // Cache implementations must avoid corrupting internal state due to rejections return new ForkJoinPool() { @Override public void execute(Runnable task) { @@ -574,8 +623,7 @@ enum CacheExecutor implements Supplier { } }; - @Override - public abstract Executor get(); + public abstract Executor create(); } /* ---------------- Populated -------------- */ 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 44ef795af1..42186b1601 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 @@ -23,6 +23,7 @@ import com.github.benmanes.caffeine.cache.RandomSeedEnforcer; import com.github.benmanes.caffeine.cache.Ticker; import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExecutor; +import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExpiry; import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheWeigher; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Expire; import com.github.benmanes.caffeine.cache.testing.CacheSpec.InitialCapacity; @@ -58,6 +59,9 @@ public static Cache newCaffeineCache(CacheContext context) { builder.maximumWeight(context.maximumWeight()); } } + if (context.expiryType() != CacheExpiry.DISABLED) { + builder.expireAfter(context.expiry); + } if (context.afterAccess != Expire.DISABLED) { builder.expireAfterAccess(context.afterAccess.timeNanos(), TimeUnit.NANOSECONDS); } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/ExpiryBuilder.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/ExpiryBuilder.java new file mode 100644 index 0000000000..7f7775fd0c --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/ExpiryBuilder.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017 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.testing; + +import java.io.Serializable; + +import com.github.benmanes.caffeine.cache.Expiry; + +/** + * A builder for unit test convenience. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +public final class ExpiryBuilder { + static final int UNSET = -1; + + private long createNanos = UNSET; + private long updateNanos = UNSET; + private long readNanos = UNSET; + + private ExpiryBuilder(long createNanos) { + this.createNanos = createNanos; + } + + /** Sets the fixed creation expiration time. */ + public static ExpiryBuilder expiringAfterCreate(long nanos) { + return new ExpiryBuilder(nanos); + } + + /** Sets the fixed update expiration time. */ + public ExpiryBuilder expiringAfterUpdate(long nanos) { + updateNanos = nanos; + return this; + } + + /** Sets the fixed read expiration time. */ + public ExpiryBuilder expiringAfterRead(long nanos) { + readNanos = nanos; + return this; + } + + public Expiry build() { + return new FixedExpiry(createNanos, updateNanos, readNanos); + } + + private static final class FixedExpiry implements Expiry, Serializable { + private static final long serialVersionUID = 1L; + + private final long createNanos; + private final long updateNanos; + private final long readNanos; + + FixedExpiry(long createNanos, long updateNanos, long readNanos) { + this.createNanos = createNanos; + this.updateNanos = updateNanos; + this.readNanos = readNanos; + } + + @Override + public long expireAfterCreate(K key, V value, long currentTime) { + return createNanos; + } + @Override + public long expireAfterUpdate(K key, V value, long currentTime, long currentDuration) { + return (updateNanos == UNSET) ? currentDuration : updateNanos; + } + @Override + public long expireAfterRead(K key, V value, long currentTime, long currentDuration) { + return (readNanos == UNSET) ? currentDuration : readNanos; + } + } +} diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index b4959148fc..24ded9ebf8 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -30,7 +30,7 @@ ext { commons_lang3: '3.5', config: '1.3.1', error_prone_annotations: '2.0.19', - fastutil: '7.1.0', + fastutil: '7.2.0', flip_tables: '1.0.2', guava: '21.0', javapoet: '1.8.0', @@ -43,13 +43,13 @@ ext { xz: '1.6', ] test_versions = [ - awaitility: '2.0.0', + awaitility: '3.0.0', easymock: '3.4', hamcrest: '2.0.0.0', jcache_tck: '1.0.1', jctools: '2.0.1', junit: '4.12', - mockito: '2.7.22', + mockito: '2.8.9', pax_exam: '4.10.0', testng: '6.11', truth: '0.24', @@ -58,9 +58,9 @@ ext { cache2k: '1.0.0.CR4', collision: '0.3.2', concurrentlinkedhashmap: '1.4.2', - ehcache2: '2.10.3', + ehcache2: '2.10.4', ehcache3: '3.3.1', - elastic_search: '5.3.0', + elastic_search: '5.3.2', expiring_map: '0.5.8', jackrabbit: '1.6.1', jamm: '0.3.1', @@ -74,7 +74,7 @@ ext { plugin_versions = [ buildscan: '1.6', buildscan_recipes: '0.2.0', - checkstyle: '7.6.1', + checkstyle: '7.7', coveralls: '2.8.1', coverity: '1.0.10', extra_conf: '3.1.0', diff --git a/gradle/jmh.gradle b/gradle/jmh.gradle index 8fee6cf493..c711eb244f 100644 --- a/gradle/jmh.gradle +++ b/gradle/jmh.gradle @@ -31,7 +31,7 @@ jmhJar { } jmh { - jmhVersion = '1.17.3' + jmhVersion = '1.18' if (project.hasProperty('includePattern')) { include = project.includePattern diff --git a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/copy/JavaSerializationCopier.java b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/copy/JavaSerializationCopier.java index 7f8b42a542..8a99d0fa89 100644 --- a/jcache/src/main/java/com/github/benmanes/caffeine/jcache/copy/JavaSerializationCopier.java +++ b/jcache/src/main/java/com/github/benmanes/caffeine/jcache/copy/JavaSerializationCopier.java @@ -57,7 +57,7 @@ protected byte[] serialize(Object object) { try (ObjectOutputStream output = new ObjectOutputStream(bytes)) { output.writeObject(object); } catch (IOException e) { - throw new UncheckedIOException("Failed to serialize " + e.getClass(), e); + throw new UncheckedIOException("Failed to serialize " + object.getClass(), e); } return bytes.toByteArray(); }