Skip to content

Commit eb44628

Browse files
committed
Fix variable expiration with async cache (fixes #159)
An in-flight future was mistakenly given the maximum expiry allowed, causing it to not honor an expire-after-create setting. Instead it was supposed to be beyond the maximum to signal adaption on the completion update. The calculations for fixed expiration was made more robust to the time rolling over. This now complies with System.nanoTime() warnings. Strengthened the remove and replace operations to be more predictably linearizable. This removed optimizations to avoid unnecessary work by checking if the entry was present in a lock-free manner. Since the hash table supresses loads until complete, that might mean that a call to remove a loading entry was not performed. The contract allows either, so the optimization is left to user code and gives preference to those who need the linearizable behavior. (See #156)
1 parent abf9add commit eb44628

File tree

8 files changed

+365
-106
lines changed

8 files changed

+365
-106
lines changed

caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -678,21 +678,20 @@ void expireAfterAccessEntries(long now) {
678678
return;
679679
}
680680

681-
long expirationTime = (now - expiresAfterAccessNanos());
682-
expireAfterAccessEntries(accessOrderEdenDeque(), expirationTime, now);
681+
expireAfterAccessEntries(accessOrderEdenDeque(), now);
683682
if (evicts()) {
684-
expireAfterAccessEntries(accessOrderProbationDeque(), expirationTime, now);
685-
expireAfterAccessEntries(accessOrderProtectedDeque(), expirationTime, now);
683+
expireAfterAccessEntries(accessOrderProbationDeque(), now);
684+
expireAfterAccessEntries(accessOrderProtectedDeque(), now);
686685
}
687686
}
688687

689688
/** Expires entries in an access-order queue. */
690689
@GuardedBy("evictionLock")
691-
void expireAfterAccessEntries(AccessOrderDeque<Node<K, V>> accessOrderDeque,
692-
long expirationTime, long now) {
690+
void expireAfterAccessEntries(AccessOrderDeque<Node<K, V>> accessOrderDeque, long now) {
691+
long duration = expiresAfterAccessNanos();
693692
for (;;) {
694693
Node<K, V> node = accessOrderDeque.peekFirst();
695-
if ((node == null) || (node.getAccessTime() > expirationTime)) {
694+
if ((node == null) || ((now - node.getAccessTime()) < duration)) {
696695
return;
697696
}
698697
evictEntry(node, RemovalCause.EXPIRED, now);
@@ -705,10 +704,10 @@ void expireAfterWriteEntries(long now) {
705704
if (!expiresAfterWrite()) {
706705
return;
707706
}
708-
long expirationTime = now - expiresAfterWriteNanos();
707+
long duration = expiresAfterWriteNanos();
709708
for (;;) {
710709
final Node<K, V> node = writeOrderDeque().peekFirst();
711-
if ((node == null) || (node.getWriteTime() > expirationTime)) {
710+
if ((node == null) || ((now - node.getWriteTime()) < duration)) {
712711
break;
713712
}
714713
evictEntry(node, RemovalCause.EXPIRED, now);
@@ -762,12 +761,10 @@ boolean evictEntry(Node<K, V> node, RemovalCause cause, long now) {
762761
if (actualCause[0] == RemovalCause.EXPIRED) {
763762
boolean expired = false;
764763
if (expiresAfterAccess()) {
765-
long expirationTime = now - expiresAfterAccessNanos();
766-
expired |= (n.getAccessTime() <= expirationTime);
764+
expired |= ((now - n.getAccessTime()) >= expiresAfterAccessNanos());
767765
}
768766
if (expiresAfterWrite()) {
769-
long expirationTime = now - expiresAfterWriteNanos();
770-
expired |= (n.getWriteTime() <= expirationTime);
767+
expired |= ((now - n.getWriteTime()) >= expiresAfterWriteNanos());
771768
}
772769
if (expiresVariable()) {
773770
expired |= (n.getVariableTime() <= now);
@@ -1333,10 +1330,10 @@ public void run() {
13331330
if (isComputingAsync(node)) {
13341331
synchronized (node) {
13351332
if (!Async.isReady((CompletableFuture<?>) node.getValue())) {
1336-
long expirationTime = expirationTicker().read() + Async.MAXIMUM_EXPIRY;
1337-
setWriteTime(node, expirationTime);
1338-
setAccessTime(node, expirationTime);
1333+
long expirationTime = expirationTicker().read() + Long.MAX_VALUE;
13391334
setVariableTime(node, expirationTime);
1335+
setAccessTime(node, expirationTime);
1336+
setWriteTime(node, expirationTime);
13401337
}
13411338
}
13421339
}
@@ -1745,9 +1742,8 @@ public V remove(Object key) {
17451742
* @return the removed value or null if no mapping was found
17461743
*/
17471744
V removeNoWriter(Object key) {
1748-
Node<K, V> node;
1749-
Object lookupKey = nodeFactory.newLookupKey(key);
1750-
if (!data.containsKey(lookupKey) || ((node = data.remove(lookupKey)) == null)) {
1745+
Node<K, V> node = data.remove(nodeFactory.newLookupKey(key));
1746+
if (node == null) {
17511747
return null;
17521748
}
17531749

@@ -1822,9 +1818,7 @@ V removeWithWriter(Object key) {
18221818
@Override
18231819
public boolean remove(Object key, Object value) {
18241820
requireNonNull(key);
1825-
1826-
Object lookupKey = nodeFactory.newLookupKey(key);
1827-
if ((value == null) || !data.containsKey(lookupKey)) {
1821+
if (value == null) {
18281822
return false;
18291823
}
18301824

@@ -1837,7 +1831,7 @@ public boolean remove(Object key, Object value) {
18371831
RemovalCause[] cause = new RemovalCause[1];
18381832

18391833
long now = expirationTicker().read();
1840-
data.computeIfPresent(lookupKey, (kR, node) -> {
1834+
data.computeIfPresent(nodeFactory.newLookupKey(key), (kR, node) -> {
18411835
synchronized (node) {
18421836
oldKey[0] = node.getKey();
18431837
oldValue[0] = node.getValue();

caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,11 @@ public int size() {
559559
return delegate.size();
560560
}
561561

562+
@Override
563+
public void clear() {
564+
delegate.clear();
565+
}
566+
562567
@Override
563568
public boolean containsKey(Object key) {
564569
return delegate.containsKey(key);
@@ -603,6 +608,28 @@ public V remove(Object key) {
603608
return Async.getWhenSuccessful(oldValueFuture);
604609
}
605610

611+
@Override
612+
public boolean remove(Object key, Object value) {
613+
requireNonNull(key);
614+
if (value == null) {
615+
return false;
616+
}
617+
CompletableFuture<V> oldValueFuture = delegate.get(key);
618+
if ((oldValueFuture != null) && !value.equals(Async.getWhenSuccessful(oldValueFuture))) {
619+
// Optimistically check if the current value is equal, but don't skip if it may be loading
620+
return false;
621+
}
622+
623+
@SuppressWarnings("unchecked")
624+
K castedKey = (K) key;
625+
boolean[] removed = { false };
626+
delegate.compute(castedKey, (k, oldValue) -> {
627+
removed[0] = value.equals(Async.getWhenSuccessful(oldValue));
628+
return removed[0] ? null : oldValue;
629+
}, /* recordStats */ false, /* recordLoad */ false);
630+
return removed[0];
631+
}
632+
606633
@Override
607634
public V replace(K key, V value) {
608635
requireNonNull(value);
@@ -616,24 +643,19 @@ public boolean replace(K key, V oldValue, V newValue) {
616643
requireNonNull(oldValue);
617644
requireNonNull(newValue);
618645
CompletableFuture<V> oldValueFuture = delegate.get(key);
619-
return oldValue.equals(Async.getIfReady(oldValueFuture))
620-
&& delegate.replace(key, oldValueFuture, CompletableFuture.completedFuture(newValue));
621-
}
622-
623-
@Override
624-
public boolean remove(Object key, Object value) {
625-
requireNonNull(key);
626-
if (value == null) {
646+
if ((oldValueFuture != null) && !oldValue.equals(Async.getWhenSuccessful(oldValueFuture))) {
647+
// Optimistically check if the current value is equal, but don't skip if it may be loading
627648
return false;
628649
}
629-
CompletableFuture<V> oldValueFuture = delegate.get(key);
630-
return value.equals(Async.getIfReady(oldValueFuture))
631-
&& delegate.remove(key, oldValueFuture);
632-
}
633650

634-
@Override
635-
public void clear() {
636-
delegate.clear();
651+
@SuppressWarnings("unchecked")
652+
K castedKey = key;
653+
boolean[] replaced = { false };
654+
delegate.compute(castedKey, (k, value) -> {
655+
replaced[0] = oldValue.equals(Async.getWhenSuccessful(value));
656+
return replaced[0] ? CompletableFuture.completedFuture(newValue) : value;
657+
}, /* recordStats */ false, /* recordLoad */ false);
658+
return replaced[0];
637659
}
638660

639661
@Override

caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,11 @@ public boolean isEmpty() {
327327
return data.isEmpty();
328328
}
329329

330+
@Override
331+
public int size() {
332+
return data.size();
333+
}
334+
330335
@Override
331336
public void clear() {
332337
if (!hasRemovalListener() && (writer == CacheWriter.disabledWriter())) {
@@ -338,11 +343,6 @@ public void clear() {
338343
}
339344
}
340345

341-
@Override
342-
public int size() {
343-
return data.size();
344-
}
345-
346346
@Override
347347
public boolean containsKey(Object key) {
348348
return data.containsKey(key);

0 commit comments

Comments
 (0)