Reputation: 2593
I am trying to integrate Caffeine cache into kotlin+spring boot application, however, I am getting the problem of calling the suspension function in the non-coroutine body. I get this, but I am looking for a solution that should be a bit more standard. I can find only one solution on the web that leads to SO, where I do not really see a stable way how to fix this.
inMemoryCache.get(id) { id ->
some call to external service <--- "Suspension function can be called only within coroutine body"
}
Upvotes: 5
Views: 4993
Reputation: 31
I have written this library. It can solve this problem, so please use it if you like. https://github.com/be-hase/caffeine-coroutines
Upvotes: 0
Reputation: 59
Observe (a) Caffeine AsyncCache::get
signature:
public interface AsyncCache<K, V> {
CompletableFuture<V> get(K key,
BiFunction<? super K, Executor, CompletableFuture<V>> mappingFunction);
}
and (b) Kotlin coroutines signatures:
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
public fun <T> CoroutineScope.future(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
) : CompletableFuture<T>
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> //Java
public suspend fun <T> CompletionStage<T>.await(): T
Suppose you have a suspending mapping function myCreate
. You can use CoroutineScope.future()
to convert it to a CompletableFuture, pass the future into AsyncCache::get
, and call await()
to make it suspending, such that you can leverage structured concurrency.
An example:
import com.github.benmanes.caffeine.cache.AsyncCache
import com.github.benmanes.caffeine.cache.Caffeine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.future.await
import kotlinx.coroutines.future.future
import java.util.concurrent.TimeUnit
import javax.inject.Named
@Named
class CacheStore
{
class Entry (val value: Double)
val cache: AsyncCache<String, Entry> = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(1, TimeUnit.HOURS)
.buildAsync()
suspend fun get( key: String,
create: suspend CoroutineScope.() -> Entry
): Entry = coroutineScope {
val fut = cache.get(key) { _, _ -> future { create() } }
fut.await()
}
}
...
suspend fun invoke(): CacheStore.Entry {
val entry = cacheStore.get(key) {
// logging or other logic
myCreate(arg)
}
return entry
}
suspend fun myCreate(arg: Double): CacheStore.Entry {
...
}
Ref: the official KEEP coroutines proposal on how to convert between callbacks, futures, and suspending functions:
https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md#asynchronous-programming-styles
Upvotes: 5
Reputation: 16387
You cannot use a suspendable function inside the Cache loading function, because those functions are not coroutines.
You have several options.
Cache
or LoadingCache
you can use runBlocking
.inMemoryCache.get(id) { id ->
runBlocking {
some call to external service
}
}
AsyncCache
or AsyncLoadingCache
. Note you must create a CoroutineScope
in order to call async.inMemoryCache.get(id) { id, _ ->
scope.async { compute(k) }.asCompletableFuture().await()
}
Upvotes: 3