Search code examples
kotlincachingkotlin-coroutinescaffeine

How to use @Cacheable with Kotlin suspend funcion


I am working in a Kotlin and Spring Boot project and I am trying to use Caffeine for caching. I have a service with a suspending function that makes an http call. Here is my config:

@Bean
open fun caffeineConfig(): @NonNull Caffeine<Any, Any> {
   return Caffeine.newBuilder().expireAfterWrite(60, TimeUnit.SECONDS)
}

@Bean
open fun cacheManager(caffeine: Caffeine<Any, Any>): CacheManager {
    val caffeineCacheManager = CaffeineCacheManager()
    caffeineCacheManager.getCache("test")
    caffeineCacheManager.setCaffeine(caffeine)
    return caffeineCacheManager
}

And here is the function that I want to cache:

@Cacheable(value = ["test"])
open suspend fun getString(id: String): String {
    return client.getString(id)
}

But it seems that the caching is not working since I can see from logs that the client gets called every time the service-function gets called. Does @Cacheable not work for suspending functions? Or am I missing something else?


Solution

  • The documentation of @Cacheable says:

    Each time an advised method is invoked, caching behavior will be applied, checking whether the method has been already invoked for the given arguments. A sensible default simply uses the method parameters to compute the key, but a SpEL expression can be provided via the key() attribute, or a custom KeyGenerator implementation can replace the default one (see keyGenerator()).

    The suspend modifier inserts an Continuation<String> parameter in the generated code which accepts input from the caller. This presumably means each invocation gets its own continuation and the cache detects this as a unique call.

    However since the return value also gets changed depending on the continuation you cannot have the cache ignore the continuation parameter. A better approach is to not use suspend functions and instead returning a Deferred which consumers can share:

    @Cacheable(value = ["test"])
    open fun getString(id: String): Deferred<String> {
        return someScope.async {
            client.getString(id)
        }
    }
    
    // Consumer side
    getString(id).await()
    

    This should work with the standard caching mechanism since Deferred is a normal object and no special parameters are required.