Miet0
Miet0

Reputation: 23

spring boot cachable, ehcache with Kotlin coroutines - best practises

I am struggling with proper coroutine usage on cache handling using spring boot @Cacheable with ehcache on two methods:

  1. calling another service using webclient:
suspend fun getDeviceOwner(correlationId: String, ownerId: String): DeviceOwner{
    webClient
                .get()
                .uri(uriProvider.provideUrl())
                .header(CORRELATION_ID, correlationId)
                .retrieve()
                .onStatus(HttpStatus::isError) {response ->
                    Mono.error(
                        ServiceCallExcpetion("Call failed with: ${response.statusCode()}")
                    )
                }.awaitBodyOrNull()
                ?: throw ServiceCallExcpetion("Call failed with - response is null.")
}
  1. calling db using r2dbc

suspend fun findDeviceTokens(ownerId: UUID, deviceType: String) {
  //CoroutineCrudRepository.findTokens
}

What seems to be working for me is calling from:

suspend fun findTokens(data: Data): Collection<String> = coroutineScope {
        val ownership = async(Dispatchers.IO, CoroutineStart.LAZY) { service.getDeviceOwner(data.nonce, data.ownerId) }.await()
        val tokens = async(Dispatchers.IO, CoroutineStart.LAZY) {service.findDeviceTokens(ownership.ownerId, ownership.ownershipType)}
        tokens.await()
    }
    @Cacheable(value = ["ownerCache"], key = "#ownerId")
fun getDeviceOwner(correlationId: String, ownerId: String)= runBlocking(Dispatchers.IO) {
    //webClientCall
}
 @Cacheable("deviceCache")
override fun findDeviceTokens(ownerId: UUID, deviceType: String) = runBlocking(Dispatchers.IO) {
  //CoroutineCrudRepository.findTokens
}

But from what I am reading it's not good practise to use runBlocking. https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine Would it block the main thread or the thread which was designated by the parent coroutine?

I also tried with

    @Cacheable(value = ["ownerCache"], key = "#ownerId")
fun getDeviceOwnerAsync(correlationId: String, ownerId: String) = GlobalScope.async(Dispatchers.IO, CoroutineStart.LAZY) {
    //webClientCall
}
 @Cacheable("deviceCache")
override fun findDeviceTokensAsync(ownerId: UUID, deviceType: String) = GlobalScope.async(Dispatchers.IO, CoroutineStart.LAZY) {
  //CoroutineCrudRepository.findTokens
}

Both called from suspended function without any additional coroutineScope {} and async{}

suspend fun findTokens(data: Data): Collection<String> =
    service.getDeviceOwnerAsync(data.nonce,data.ownerId).await()
       .let{service.findDeviceTokensAsync(it.ownerId, it.ownershipType).await()}
    

I am reading that using GlobalScope is not good practise either due to possible endless run of this coroutine when something stuck or long response (in very simple words). Also in this approach, using GlobalScope, when I tested negative scenarios and external ms call resulted with 404(on purpose) result was not stored in the cache (as I excepted) but for failing CoroutineCrudRepository.findTokens call (throwing exception) Deferred value was cached which is not what I wanted. Storing failing exececution results is not a thing with runBlocking.

I tried also @Cacheable("deviceCache", unless = "#result.isCompleted == true && #result.isCancelled == true") but it also seems to not work as I would imagine.

Could you please advice the best coroutine approach with correct exception handling for integrating with spring boot caching which will store value in cache only on non failing call?

Upvotes: 2

Views: 2894

Answers (2)

Michal
Michal

Reputation: 133

Although annotations from Spring Cache abstraction are fancy, I also, unfortunately, haven't found any official solution for using them side by side with Kotlin coroutines.

Yet there is a library called spring-kotlin-coroutine that claims to solve this issue. Though, never tried as it doesn't seem to be maintained any longer - the last commit was pushed in May 2019.

For the moment I've been using CacheManager bean and managing the aforementioned manually. I found that a better solution rather than blocking threads.


Sample code with Redis as a cache provider:

Dependency in build.gradle.kts:

    implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")

application.yml configuration:

    spring:
      redis:
        host: redis
        port: 6379
        password: changeit
      cache:
        type: REDIS
        cache-names:
          - account-exists
        redis:
          time-to-live: 3m



 

Code:

    @Service
    class AccountService(
      private val accountServiceApiClient: AccountServiceApiClient,
      private val redisCacheManager: RedisCacheManager
    ) {

      suspend fun isAccountExisting(accountId: UUID): Boolean {
        if (getAccountExistsCache().get(accountId)?.get() as Boolean? == true) {
          return true
        }
        
        val account = accountServiceApiClient.getAccountById(accountId) // this call is reactive
        if (account != null) {
          getAccountExistsCache().put(account.id, true)
          return true
        }
        
        return false
      }
      
      private fun getAccountExistsCache() = redisCacheManager.getCache("account-exists") as RedisCache
    }

Upvotes: 2

Jorge F. Sanchez
Jorge F. Sanchez

Reputation: 341

In the Kotlin Coroutines context, every suspend function has 1 additional param of type kotlin.coroutines.Continuation<T>, that's why the org.springframework.cache.interceptor.SimpleKeyGenerator generates always a wrong key. Also, the CacheInterceptor does not know anything about suspend functions, so, it stores a COROUTINE_SUSPENDED object instead of the actual value, without evaluating the suspended wrapper.

You can check this repository https://github.com/konrad-kaminski/spring-kotlin-coroutine, they added Cache support for Coroutines, the specific Cache support implementation is here -> https://github.com/konrad-kaminski/spring-kotlin-coroutine/blob/master/spring-kotlin-coroutine/src/main/kotlin/org/springframework/kotlin/coroutine/cache/CoroutineCacheConfiguration.kt.

Take a look at CoroutineCacheInterceptor and CoroutineAwareSimpleKeyGenerator,

Hope this fixes your issue

Upvotes: 3

Related Questions