Skip to content

LDEV-6503 discard timed-out connections instead of returning them to the pool - #2853

Merged
michaeloffner merged 1 commit into
lucee:7.1from
GianTschingt:LDEV-6503-pool-timeout-7.0
Oct 5, 2026
Merged

michaeloffner merged 1 commit into
lucee:7.1from
GianTschingt:LDEV-6503-pool-timeout-7.0

Conversation

@GianTschingt

@GianTschingt GianTschingt commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Jira: https://luceeserver.atlassian.net/browse/LDEV-6503
Forum report: https://dev.lucee.org/t/17621

Problem

Since LDEV-5966 (3417970d4, 6.2.5.6 / 7.0.2.10) DatasourceManagerImpl.releaseConnection() always returns the connection to the pool with release(). Before that, a connection used by a timed-out request was closed. The idea was that validation on the next borrow catches a bad connection. But with validate=false (the default) validateObject() only checks isClosed() and the idle/live timeout.

On 6.2 and 7.0 a request timeout interrupts the thread and then calls Thread.stop(). The ThreadDeath hits as soon as the driver's socket read returns, so the response is left half read on the connection. The connection goes back to the pool and the next request that borrows it gets the result of the timed-out query instead of its own. On a server-level datasource that can be another application or user.

Fix

In releaseConnection(), if the current thread is interrupted, or the request (ThreadLocalPageContext.get(pc)) has timed out (getTimeoutStackTrace() != null), the connection is invalidated in the pool with the existing private invalidate(dc) helper (LDEV-6129) instead of release(). The connection is destroyed through commons-pool2 (invalidateObject()), so the pool counters stay right and the LDEV-5966 fix is kept. One change in one place: Query, QueryLazy, Insert/Update, StoredProc, DBInfo and the transaction end() path all release through this method.

No classloader changes, no new exception wrapping, no duplicated code. It reuses invalidate(), ThreadLocalPageContext.get() and PageContextImpl.getTimeoutStackTrace().

Trade-off

  • The interrupt check also covers cfthread action="terminate" (there SystemUtil.stop() gets no PageContext, so no timeout stack trace is set).
  • The cost: if a stale interrupt flag is left on a thread, every release in that request discards its connection. That means extra reconnects, but never wrong data.
  • On Java 20+ (Thread.stop() is gone) the query may have finished cleanly before the release. The connection is still discarded, so it's one reconnect per timed-out request, like before 6.2.5.6.

Test

test/tickets/LDEV6503.cfc (labels postgres,datasource, skipped when no Postgres datasource is configured; CI has one). It uses a datasource with validate=false, records pg_backend_pid(), then runs a cfthread with requesttimeout=1 and pg_sleep(5) that returns A_MARKER,A_ID. While that query runs, the test calls getCFMLFactory().checkTimeout() itself: the controller only checks for request timeouts every 5 seconds, so otherwise it could act after the query has finished (then nothing was aborted and the connection is rightly reused). After that it asserts:

  1. the next query gets its own columns (PID), not the timed-out request's result (fails without the fix on Java < 20);
  2. the backend pid is different, i.e. the connection of the timed-out request was not reused (fails without the fix on every Java version, including CI's);
  3. the pool's active count is back to where it started (LDEV-5966 accounting), and a further query returns the right result.

Repro summary

The forum repro (request A in a cfthread with requesttimeout=3, then request B on the same datasource), PostgreSQL 17, bundled pgjdbc, validate=false, 6 runs each:

Version Java Result
6.2.8.20 17 6/6 swapped (B got A_MARKER,A_ID)
6.2.9.4-RC 17 5/6 swapped
7.0.6.9-RC 17 5/6 swapped
7.1.1.15-RC 17 no swap (7.1 doesn't call Thread.stop()), but the same connection is reused after every timeout
6.2.9.4-RC, 7.0.6.9-RC, 7.1.1.15-RC, 8.0 snapshot 21 no swap, but the same connection is reused after every timeout
with this fix 17, 21 0 swaps, a new backend pid after every timed-out request

validate=true on the datasource avoided the swap in all runs.

Local verification

The changed class was compiled from this branch (javac --release 11) into the 7.1.1.15-RC jar. The branch base (7.1 2059469e) has the same core sources as that RC, only version and build files changed since. LDEV6503.cfc was run with script-runner against a local PostgreSQL 17:

  • unpatched 7.1.1.15-RC, Java 17 and 21: fails, same backend pid (the next query does get its own result)
  • with this PR, Java 17 and 21: passes (about 5 s)

Merge-up

The branch is based on the current 7.1 tip (2059469e). It also applies cleanly to 8.0 (git apply --check on the current tip 92487b9e).

Opened by Gian Tschingt, Lucee community assistant for Michael Offner.

…the pool

- DatasourceManagerImpl.releaseConnection() invalidates the connection in the
  pool (existing invalidate() helper from LDEV-6129) instead of release() when
  the thread is interrupted or the request timed out; a request timeout can stop
  the driver in the middle of reading a response and the next borrower would get
  that unread result
- the pool accounting from LDEV-5966 is kept, the connection is destroyed
  through the pool
- test/tickets/LDEV6503.cfc
@GianTschingt
GianTschingt force-pushed the LDEV-6503-pool-timeout-7.0 branch from 1913131 to 3490b5a Compare October 3, 2026 09:08
@michaeloffner
michaeloffner merged commit ff99e3b into lucee:7.1 Oct 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants