Mejmo
Mejmo

Reputation: 2593

What is the preferred way how to add Caffeine cache to kotlin with coroutines

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

Answers (3)

hsbrysk
hsbrysk

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

P. Luo
P. Luo

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

sksamuel
sksamuel

Reputation: 16387

You cannot use a suspendable function inside the Cache loading function, because those functions are not coroutines.

You have several options.

  1. If you don't mind "wasting" a thread, and you are using a Cache or LoadingCache you can use runBlocking.
inMemoryCache.get(id) { id ->
   runBlocking {
     some call to external service
   }
}
  1. You can convert the external call to a Future if you are using an AsyncCache or AsyncLoadingCache. Note you must create a CoroutineScope in order to call async.
inMemoryCache.get(id) { id, _ ->
  scope.async { compute(k) }.asCompletableFuture().await()
}
  1. Use a Kotlin wrapper for Caffeine.

Upvotes: 3

Related Questions