Skip to content

Commit 83ae0dc

Browse files
ben-manesfacboy
andcommitted
Fix write-time optimization for variable expiration (fixes #478)
A high write rate to the same key can overwhelm the write buffer as it may not drop entries and has a maximum capacity. When full this causes backpressure to allow for the mainance task to catch up. A write only needs to be recorded in this buffer when a major event occurs, such as the entry's size changed or expiration time differs. To improve throughput for expireAfterWrite a tolerance of 1s is used to allow for skipping the write buffer and recording into the read buffer instead. This improves throughput by 5x in a same key write benchmark. This optimization was not updated to take into account variable expiration, where the expire time may change based on the calculation determined by configured Expiry. This would cause the entry to not be reordered in the TimerWheel, possibly delaying the automic removal indefinitely. Now when the variable time differs by +/- 1s then the write-time reordering is required. Co-authored-by: Christopher Ng <[email protected]>
1 parent a54afaa commit 83ae0dc

File tree

4 files changed

+90
-7
lines changed

4 files changed

+90
-7
lines changed

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2026,7 +2026,7 @@ public Map<K, V> getAllPresent(Iterable<?> keys) {
20262026
int oldWeight;
20272027
boolean expired = false;
20282028
boolean mayUpdate = true;
2029-
boolean exceedsTolerance = true;
2029+
boolean exceedsTolerance = false;
20302030
synchronized (prior) {
20312031
if (!prior.isAlive()) {
20322032
continue;
@@ -2051,7 +2051,10 @@ public Map<K, V> getAllPresent(Iterable<?> keys) {
20512051
writer.write(key, value);
20522052
}
20532053
if (mayUpdate) {
2054-
exceedsTolerance = ((now - prior.getWriteTime()) > EXPIRE_WRITE_TOLERANCE);
2054+
exceedsTolerance =
2055+
(expiresAfterWrite() && (now - prior.getWriteTime()) > EXPIRE_WRITE_TOLERANCE)
2056+
|| (expiresVariable()
2057+
&& Math.abs(varTime - prior.getVariableTime()) > EXPIRE_WRITE_TOLERANCE);
20552058

20562059
setWriteTime(prior, now);
20572060
prior.setWeight(newWeight);
@@ -2075,7 +2078,7 @@ public Map<K, V> getAllPresent(Iterable<?> keys) {
20752078
int weightedDifference = mayUpdate ? (newWeight - oldWeight) : 0;
20762079
if ((oldValue == null) || (weightedDifference != 0) || expired) {
20772080
afterWrite(new UpdateTask(prior, weightedDifference));
2078-
} else if (!onlyIfAbsent && expiresAfterWrite() && exceedsTolerance) {
2081+
} else if (!onlyIfAbsent && exceedsTolerance) {
20792082
afterWrite(new UpdateTask(prior, weightedDifference));
20802083
} else {
20812084
if (mayUpdate) {

caffeine/src/test/java/com/github/benmanes/caffeine/cache/BoundedLocalCacheTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.github.benmanes.caffeine.cache.BLCHeader.DrainStatusRef.PROCESSING_TO_IDLE;
2020
import static com.github.benmanes.caffeine.cache.BLCHeader.DrainStatusRef.PROCESSING_TO_REQUIRED;
2121
import static com.github.benmanes.caffeine.cache.BLCHeader.DrainStatusRef.REQUIRED;
22+
import static com.github.benmanes.caffeine.cache.BoundedLocalCache.EXPIRE_WRITE_TOLERANCE;
2223
import static com.github.benmanes.caffeine.cache.BoundedLocalCache.PERCENT_MAIN_PROTECTED;
2324
import static com.github.benmanes.caffeine.cache.testing.HasRemovalNotifications.hasRemovalNotifications;
2425
import static com.github.benmanes.caffeine.cache.testing.HasStats.hasEvictionCount;
@@ -34,10 +35,13 @@
3435
import static org.hamcrest.Matchers.not;
3536
import static org.hamcrest.Matchers.nullValue;
3637
import static org.mockito.ArgumentMatchers.any;
38+
import static org.mockito.ArgumentMatchers.anyLong;
39+
import static org.mockito.Mockito.when;
3740

3841
import java.util.List;
3942
import java.util.Map;
4043
import java.util.concurrent.Executor;
44+
import java.util.concurrent.TimeUnit;
4145
import java.util.concurrent.atomic.AtomicBoolean;
4246
import java.util.concurrent.atomic.AtomicInteger;
4347
import java.util.concurrent.locks.ReentrantLock;
@@ -85,6 +89,8 @@ static BoundedLocalCache<Integer, Integer> asBoundedLocalCache(Cache<Integer, In
8589
return (BoundedLocalCache<Integer, Integer>) cache.asMap();
8690
}
8791

92+
/* --------------- Maintenance --------------- */
93+
8894
@Test
8995
@SuppressWarnings("UnusedVariable")
9096
public void cleanupTask_allowGc() {
@@ -169,6 +175,8 @@ public void scheduleDrainBuffers_rejected(Cache<Integer, Integer> cache, CacheCo
169175
cache.put(context.absentKey(), context.absentValue());
170176
}
171177

178+
/* --------------- Eviction --------------- */
179+
172180
@Test
173181
public void putWeighted_noOverflow() {
174182
Cache<Integer, Integer> cache = Caffeine.newBuilder()
@@ -708,4 +716,75 @@ private void adapt(Cache<Integer, Integer> cache,
708716
// Fill main protected space
709717
cache.asMap().keySet().stream().forEach(cache::getIfPresent);
710718
}
719+
720+
/* --------------- Expiration --------------- */
721+
722+
@Test(dataProvider = "caches")
723+
@CacheSpec(compute = Compute.SYNC, implementation = Implementation.Caffeine,
724+
population = Population.EMPTY, initialCapacity = InitialCapacity.FULL,
725+
expireAfterWrite = Expire.ONE_MINUTE)
726+
public void put_expireTolerance_expireAfterWrite(
727+
Cache<Integer, Integer> cache, CacheContext context) {
728+
BoundedLocalCache<Integer, Integer> localCache = asBoundedLocalCache(cache);
729+
boolean mayCheckReads = context.isStrongKeys() && context.isStrongValues()
730+
&& localCache.readBuffer != Buffer.<Node<Integer, Integer>>disabled();
731+
732+
cache.put(1, 1);
733+
assertThat(localCache.writeBuffer().producerIndex, is(2L));
734+
735+
// If within the tolerance, treat the update as a read
736+
cache.put(1, 2);
737+
if (mayCheckReads) {
738+
assertThat(localCache.readBuffer.reads(), is(0));
739+
assertThat(localCache.readBuffer.writes(), is(1));
740+
}
741+
assertThat(localCache.writeBuffer().producerIndex, is(2L));
742+
743+
// If exceeds the tolerance, treat the update as a write
744+
context.ticker().advance(EXPIRE_WRITE_TOLERANCE + 1, TimeUnit.NANOSECONDS);
745+
cache.put(1, 3);
746+
if (mayCheckReads) {
747+
assertThat(localCache.readBuffer.reads(), is(1));
748+
assertThat(localCache.readBuffer.writes(), is(1));
749+
}
750+
assertThat(localCache.writeBuffer().producerIndex, is(4L));
751+
}
752+
753+
@Test(dataProvider = "caches")
754+
@CacheSpec(compute = Compute.SYNC, implementation = Implementation.Caffeine,
755+
population = Population.EMPTY, expiry = CacheExpiry.MOCKITO, expiryTime = Expire.ONE_MINUTE)
756+
public void put_expireTolerance_expiry(Cache<Integer, Integer> cache, CacheContext context) {
757+
BoundedLocalCache<Integer, Integer> localCache = asBoundedLocalCache(cache);
758+
cache.put(1, 1);
759+
assertThat(localCache.writeBuffer().producerIndex, is(2L));
760+
761+
// If within the tolerance, treat the update as a read
762+
cache.put(1, 2);
763+
assertThat(localCache.readBuffer.reads(), is(0));
764+
assertThat(localCache.readBuffer.writes(), is(1));
765+
assertThat(localCache.writeBuffer().producerIndex, is(2L));
766+
767+
// If exceeds the tolerance, treat the update as a write
768+
context.ticker().advance(EXPIRE_WRITE_TOLERANCE + 1, TimeUnit.NANOSECONDS);
769+
cache.put(1, 3);
770+
assertThat(localCache.readBuffer.reads(), is(1));
771+
assertThat(localCache.readBuffer.writes(), is(1));
772+
assertThat(localCache.writeBuffer().producerIndex, is(4L));
773+
774+
// If the expire time reduces by more than the tolerance, treat the update as a write
775+
when(context.expiry().expireAfterUpdate(any(), any(), anyLong(), anyLong()))
776+
.thenReturn(Expire.ONE_MILLISECOND.timeNanos());
777+
cache.put(1, 4);
778+
assertThat(localCache.readBuffer.reads(), is(1));
779+
assertThat(localCache.readBuffer.writes(), is(1));
780+
assertThat(localCache.writeBuffer().producerIndex, is(6L));
781+
782+
// If the expire time increases by more than the tolerance, treat the update as a write
783+
when(context.expiry().expireAfterUpdate(any(), any(), anyLong(), anyLong()))
784+
.thenReturn(Expire.FOREVER.timeNanos());
785+
cache.put(1, 4);
786+
assertThat(localCache.readBuffer.reads(), is(1));
787+
assertThat(localCache.readBuffer.writes(), is(1));
788+
assertThat(localCache.writeBuffer().producerIndex, is(8L));
789+
}
711790
}

checksum.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
<trusted-key id='94b291aef984a085' group='io.reactivex.rxjava3' />
9696
<trusted-key id='5e2f2b3d474efe6b' group='it.unimi.dsi' />
9797
<trusted-key id='823dba283a93ee3c' group='it.unimi.dsi' />
98+
<trusted-key id='ca85ffe638d4407a' group='it.unimi.dsi' />
9899
<trusted-key id='d908a43fb7ec07ac' group='jakarta.activation' />
99100
<trusted-key id='0e325becb6962a24' group='jakarta.annotation' />
100101
<trusted-key id='0aa3e5c3d232e79b' group='jakarta.inject' />

gradle/dependencies.gradle

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
ext {
2727
versions = [
2828
akka: '2.6.10',
29-
cache2k: '1.9.3.Alpha',
30-
checkerFramework: '3.7.1',
29+
cache2k: '1.9.4.Beta',
30+
checkerFramework: '3.8.0',
3131
coherence: '20.06',
3232
collision: '0.3.3',
3333
commonsCompress: '1.20',
@@ -41,7 +41,7 @@ ext {
4141
elasticSearch: '7.10.0',
4242
expiringMap: '0.5.9',
4343
fastfilter: 'bf0b02297f',
44-
fastutil: '8.4.3',
44+
fastutil: '8.4.4',
4545
flipTables: '1.1.0',
4646
googleJavaFormat: '1.7',
4747
guava: '30.0-jre',
@@ -97,7 +97,7 @@ ext {
9797
semanticVersioning: '1.1.0',
9898
shadow: '6.1.0',
9999
sonarqube: '3.0',
100-
spotbugs: '4.1.4',
100+
spotbugs: '4.2.0',
101101
spotbugsPlugin: '4.6.0',
102102
stats: '0.2.2',
103103
versions: '0.36.0',

0 commit comments

Comments
 (0)