sksamuel / aedile

Kotlin Wrapper for Caffeine
Apache License 2.0
170 stars 15 forks source link

Exception in CacheLoader leads to broken cache #1

Closed dadadom closed 2 years ago

dadadom commented 2 years ago

(first off: Thanks for providing this Kotlin wrapper around Caffeine, highly appreciated!)

I am using the cache with a loader/builder which generates the entries if they don't exist.

The problem is that, if a call to a Loader fails, the cache as a whole becomes unusable afterwards.

I created the following minimal example to demonstrate the issue:

class CacheTest {
    @kotlin.test.Test
    fun `Simplified test`() {
        val list1 = listOf(1, 2)
        val list2 = listOf(3, 4)

        val sut = CacheTesting()

        assertFails { runBlocking { sut.testStuff(list1) } }
        val result = runBlocking { sut.testStuff(list2) }
        assertTrue { result.size == 2 }
    }
}

class CacheTesting {
    private val intCache = caffeineBuilder<Int, Int>().build {
        calculateValue(it)
    }

    suspend fun testStuff(ints: List<Int>): List<Int> =
        ints.map { intCache.get(it) }

    private fun calculateValue(i: Int): Int {
        if (i == 2) throw IllegalArgumentException("Bad number")
        return i
    }
}

The output is:

Parent job is Cancelling
kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=JobImpl{Cancelling}@6548fcd1
    at app//kotlinx.coroutines.JobSupport.getChildJobCancellationCause(JobSupport.kt:714)
    at app//kotlinx.coroutines.JobSupport.createCauseException(JobSupport.kt:720)
    at app//kotlinx.coroutines.JobSupport.makeCancelling(JobSupport.kt:752)
    at app//kotlinx.coroutines.JobSupport.cancelImpl$kotlinx_coroutines_core(JobSupport.kt:671)
    at app//kotlinx.coroutines.JobSupport.parentCancelled(JobSupport.kt:637)
    at app//kotlinx.coroutines.ChildHandleNode.invoke(JobSupport.kt:1466)
    at app//kotlinx.coroutines.ChildHandleNode.invoke(JobSupport.kt:1462)
    at app//kotlinx.coroutines.JobSupport.invokeOnCompletion(JobSupport.kt:1548)
    at app//kotlinx.coroutines.Job$DefaultImpls.invokeOnCompletion$default(Job.kt:341)
    at app//kotlinx.coroutines.JobSupport.attachChild(JobSupport.kt:970)
    at app//kotlinx.coroutines.JobSupport.initParentJob(JobSupport.kt:150)
    at app//kotlinx.coroutines.AbstractCoroutine.<init>(AbstractCoroutine.kt:51)
    at app//kotlinx.coroutines.DeferredCoroutine.<init>(Builders.common.kt:99)
    at app//kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:90)
    at app//kotlinx.coroutines.BuildersKt.async(Unknown Source)
    at app//kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:82)
    at app//kotlinx.coroutines.BuildersKt.async$default(Unknown Source)
    at app//com.sksamuel.aedile.core.Builder.build$lambda-0(caffeine.kt:136)
    at app//com.github.benmanes.caffeine.cache.LocalAsyncLoadingCache.lambda$newMappingFunction$0(LocalAsyncLoadingCache.java:68)
    at app//com.github.benmanes.caffeine.cache.LocalAsyncCache.lambda$get$2(LocalAsyncCache.java:92)
    at app//com.github.benmanes.caffeine.cache.UnboundedLocalCache.lambda$computeIfAbsent$2(UnboundedLocalCache.java:298)
    at java.base@18.0.1/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
    at app//com.github.benmanes.caffeine.cache.UnboundedLocalCache.computeIfAbsent(UnboundedLocalCache.java:294)
    at app//com.github.benmanes.caffeine.cache.LocalAsyncCache.get(LocalAsyncCache.java:90)
    at app//com.github.benmanes.caffeine.cache.LocalAsyncCache.get(LocalAsyncCache.java:81)
    at app//com.github.benmanes.caffeine.cache.LocalAsyncLoadingCache.get(LocalAsyncLoadingCache.java:134)
    at app//com.sksamuel.aedile.core.LoadingCache.get(LoadingCache.kt:25)
    at app//CacheTesting.testStuff(CacheTest.kt:27)
    at app//CacheTest$Simplified test$result$1.invokeSuspend(CacheTest.kt:16)
    (Coroutine boundary)
    at CacheTesting.testStuff(CacheTest.kt:27)
    at CacheTest$Simplified test$result$1.invokeSuspend(CacheTest.kt:16)
