How to use @Cacheable with Kotlin suspend funcion
Asked Answered
O

2

6

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?

Oestrogen answered 15/10, 2020 at 13:26 Comment(0)
W
14

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.

Waylay answered 15/10, 2020 at 13:53 Comment(4)
This worked! Thank you for your help and for a really good explanation!Oestrogen
This might seem like easy solution but be warned that with this approach exception might be cached.Lifer
I tried exactly the same thing but did not work for me. Are there any other tricks that I am missing?Gnat
This now works in Spring 6.1, link in the other answer below.Arnitaarno
A
2

@Cacheable is now supported in Spring 6.1:

https://github.com/spring-projects/spring-framework/issues/31412

Arnitaarno answered 23/1 at 0:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.