spring-projects / spring-projects/spring-framework

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

Open
#36,644 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage theme: kotlin
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 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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.