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?

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
jojo
  • 63
  • 1
  • 4

1 Answers1

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.

Kiskae
  • 24,655
  • 2
  • 77
  • 74
  • This worked! Thank you for your help and for a really good explanation! – jojo Oct 16 '20 at 07:00
  • 1
    This might seem like easy solution but be warned that with this approach exception might be cached. – Piotr May 29 '22 at 19:23
  • I tried exactly the same thing but did not work for me. Are there any other tricks that I am missing? – Ktt Feb 15 '23 at 13:25