spring-projects / spring-projects/spring-framework
Improve non-blocking support for @Cacheable suspend functions by using retrieve() instead of get()
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 60.2k
- Forks
- 38.8k
- Avg merge
- 5d 2h
- Merged PRs (30d)
- 27
Description
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
ReactiveCacheAspectSupportlogic (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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java around lines 541 and 1143. Compare the suspend-function path with ReactiveCacheAspectSupport's retrieve-based CompletionStage path, then verify that suspend functions with sync = false use the non-blocking route without losing the intended cache behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, kotlin, spring
- Domain
- backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100