Skip to content

Release reactive connection that arrives after close#3372

Open
wushiyuanmaimob wants to merge 1 commit into
spring-projects:mainfrom
wushiyuanmaimob:fix/3609-reactive-connection-pool-leak
Open

Release reactive connection that arrives after close#3372
wushiyuanmaimob wants to merge 1 commit into
spring-projects:mainfrom
wushiyuanmaimob:fix/3609-reactive-connection-pool-leak

Conversation

@wushiyuanmaimob
Copy link
Copy Markdown

Closes #3371

Problem

LettuceReactiveRedisConnection.AsyncConnect leaks a pooled connection when the connection acquisition is cancelled (request timeout, client disconnect, subscription cancellation) before the connection has arrived from the LettuceConnectionProvider.

The late-arriving connection is closed via it.closeAsync() but never released back to the provider, so a pooled provider keeps counting it as active (Lettuce's BoundedAsyncPool never decrements objectCount / removes it from the all queue). Over time the pool is exhausted and throws PoolException / Pool exhausted, even though the physical connections are already closed.

Race scenario:

  1. Subscriber subscribes to getConnection(); a connection is requested from the pool. this.connection is still null.
  2. The subscription is cancelled, so close() runs. Because this.connection is still null, releaseAsync(...) is skipped and the state becomes CLOSED.
  3. The pool finally yields the connection. doOnNext sees the closing state and calls it.closeAsync().
  4. releaseAsync(it) is never called, so the pool never reclaims the slot. Leak.

Fix

Release the late-arriving connection through connectionProvider.releaseAsync(it) instead of it.closeAsync(), mirroring what close() already does for an already-arrived connection. For non-pooled providers this is equivalent to closing (the default releaseAsync delegates to closeAsync()); for pooled providers it correctly returns the connection to the pool.

Tests

New unit test shouldReleaseConnectionArrivingAfterClose reproduces the race using a deferred connection future: it subscribes, closes while the acquisition is in-flight, then completes the future and verifies releaseAsync is invoked for the late-arriving connection. The test fails on main (Wanted but not invoked: releaseAsync) and passes with this change.

This issue was originally reported against Lettuce as redis/lettuce#3609, but the root cause is here in AsyncConnect.


  • You have read the Spring Data contribution guidelines.
  • You use the code formatters provided and have them applied to your changes.
  • You submit test cases (unit or integration tests) that back your changes.
  • You added yourself as author in the headers of the classes you touched.

LettuceReactiveRedisConnection.AsyncConnect leaked a pooled connection
when the connection acquisition was cancelled before the connection
arrived from the LettuceConnectionProvider. The late-arriving connection
was closed with closeAsync() but never released back to the provider, so
a pooled provider kept counting it as active and eventually exhausted
the pool.

Release the late-arriving connection through
connectionProvider.releaseAsync(it) instead, mirroring close(). For
non-pooled providers this is equivalent to closing (the default
releaseAsync delegates to closeAsync), and for pooled providers it
returns the connection to the pool.

Closes spring-projects#3371

Signed-off-by: sywu14 <sywu14@iflytek.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reactive connection leaks from pool when acquisition is cancelled before the connection arrives

2 participants