Caused by: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=JobImpl{Cancelled}@6548fcd1
    at app//kotlinx.coroutines.JobSupport.getChildJobCancellationCause(JobSupport.kt:714)
    at app//kotlinx.coroutines.JobSupport.createCauseException(JobSupport.kt:720)
    at app//kotlinx.coroutines.JobSupport.makeCancelling(JobSupport.kt:752)
    at app//kotlinx.coroutines.JobSupport.cancelImpl$kotlinx_coroutines_core(JobSupport.kt:671)
    at app//kotlinx.coroutines.JobSupport.parentCancelled(JobSupport.kt:637)
    at app//kotlinx.coroutines.ChildHandleNode.invoke(JobSupport.kt:1466)
    at app//kotlinx.coroutines.ChildHandleNode.invoke(JobSupport.kt:1462)
    at app//kotlinx.coroutines.JobSupport.invokeOnCompletion(JobSupport.kt:1548)
    at app//kotlinx.coroutines.Job$DefaultImpls.invokeOnCompletion$default(Job.kt:341)
    at app//kotlinx.coroutines.JobSupport.attachChild(JobSupport.kt:970)
    at app//kotlinx.coroutines.JobSupport.initParentJob(JobSupport.kt:150)
    at app//kotlinx.coroutines.AbstractCoroutine.<init>(AbstractCoroutine.kt:51)
    at app//kotlinx.coroutines.DeferredCoroutine.<init>(Builders.common.kt:99)
    at app//kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:90)
    at app//kotlinx.coroutines.BuildersKt.async(Unknown Source)
    at app//kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:82)
    at app//kotlinx.coroutines.BuildersKt.async$default(Unknown Source)
    at app//com.sksamuel.aedile.core.Builder.build$lambda-0(caffeine.kt:136)
    at app//com.github.benmanes.caffeine.cache.LocalAsyncLoadingCache.lambda$newMappingFunction$0(LocalAsyncLoadingCache.java:68)
    at app//com.github.benmanes.caffeine.cache.LocalAsyncCache.lambda$get$2(LocalAsyncCache.java:92)
    at app//com.github.benmanes.caffeine.cache.UnboundedLocalCache.lambda$computeIfAbsent$2(UnboundedLocalCache.java:298)
    at java.base@18.0.1/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
    at app//com.github.benmanes.caffeine.cache.UnboundedLocalCache.computeIfAbsent(UnboundedLocalCache.java:294)
    at app//com.github.benmanes.caffeine.cache.LocalAsyncCache.get(LocalAsyncCache.java:90)
    at app//com.github.benmanes.caffeine.cache.LocalAsyncCache.get(LocalAsyncCache.java:81)
    at app//com.github.benmanes.caffeine.cache.LocalAsyncLoadingCache.get(LocalAsyncLoadingCache.java:134)
    at app//com.sksamuel.aedile.core.LoadingCache.get(LoadingCache.kt:25)
    at app//CacheTesting.testStuff(CacheTest.kt:27)
    at app//CacheTest$Simplified test$result$1.invokeSuspend(CacheTest.kt:16)
    at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at app//kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at app//kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:284)
    at app//kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
    at app//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
    at app//kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
    at app//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
    at app//kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
    at app//CacheTest.Simplified test(CacheTest.kt:16)
    at java.base@18.0.1/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base@18.0.1/java.lang.reflect.Method.invoke(Method.java:577)
    at app//org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
    at app//org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at app//org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
    at app//org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
    at app//org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
    at app//org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
    at app//org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at app//org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at app//org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
    at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
    at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
    at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base@18.0.1/java.util.ArrayList.forEach(ArrayList.java:1511)
    at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base@18.0.1/java.util.ArrayList.forEach(ArrayList.java:1511)
    at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
    at app//org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at app//org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
    at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
    at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
    at java.base@18.0.1/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base@18.0.1/java.lang.reflect.Method.invoke(Method.java:577)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at jdk.proxy1/jdk.proxy1.$Proxy2.stop(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
    at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
    at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
    at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: java.lang.IllegalArgumentException: Bad number
    at CacheTesting.calculateValue(CacheTest.kt:30)
    ...

I expect this test to pass as, in my understanding, the second call to sut.testStuff() should not depend on the outcome of the first one.

I am not sure if this really is a bug or just a misunderstanding on my part, but I know for sure that this currently leads to a broken system :-)

sksamuel commented 2 years ago

Good spot thanks. Should be fixed in 1.1.2