Skip to content

Commit

Permalink
Variable expiration support (fixes #70, #75, #141)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ben-manes committed May 1, 2017
1 parent 6e6541b commit d6f0c6d
Show file tree
Hide file tree
Showing 30 changed files with 925 additions and 379 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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");
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Integer, Integer> {
static final class Timer implements Node<Integer, Integer> {
Node<Integer, Integer> prev;
Node<Integer, Integer> 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<Integer, Integer> getPreviousInAccessOrder() {
@Override public Node<Integer, Integer> getPreviousInVariableOrder() {
return prev;
}
@Override public void setPreviousInAccessOrder(@Nullable Node<Integer, Integer> prev) {
@Override public void setPreviousInVariableOrder(@Nullable Node<Integer, Integer> prev) {
this.prev = prev;
}
@Override public Node<Integer, Integer> getNextInAccessOrder() {
@Override public Node<Integer, Integer> getNextInVariableOrder() {
return next;
}
@Override public void setNextInAccessOrder(@Nullable Node<Integer, Integer> next) {
@Override public void setNextInVariableOrder(@Nullable Node<Integer, Integer> next) {
this.next = next;
}

Expand All @@ -120,7 +122,7 @@ private static final class Timer implements Node<Integer, Integer> {
@Override public void die() {}
}

private static final class MockCache extends BoundedLocalCache<Integer, Integer> {
static final class MockCache extends BoundedLocalCache<Integer, Integer> {

@SuppressWarnings({"unchecked", "rawtypes"})
protected MockCache() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
* @author [email protected] (Ben Manes)
*/
final class Async {
static final long MAXIMUM_EXPIRY = (Long.MAX_VALUE >> 1); // 150 years

private Async() {}

Expand Down Expand Up @@ -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<K, V> implements Expiry<K, CompletableFuture<V>>, Serializable {
private static final long serialVersionUID = 1L;

final Expiry<K, V> delegate;

AsyncExpiry(Expiry<K, V> delegate) {
this.delegate = requireNonNull(delegate);
}

@Override
public long expireAfterCreate(K key, CompletableFuture<V> 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<V> 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<V> 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;
}
}
}
Loading

0 comments on commit d6f0c6d

Please sign in to comment.