spring-r2dbc: Concurrent first `ConnectionFactoryUtils` acquisitions leak a connection and fail during reactive resource binding

Author: XiphoseerCreated Sep 14, 2026Updated Sep 14, 2026
Labelsin: datatype: bug

Summary

ConnectionFactoryUtils.getConnection(ConnectionFactory) can fail when two concurrent first acquisitions of the same, previously unbound ConnectionFactory share an active reactive transaction context with synchronization enabled. Any reactive transaction manager can establish that context; it may but need not wrap a ConnectionFactory. If it does, that factory must be distinct from the contended one (after unwrapping).

Both acquisitions observe no holder, each asynchronously creates a connection, and both then try to bind a distinct ConnectionHolder for the same factory. The later bind fails with:

java.lang.IllegalStateException: Already value [...] for key [...] bound to context

This is an async check-then-act race involving Spring Framework's spring-tx transaction-resource binding. To reproduce it, no driver, database, parallel threads, or probabilistic timing are required.

It is primarily a spring-r2dbc issue: ConnectionFactoryUtils owns the asynchronous acquisition and binding protocol. spring-tx supplies the transaction-context resource map and duplicate-binding invariant, but TransactionSynchronizationManager.bindResource is behaving as designed when it rejects the second holder.

LLM Disclosure

This report and the analysis were prepared with assistance from an LLM. The supplied test was compiled and executed locally as described above; the behavior and source-path claims were independently verified from that execution and the Spring Framework 7.0.2 source.

I'm open to preparing a regression-test or implementation pull request once the intended semantics for concurrent first acquisition are confirmed.

Version

  • Spring Framework 7.0.2 (latest GA when verified)
  • Reactor Core / Reactor Test 3.8.1
  • Java 21
  • Maven 3.9.7

The same reproducer also passes its expected-error assertions on Spring Framework 6.2.15. No version bisection has been performed.

Minimal sample

The sample is available at https://github.com/Xiphoseer/spring-r2dbc-connection-binding-race (ConnectionBindingRaceTest.java and pom.xml). Run:

mvn test -Dtest=ConnectionBindingRaceTest

Six nested tests are made to pass: three assert the observed error, while the controls cover sequential access, pre-binding, and concurrent reuse after binding.

Most scenarios use R2dbcTransactionManager with one factory and contend on a second factory. This is not a coordinated two-resource transaction; the second factory only shares the reactive transaction context and synchronization lifecycle. A synchronization-only AbstractReactiveTransactionManager also reproduces the failure.

The test uses mocked connections and virtual time; distinct delays make the losing contender deterministic. It exercises Spring's binding code directly. Driver behavior cannot sidestep this race: both calls have already observed an unbound factory before either driver's create() completes; driver timing only determines which bind loses.

Actual behavior

Within ConnectionFactoryUtils.doGetConnection, both Mono.zip branches do the following for the same previously unbound factory:

  1. Read null from TransactionSynchronizationManager.getResource(connectionFactory).
  2. Cross the asynchronous fetchConnection(connectionFactory) boundary.
  3. Create a new holder, register a synchronization, and call bindResource.

The first completion binds successfully. The second calls bindResource with a different holder and fails. The reproducer verifies that exactly one connection is closed during cleanup: the first connection, which completed the successful bind. The second connection, whose bind failed, is left unreleased. This makes the race a resource-leak issue in addition to the observed error.

Expected behavior

Concurrent first acquisitions for one ConnectionFactory in the same active reactive transaction should coordinate as one logical acquisition: later callers should reuse or await the holder/connection established by the first caller. They should not fail merely because their creation completion interleaves after the initial getResource check.

Analysis

A concurrent map would address collection safety/visibility but not this invariant: both callers have already committed to distinct holders across the async boundary. The fix needs atomic reservation plus single-flight initialization (or equivalent coordination) per factory in a transaction context, with cleanup and synchronization registration kept consistent.

Note that a thread-bound connection holder does not expose this same race: another thread cannot see or bind into the first thread's ThreadLocal transaction resources. A reactive transaction context can instead be shared by concurrent publishers, allowing both to observe the factory as unbound before either asynchronous acquisition completes.

Potential fix: complete one ConnectionHolder in place

Bind one ConnectionHolder before asynchronous creation and transition it from pending to ready in place. Concurrent callers await its shared completion. On failure or cancellation, they see the same error and the pending binding is removed. This preserves resource identity.

An atomic bind-if-absent operation is still required. The pending state can remain R2DBC-specific; spring-tx would provide only generic resource coordination. Synchronization and cleanup while the holder is pending need explicit handling.

Alternative: serialize the first acquisition at the call site

Treat this as an application-level constraint: establish one entry point for the first ConnectionFactory acquisition in a transaction, then share its connection or holder. Callers must serialize or share that acquisition, even when composition through Mono.zip makes the concurrency otherwise natural.

IllegalStateException accurately describes the duplicate binding invariant, but may be a poor application-facing error. This avoids framework changes while making concurrent composition the caller's responsibility.

Related issues

  • #30134 added savepoint-backed PROPAGATION_NESTED support to R2dbcTransactionManager in 6.0.10. It is related because the previous nested-transaction behavior surfaced the same Already value [...] bound to context exception from the same module. It is different because the nested manager deliberately attempted to bind another holder for its own already-bound factory; this reproducer has two concurrent first acquisitions that both observed no holder for a previously unbound factory. The 7.0.2 source still has the latter async check-then-bind sequence.
  • #35921, backported as #35922, made reactive transaction synchronization registration use CopyOnWriteArraySet. It is related because it acknowledges interleaved reactive operations against a transaction context. It is different because this failure concerns the separate resources map and, more fundamentally, the async first-acquisition protocol; the 7.0.2 source still has that protocol and LinkedHashMap.
  • #32115 reports R2DBC resource cleanup/leak behavior around cancellation and PROPAGATION_REQUIRES_NEW. It is related through reactive transaction resource lifecycle failures, but differs in trigger and symptom: it uses real PostgreSQL/pooling, nested transactions, and timeout cancellation; this reproducer fails during the initial same-factory bind before those paths.
  • #28133 fixed a transactional R2DBC connection being closed by TransactionAwareConnectionFactoryProxy. It is related by the same ConnectionFactoryUtils lifecycle area, but differs because it is an erroneous release/close of an already-bound connection, rather than a concurrent initial bind.

Source: spring-projects/spring-framework