fix(postgres): roll back when begin() is cancelled after queuing BEGIN - #4365
Open
jiaming-cao-1111 wants to merge 1 commit into
Open
fix(postgres): roll back when begin() is cancelled after queuing BEGIN#4365jiaming-cao-1111 wants to merge 1 commit into
jiaming-cao-1111 wants to merge 1 commit into
Conversation
…response A future dropped between queueing BEGIN and reading its response left the server session inside a transaction that the client no longer tracked (transaction_depth is only incremented after the response is read, so the Rollback drop guard was a no-op at depth 0). The connection then returned to the pool and subsequent checkouts silently ran inside the leaked transaction. Queue an explicit ROLLBACK in the guard for the depth-0 case; if the BEGIN never reached the server the paired ROLLBACK is a harmless warning.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2805.
Problem
PgTransactionManager::beginqueuesBEGIN, then awaitswait_until_readybefore incrementingtransaction_depth. If the future is dropped at that await point (client disconnect,tokio::time::timeout,select!), the server has already executedBEGINbut the client-side depth is still 0, so theRollbackdrop guard'sstart_rollbackis a no-op. The connection is then returned to the pool with an untracked open transaction: the backend sits inidle in transaction, every subsequent checkout runs inside the leaked transaction (producing thethere is already a transaction in progresswarnings reported in #2805), rows it wrote stay locked, and the writes are silently discarded when the connection is eventually closed.#2054 reported this class of bug; #2057 fixed it for SQLite only. #3980 constructs the
Transactionbefore begin so itsDropalways runs, but for Postgres the drop path is still gated ontransaction_depth > 0, which is exactly the state a cancelled outermostBEGINleaves behind — verified against 0.9.0.Fix
In the begin drop guard, when
transaction_depth == 0, queue an explicitROLLBACK. Reaching the guard with depth 0 can only mean the outermostBEGINwas queued but its response never read. If theBEGINreached the server, theROLLBACKcloses the leaked transaction before the connection is reused; if it never left the client buffer, both statements flush together and the pair is a no-op (plus a harmless warning). The depth counter is left untouched: the cancelled level was never recorded, so unlikestart_rollbackthere is nothing to decrement.Reproduction (deterministic, real Postgres)
Before this patch, 500ms after the drop the backend is still
idle in transaction, andclock_timestamp() - xact_startmeasured from the next checkout shows it running inside the leaked transaction; a subsequentbegin()on the same connection logsWARNING: there is already a transaction in progress. With the patch the backend returns toidleand the next checkout is clean.Related observation (not addressed here)
The
depth > 0path has an adjacent issue: a cancelled savepoint-begin at depth 1 issues a fullROLLBACK, killing the outer transaction, after whichcommit()on the outerTransactionsilently no-ops (depth is 0) and returnsOk— the caller believes the outer transaction committed. Left out of scope to keep this change minimal; happy to file separately.