I want to configure my Caffeine
cache to return stale results when loader fails to refresh the cache. The following Kotlin code demonstrates the case:
@Test fun `completeble future`() = runBlocking { val cache = Caffeine.newBuilder() .refreshAfterWrite(Duration.ofSeconds(1)) .expireAfterWrite(Duration.ofSeconds(1)) .buildAsync<String, String> { key: String, executor -> GlobalScope.future(executor.asCoroutineDispatcher()) { throw Exception("== Error ==") } } cache.put("id", CompletableFuture.completedFuture("value")) delay(2000) assertEquals("value", cache.get("id").await()) }
I expect this test to pass but instead I get the following error:
WARNING: Exception thrown during asynchronous load java.lang.Exception: == Error == at fsra.manager.TranslationManagerImplTest$completeble future$1$cache$1$1.invokeSuspend(TranslationManagerImplTest.kt:93) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241) at java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1426) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177) java.lang.Exception: == Error == at fsra.manager.TranslationManagerImplTest$completeble future$1$cache$1$1.invokeSuspend(TranslationManagerImplTest.kt:93) at |b|b|b(Coroutine boundary.|b(|b) at fsra.manager.TranslationManagerImplTest$completeble future$1.invokeSuspend(TranslationManagerImplTest.kt:101) Caused by: java.lang.Exception: == Error == at fsra.manager.TranslationManagerImplTest$completeble future$1$cache$1$1.invokeSuspend(TranslationManagerImplTest.kt:93) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241) at java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1426) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)
I wrote the code in Kotlin but I don’t think the issue is related to Kotlin coroutines. I want to configure Caffeine to not throw when refreshing, but instead return the previous result in the cache.
Advertisement
Answer
The cache is set to expire entries after 1 second and the test waits 2 seconds. The next call then forces it to be loaded anew because the entry is unusable, and you throw an exception.
The refresh has no effect when it is greater-or-equal to the expiration time. When less than, the entry is stale but usable so it is returned and asynchronously reloaded. This is to allow popular items, like a configuration, to stay in the cache without a periodic penalty on a reload. The unpopular items are those not accessed within the expiration interval and are allowed to be evicted. If the refresh was unable to succeed then expiration would kick in and the entry would be dropped, as the expiration sets the maximum time it is considered usable.
A greater expiration value, like 5 seconds, would pass your test. If your use-case is instead to blindly reload all of the cache’s contents periodically, you can instead use a regular Map
and a ScheduledExecutorService
to refresh it.