Caffeine: Use stale values when AsyncLoader fails to refresh

Tags: , , , ,



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.

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.



Source: stackoverflow