Skip to content

Commit 68fbff8

Browse files
committed
Reduce the lock hold time during Map.clear (fixes #835)
When invalidating the cache, it is faster and simpler to discard under the eviction lock. This avoids thrashing on the write buffer and causing excessive maintenence runs, which are triggered per write. However, for a large cache this hold time may be excessive by causing other writes to pile up so that the write buffer is full and backpressure causes delays. In that case removing through the write buffer allows fairer throughput as all writes compete to append into the buffer and async draining will captures a batch of work. This avoids delaying any other write, or at least makes the backpressure times short, so that writes have better latencies by disfavoring the clear throughput. The cache adapts strategies by monitoring the size of the write buffer. If it grows over a threshold then the clear operation backs off by releasing the eviction lock and performing a one-by-one removal instead.
1 parent 2fbc869 commit 68fbff8

File tree

4 files changed

+48
-20
lines changed

4 files changed

+48
-20
lines changed

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1989,34 +1989,42 @@ public long estimatedSize() {
19891989
}
19901990

19911991
@Override
1992-
@SuppressWarnings("FutureReturnValueIgnored")
19931992
public void clear() {
19941993
evictionLock.lock();
19951994
try {
1996-
long now = expirationTicker().read();
1995+
// Discard all pending reads
1996+
readBuffer.drainTo(e -> {});
19971997

19981998
// Apply all pending writes
19991999
Runnable task;
20002000
while ((task = writeBuffer.poll()) != null) {
20012001
task.run();
20022002
}
20032003

2004-
// Discard all entries
2005-
for (var entry : data.entrySet()) {
2006-
removeNode(entry.getValue(), now);
2007-
}
2008-
20092004
// Cancel the scheduled cleanup
20102005
Pacer pacer = pacer();
20112006
if (pacer != null) {
20122007
pacer.cancel();
20132008
}
20142009

2015-
// Discard all pending reads
2016-
readBuffer.drainTo(e -> {});
2010+
// Discard all entries
2011+
int threshold = (WRITE_BUFFER_MAX / 2);
2012+
long now = expirationTicker().read();
2013+
for (var node : data.values()) {
2014+
if (writeBuffer.size() >= threshold) {
2015+
// Fallback to one-by-one to avoid excessive lock hold times
2016+
break;
2017+
}
2018+
removeNode(node, now);
2019+
}
20172020
} finally {
20182021
evictionLock.unlock();
20192022
}
2023+
2024+
// Remove any stragglers, such as if released early to more aggressively flush incoming writes
2025+
for (Object key : keySet()) {
2026+
remove(key);
2027+
}
20202028
}
20212029

20222030
@GuardedBy("evictionLock")

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,25 @@
139139
@Test(dataProviderClass = CacheProvider.class)
140140
public final class BoundedLocalCacheTest {
141141

142+
@Test(dataProvider = "caches")
143+
@CacheSpec(population = Population.FULL, removalListener = Listener.MOCKITO)
144+
public void clear_pendingWrites(BoundedLocalCache<Int, Int> cache, CacheContext context) {
145+
var insert = new boolean[] { true };
146+
Mockito.doAnswer(invocation -> {
147+
if (insert[0]) {
148+
while (cache.writeBuffer.offer(() -> {})) {
149+
// ignored
150+
}
151+
insert[0] = false;
152+
}
153+
return null;
154+
}).when(context.removalListener()).onRemoval(any(), any(), any());
155+
156+
cache.clear();
157+
assertThat(cache).isExhaustivelyEmpty();
158+
assertThat(cache.writeBuffer).isEmpty();
159+
}
160+
142161
/* --------------- Maintenance --------------- */
143162

144163
@Test

gradle/codeQuality.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ tasks.withType(JavaCompile).configureEach {
165165
'UnusedTypeParameter', 'UsingJsr305CheckReturnValue']
166166
enabledChecks.each { enable(it) }
167167

168-
def disabledChecks = [ 'LexicographicalAnnotationListing', 'MissingSummary', 'StaticImport' ]
168+
def disabledChecks = [ 'LexicographicalAnnotationListing',
169+
'MissingSummary', 'IsInstanceLambdaUsage', 'StaticImport' ]
169170
disabledChecks.each { disable(it) }
170171

171172
def errorChecks = [ 'NullAway' ]

gradle/dependencies.gradle

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ ext {
3737
config: '1.4.2',
3838
ehcache3: '3.10.8',
3939
errorprone: '2.16',
40-
errorproneSupport: '0.5.0',
40+
errorproneSupport: '0.6.0',
4141
expiringMap: '0.5.10',
4242
fastfilter: '1.0.2',
43-
fastutil: '8.5.9',
43+
fastutil: '8.5.11',
4444
flipTables: '1.1.0',
4545
googleJavaFormat: '1.15.0',
4646
guava: '31.1-jre',
@@ -58,7 +58,7 @@ ext {
5858
ohc: '0.6.1',
5959
osgiComponentAnnotations: '1.5.0',
6060
picocli: '4.7.0',
61-
slf4j: '2.0.5',
61+
slf4j: '2.0.6',
6262
tcache: '2.0.1',
6363
stream: '2.9.8',
6464
univocityParsers: '2.9.1',
@@ -70,6 +70,8 @@ ext {
7070
awaitility: '4.2.0',
7171
commonsCollectionsTests: '4.4',
7272
eclipseCollections: '11.1.0',
73+
felix: '7.0.5',
74+
felixScr: '2.2.4',
7375
guice: '5.1.0',
7476
hamcrest: '2.2',
7577
jcacheTck: '1.1.1',
@@ -79,21 +81,19 @@ ext {
7981
junitTestNG: '1.0.4',
8082
lincheck: '2.16',
8183
mockito: '4.9.0',
82-
paxExam: '4.13.5',
83-
slf4jTest: '2.6.1',
84-
testng: '7.6.1',
85-
truth: '1.1.3',
86-
felix: '7.0.5',
87-
felixScr: '2.2.4',
8884
osgiSvcComponent: '1.5.0',
8985
osgiUtilFunction: '1.2.0',
9086
osgiUtilPromise: '1.2.0',
87+
paxExam: '4.13.5',
88+
slf4jTest: '2.6.1',
89+
testng: '7.7.0',
90+
truth: '1.1.3',
9191
]
9292
pluginVersions = [
9393
bnd: '6.4.0',
9494
checkstyle: '10.5.0',
9595
coveralls: '2.12.0',
96-
dependencyCheck: '7.3.2',
96+
dependencyCheck: '7.4.1',
9797
errorprone: '3.0.1',
9898
findsecbugs: '1.12.0',
9999
forbiddenApis: '3.4',

0 commit comments

Comments
 (0)