spring-projects / spring-projects/spring-data-redis

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

Open
#3,371 0 comments 0 reactions 1 assignee View on GitHub

@mp911de is already working on this.

Since Jun 1, 2026.

type: bug
Dominant language
Java
Stars
1.9k
Forks
1.3k
PR merge metrics
No merged PRs in 30d

Description

Bug Report

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

When this happens, the late-arriving connection is closed via it.closeAsync() but is never released back to the connection provider, so a pooled provider's internal accounting (objectCount / all queue in Lettuce's BoundedAsyncPool) still counts the connection as active. Over time this exhausts the pool and leads to PoolException / NoSuchElementException: Pool exhausted, even though the physical connections are already closed.

Current Behavior

In the AsyncConnect constructor:

this.connectionPublisher = defer.doOnNext(it -> {
    if (isClosing(STATE.get(this))) {
        it.closeAsync();           // closes the connection but never releases the pool slot
    } else {
        connection = it;
    }
})

Race scenario:

  1. Subscriber subscribes to getConnection(); AsyncConnect requests a connection from the pool. this.connection is still null.
  2. The subscription is cancelled, so close() runs. Because this.connection is still null, connectionProvider.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. connectionProvider.releaseAsync(it) is never called, so the pool never reclaims the slot. Leak.
Expected Behavior

A connection that arrives after close() should be released back to the connection provider (connectionProvider.releaseAsync(it)), mirroring what close() itself does for an already-arrived connection. For non-pooled providers this is equivalent to closing (the default releaseAsync delegates to closeAsync()), and for pooled providers it correctly returns the connection to the pool.

Context

Originally reported against Lettuce as redis/lettuce#3609, but the root cause is in Spring Data Redis's AsyncConnect. The fix is a one-line change plus a regression test; PR to follow.

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.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.