Skip to content

Improve non-blocking support for @Cacheable suspend functions by using retrieve() instead of get() #36644

@thomasdac

Description

@thomasdac

Enhancement Request: Alignment of Kotlin Coroutines 'suspend' support with Reactive 'retrieve' pipeline

Concrete Use Case

In a high-throughput Spring Boot application using Spring WebFlux and Kotlin Coroutines, we expect the entire execution chain to be non-blocking. When using @Cacheable on a suspend function, the framework is expected to leverage the non-blocking capabilities of the underlying cache (e.g., Redis via Lettuce or Caffeine).

However, the current implementation of CacheAspectSupport treats suspend functions differently from Publisher types (Mono/Flux). Even with the kotlinx-coroutines-reactor bridge, a suspend function with sync = false (default) triggers a synchronous Cache.get(key) call. In a reactive environment, this synchronous call can lead to "thread pinning" or starvation of the Event Loop/Worker threads if the cache access is slow or remote.

How I have tried to solve this so far

Enabling sync = true: This is the only way to force the interceptor to use the retrieve method (which returns a CompletableFuture). While this makes the flow non-blocking, it introduces mandatory synchronization (locks) at the cache level, which is often undesirable when we only want an asynchronous lookup without the overhead of a coordinated lock.

Manual Bridge: Implementing a custom Cache wrapper that delegates get to getAsync().join(). While this works, it still consumes a thread waiting for the result, rather than allowing the Coroutine to suspend naturally in a non-blocking pipeline.

Proposed Change

I have identified an inconsistency in CacheAspectSupport.java that prevents suspend functions from benefiting from the reactive pipeline:

  • Sequential Lookup: For suspend functions (around line 541), the logic falls back to findInCaches, which is strictly synchronous.

  • Reactive Advantage: In contrast, the ReactiveCacheAspectSupport logic (around line 1143) uses a retrieve based approach, providing a fully non-blocking CompletionStage chain.

The enhancement would consist of treating suspend function returns as "async-first" citizens, similar to Publisher. Since Spring 6.1+ already recognizes suspend functions, the interceptor should be able to route these calls through the retrieve/CompletableFuture path even when sync = false. This would ensure that the Coroutine suspension is truly non-blocking regarding the cache I/O, matching the performance characteristics of Project Reactor types.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions