Skip to content

Fix hanging retries for Flux-returning methods with caching - #37403

Merged
sbrannen merged 1 commit into
spring-projects:7.0.xfrom
noojung:fix/reactive-cache-resubscription
Oct 6, 2026
Merged

sbrannen merged 1 commit into
spring-projects:7.0.xfrom
noojung:fix/reactive-cache-resubscription

Conversation

@noojung

@noojung noojung commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Combining @Retryable with @Cacheable or @CacheEvict on a method returning a Flux can cause the operation to hang when the publisher fails and a retry is attempted:

@Cacheable("items")
@Retryable
public Flux<Item> findItems() { ... }  // hangs on the first retry

Problem

ReactiveCachingHandler shares the returned Flux between the caller and an internal subscriber that collects emitted values for cache put or eviction operations, using publish().refCount(2). Both subscribers terminate when the source signals an error. On resubscription (for example, due to @Retryable), only the caller subscribes again, leaving refCount(2) waiting for a second subscriber and preventing a new subscription to the source.

This PR addresses two distinct problems caused by this pattern:

  1. Existing bug (@Cacheable / @CachePut): processPutRequest() has used publish().refCount(2) since 6.1, so any resubscription after an error hangs. @Retryable just makes it easy to hit.
  2. Unreleased regression (@CacheEvict): processCacheEvicts() copied the same pattern in 26de340 (@CacheEvict truncates Flux results to the first element #37309), so a Flux-returning @CacheEvict method combined with @Retryable now hangs, whereas it did not before that commit.

Fix

Wrap the shared Flux creation and internal subscriber registration in Flux.defer() so that each subscription, including a retry, creates a new shared Flux and registers a new cache subscriber. Apply this change to both cache put and eviction handling.

Tests

Add regression tests for @Retryable combined with @Cacheable or @CacheEvict. The tests verify that an initial failure is followed by a successful second subscription, that all expected values are returned, and that the successful operation populates or evicts the cache as appropriate.

Related Issues

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Oct 6, 2026
@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label Oct 6, 2026
@sbrannen sbrannen self-assigned this Oct 6, 2026
@sbrannen sbrannen added this to the 7.0.10 milestone Oct 6, 2026
@sbrannen sbrannen added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Oct 6, 2026

@sbrannen sbrannen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR.

The Flux.defer() approach looks right to me. I applied the change locally and confirmed that both new tests fail without it and pass with it.

Could you please update this PR's description and the commit message to distinguish the two problems this PR addresses?

  1. Existing bug (@Cacheable / @CachePut): processPutRequest() has used publish().refCount(2) since 6.1, so any resubscription after an error hangs. @Retryable just makes it easy to hit.
  2. Unreleased regression (@CacheEvict): processCacheEvicts() copied the same pattern in 26de340 (gh-37309), so a Flux-returning @CacheEvict method combined with @Retryable now hangs, whereas it did not before that commit.

Please also end the commit message with the following trailers:

See gh-37309
Closes gh-37403

Note that annotations such as @Cacheable should be enclosed in backticks in the commit message, so that they don't mention GitHub users.

Also, this should target 7.0.x rather than main, since 26de340 is already on that branch. We will forward merge it to main on our side.

Cheers

@sbrannen sbrannen added the status: waiting-for-feedback We need additional information before we can continue label Oct 6, 2026
@noojung
noojung force-pushed the fix/reactive-cache-resubscription branch from cc2fe6c to 6638d91 Compare October 6, 2026 12:44
@noojung
noojung changed the base branch from main to 7.0.x October 6, 2026 12:48
Fix an existing bug in processPutRequest() affecting `@Cacheable`
and `@CachePut` since 6.1: resubscription after an error hangs while
publish().refCount(2) waits for a second subscriber.

Also fix an unreleased regression introduced by 26de340 (spring-projectsgh-37309) for
`@CacheEvict`, which copied the same pattern into processCacheEvicts().

Create the shared Flux and cache subscriber for each subscription
via Flux.defer(). Add regression tests combining reactive caching
with `@Retryable`.

See spring-projectsgh-37309
Closes spring-projectsgh-37403

Signed-off-by: Hyunwoo Jung <hyunwoojung@kakao.com>
@noojung
noojung force-pushed the fix/reactive-cache-resubscription branch from 6638d91 to 9801b81 Compare October 6, 2026 12:55
@noojung

noojung commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review!

I've addressed your feedback:

  • Updated the commit message and the PR description to distinguish the existing @Cacheable / @CachePut bug from the unreleased @CacheEvict regression
  • Added the See gh-37309 and Closes gh-37403 trailers to the commit message
  • Rebased the branch onto 7.0.x and changed the base branch accordingly
  • I also added // gh-37403 comments to the new tests

Please let me know if you have any further feedback.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Oct 6, 2026
@noojung
noojung requested a review from sbrannen October 6, 2026 13:14
@noojung

This comment was marked as outdated.

@sbrannen sbrannen removed the status: feedback-provided Feedback has been provided label Oct 6, 2026

@sbrannen sbrannen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making the requested changes.

I'll make a final review locally before merging this into 7.0.x and main.

@sbrannen
sbrannen merged commit 440139f into spring-projects:7.0.x Oct 6, 2026
3 of 4 checks passed
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Fixed via 440139f

sbrannen added a commit that referenced this pull request Oct 6, 2026
- Add tests for @⁠CachePut, @⁠Cacheable, and
  @⁠CacheEvict with Flux resubscription in
  ReactiveCachingTests, using plain Reactor retry() instead of
  @⁠Retryable.
- Reduce the block() timeouts in ReactiveRetryInterceptorTests from 5
  seconds to 1 second so that regressions fail faster.

See gh-37403
@sbrannen

sbrannen commented Oct 6, 2026

Copy link
Copy Markdown
Member

This has been merged into 7.0.x and main.

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in: core Issues in core modules (aop, beans, core, context, expression) type: bug A general bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants