Skip to content

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

Description

@wushiyuanmaimob

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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions