Skip to content

Commit a5f42be

Browse files
committed
Fix compute with null #45
1 parent d206357 commit a5f42be

File tree

3 files changed

+43
-39
lines changed

3 files changed

+43
-39
lines changed

aedile-core/src/main/kotlin/com/sksamuel/aedile/core/LoadingCache.kt

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import java.util.concurrent.CompletableFuture
1212
import kotlin.coroutines.coroutineContext
1313

1414
class LoadingCache<K : Any, V>(
15-
private val defaultScope: CoroutineScope,
16-
private val useCallingContext: Boolean,
1715
private val cache: AsyncLoadingCache<K, V>
1816
) {
1917

@@ -68,7 +66,7 @@ class LoadingCache<K : Any, V>(
6866
* See full docs at [AsyncCache.getAll].
6967
*/
7068
suspend fun getAll(keys: Collection<K>, compute: suspend (Set<K>) -> Map<K, V>): Map<K, V> {
71-
val scope = scope()
69+
val scope = CoroutineScope(coroutineContext)
7270
return cache.getAll(keys) { k, _ -> scope.async { compute(k.toSet()) }.asCompletableFuture() }.await()
7371
}
7472

@@ -83,10 +81,29 @@ class LoadingCache<K : Any, V>(
8381
* and enters it into this cache unless null.
8482
*
8583
* If the suspendable computation throws, the entry will be automatically removed from this cache.
84+
*
85+
* See full docs at [AsyncLoadingCache.get].
8686
*/
8787
suspend fun get(key: K, compute: suspend (K) -> V): V {
88-
val scope = scope()
89-
return cache.get(key) { k, _ -> scope.async { compute(k) }.asCompletableFuture() }.await()
88+
89+
val scope = CoroutineScope(coroutineContext)
90+
var error: Throwable? = null
91+
val value = cache.get(key) { k, _ ->
92+
val asCompletableFuture = scope.async {
93+
// if compute throws, then it will cause the parent coroutine to be cancelled as well
94+
// we don't want that, as want to throw the exception back to the caller.
95+
// so we must capture it and throw it manually
96+
try {
97+
compute(k)
98+
} catch (e: Throwable) {
99+
error = e
100+
null
101+
}
102+
}.asCompletableFuture()
103+
asCompletableFuture.thenApply { it ?: throw error ?: NullPointerException() }
104+
}.await()
105+
error?.let { throw it }
106+
return value
90107
}
91108

92109
/**
@@ -102,7 +119,7 @@ class LoadingCache<K : Any, V>(
102119
* @return the present value, the computed value, or throws.
103120
*/
104121
suspend fun getOrNull(key: K, compute: suspend (K) -> V?): V? {
105-
val scope = scope()
122+
val scope = CoroutineScope(coroutineContext)
106123
return cache.get(key) { k, _ -> scope.async { compute(k) }.asCompletableFuture() }.await()
107124
}
108125

@@ -118,10 +135,10 @@ class LoadingCache<K : Any, V>(
118135
* If the suspendable computation throws, the entry will be automatically removed.
119136
*
120137
* @param key the key to associate the computed value with
121-
* @param compute the suspendable function that generate the value.
138+
* @param compute the suspendable function that generates the value.
122139
*/
123140
suspend fun put(key: K, compute: suspend () -> V) {
124-
val scope = scope()
141+
val scope = CoroutineScope(coroutineContext)
125142
cache.put(key, scope.async { compute() }.asCompletableFuture())
126143
}
127144

@@ -194,8 +211,4 @@ class LoadingCache<K : Any, V>(
194211
suspend fun refreshAll(keys: Collection<K>): Map<K, V> {
195212
return cache.synchronous().refreshAll(keys).await()
196213
}
197-
198-
private suspend fun scope(): CoroutineScope {
199-
return if (useCallingContext) CoroutineScope(coroutineContext) else defaultScope
200-
}
201214
}

aedile-core/src/main/kotlin/com/sksamuel/aedile/core/builders.kt

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,7 @@ import java.util.concurrent.Executor
2020
* If the suspendable computation throws or computes a null value, then the
2121
* entry will be automatically removed.
2222
*/
23-
fun <K : Any, V : Any> Caffeine<in K, in V>.asCache(): Cache<K, V> {
24-
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("Aedile-AsyncLoadingCache-Scope") + SupervisorJob())
25-
return asCache(scope)
26-
}
27-
28-
/**
29-
* Returns a [Cache] which suspends when requesting values.
30-
*
31-
* If the key is not present in the cache, it returns null, unless a compute function
32-
* is provided with the key.
33-
*
34-
* If the suspendable computation throws or computes a null value, then the
35-
* entry will be automatically removed.
36-
*/
37-
fun <K : Any, V : Any> Caffeine<in K, in V>.asCache(scope: CoroutineScope): Cache<K, V> {
38-
return Cache(buildAsync())
39-
}
23+
fun <K : Any, V : Any> Caffeine<in K, in V>.asCache(): Cache<K, V> = Cache(buildAsync())
4024

4125
/**
4226
* Returns a [LoadingCache] which uses the provided [compute] function
@@ -54,16 +38,16 @@ fun <K : Any, V : Any> Caffeine<in K, in V>.asLoadingCache(compute: suspend (K)
5438
* Returns a [LoadingCache] which uses the provided [compute] function
5539
* to compute a value, unless a specific compute has been provided with the key.
5640
*
57-
* The compute function will execute on the given [scope].
41+
* The [compute] function will execute on the given [scope].
5842
*
59-
* If the suspendable computation throws or computes a null value then the
43+
* If the suspendable computation throws or computes a null value, then the
6044
* entry will be automatically removed.
6145
*/
6246
fun <K : Any, V : Any> Caffeine<in K, in V>.asLoadingCache(
6347
scope: CoroutineScope,
6448
compute: suspend (K) -> V
6549
): LoadingCache<K, V> {
66-
return LoadingCache(scope, true, buildAsync { key, _ -> scope.async { compute(key) }.asCompletableFuture() })
50+
return LoadingCache(buildAsync { key, _ -> scope.async { compute(key) }.asCompletableFuture() })
6751
}
6852

6953
/**
@@ -92,20 +76,20 @@ fun <K : Any, V : Any> Caffeine<in K, in V>.asLoadingCache(
9276
* If the key does not exist, then the suspendable [compute] function is invoked
9377
* to compute a value, unless a specific compute has been provided with the key.
9478
*
95-
* If the suspendable computation throws or computes a null value then the
79+
* If the suspendable computation throws or computes a null value, then the
9680
* entry will be automatically removed.
9781
*
9882
* The [reloadCompute] function is invoked to refresh an entry if refreshAfterWrite
9983
* is enabled or refresh is invoked. See full docs [AsyncCacheLoader.asyncReload].
10084
*
101-
* The compute functions will execute on the given [scope].
85+
* The given compute functions will execute on the given [scope].
10286
*/
10387
fun <K : Any, V : Any> Caffeine<in K, in V>.asLoadingCache(
10488
scope: CoroutineScope,
10589
compute: suspend (K) -> V,
10690
reloadCompute: suspend (K, V) -> V,
10791
): LoadingCache<K, V> {
108-
return LoadingCache(scope, true, buildAsync(object : AsyncCacheLoader<K, V> {
92+
return LoadingCache(buildAsync(object : AsyncCacheLoader<K, V> {
10993
override fun asyncLoad(key: K, executor: Executor): CompletableFuture<out V> {
11094
return scope.async { compute(key) }.asCompletableFuture()
11195
}
@@ -122,7 +106,7 @@ fun <K : Any, V : Any> Caffeine<in K, in V>.asLoadingCache(
122106
* If a requested key does not exist, then the suspendable [compute] function is invoked
123107
* to compute the required values.
124108
*
125-
* If the suspendable computation throws or computes a null value then the
109+
* If the suspendable computation throws or computes a null value, then the
126110
* entry will be automatically removed.
127111
*/
128112
fun <K : Any, V : Any> Caffeine<in K, in V>.asBulkLoadingCache(compute: suspend (Set<K>) -> Map<K, V>): LoadingCache<K, V> {
@@ -145,7 +129,7 @@ fun <K : Any, V : Any> Caffeine<in K, in V>.asBulkLoadingCache(
145129
scope: CoroutineScope,
146130
compute: suspend (Set<K>) -> Map<K, V>
147131
): LoadingCache<K, V> {
148-
return LoadingCache(scope, true, buildAsync(AsyncCacheLoader.bulk { keys, _ ->
132+
return LoadingCache(buildAsync(AsyncCacheLoader.bulk { keys, _ ->
149133
scope.async { compute(keys) }.asCompletableFuture()
150134
}))
151135
}

aedile-core/src/test/kotlin/com/sksamuel/aedile/core/AsLoadingCacheTest.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,15 @@ class AsLoadingCacheTest : FunSpec() {
106106
keys.associateWith { "$it-value" }
107107
}
108108
shouldThrow<IllegalStateException> {
109-
supervisorScope {
110-
cache.get("foo") { error("kapow") }
109+
cache.get("foo") { error("kapow") }
110+
}
111+
}
112+
113+
test("get should propagate exceptions in the compute function") {
114+
val cache = Caffeine.newBuilder().asCache<String, String>()
115+
shouldThrow<IllegalStateException> {
116+
cache.get("foo") {
117+
error("kapow")
111118
}
112119
}
113120
}

0 commit comments

Comments
 (0)