Repository navigation
Conversation
PgConnection::run sent Bind, Execute, Close of the unnamed portal, and Sync for every statement with arguments. Send Bind, Execute, and Sync, as libpq does. The Close came with transact-rs#2081 for transact-rs#2077, when fetch_one executed the portal with a row limit and left it suspended. Since transact-rs#3673 every Execute has no row limit, so the portal runs to completion, and the next Bind or the end of the transaction destroys it. YugabyteDB treats an Execute that another message follows before the Sync as part of a batch, and does not retry a conflicting statement of a batch once its transaction has written. With the Close, such conflicts reached the application as SQLSTATE 40001. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Does your PR solve an issue?
No issue is open for this. It follows up #2081, the fix for #2077, whose Close this change removes now that #3673 makes every Execute run to completion.
Is this a breaking change?
No. The client sends the same statements and reads the same results. The only change is that the unnamed portal of a completed statement is destroyed by the next Bind or the end of the transaction instead of by an explicit Close, which is what libpq does.
it_runs_statements_after_fetch_one_in_a_transaction_issue_2077checks that #2077 stays fixed.PgConnection::runsends Bind, Execute, Close of the unnamed portal, and Sync for every statementwith arguments. This change drops the Close, so the sequence is Bind, Execute, Sync, which is what
libpq sends for
PQexecParams.Why
YugabyteDB decides whether an Execute is part of a batch by looking at the message that follows it.
With the default
yb_pg_batch_detection_mechanism(detect_by_peeking), an Execute that isfollowed by any message other than Sync counts as batched. In
READ COMMITTED, YugabyteDB normallyreruns a statement that hit a conflict at the query layer, but not a batched statement once the
transaction has already written:
yb_is_retry_possibleinsrc/postgres/src/backend/tcop/postgres.crefuses the retry when
YbIsBatchedExecution() && GetCurrentCommandId(false) > FirstCommandId.Because sqlx puts a Close between Execute and Sync, every statement that runs after a transaction's
first write is treated as batched. A conflict on such a statement reaches the application as
SQLSTATE 40001:
Through libpq, the same transactions get YugabyteDB's retry and see no error.
Why the Close is no longer needed
The Close came with #2081, the fix for #2077. At the time,
fetch_oneandfetch_optionalexecuted the portal with a row limit of 1. The portal stopped after one row (PortalSuspended) and
stayed open, and CockroachDB rejected the next statement in the transaction with "multiple active
portals not supported".
Since #3673, every Execute is sent with a row limit of 0, so the portal always runs to completion
and is never suspended. The PostgreSQL protocol documentation says the unnamed portal "is destroyed
at the end of the transaction, or as soon as the next Bind statement specifying the unnamed portal
as destination is issued", and a simple Query also destroys it. The Close therefore no longer
prevents a suspended portal. It only frees a completed portal one statement earlier.
The cost of this change: the completed unnamed portal of a transaction's last statement now lives
until the next statement or the end of the transaction, as it does with libpq.
Measurement
YugabyteDB 2025.2.5.2, single node,
READ COMMITTED. Six tasks each ran ten transactions. Eachtransaction first updated one shared row and then ran the step-completion statements of a
durable-workflow library, which lock a row that a concurrent lease renewal also writes. We ran two
copies at once, three runs each, and counted the 40001 errors the library caught and retried:
The other contention cases in the same runs (claims, renewals, and the same completions without
the earlier write) behaved the same with and without the Close. An earlier run on the same setup
gave 3, 11, and 11 conflicts with the Close and 0 without it. Through libpq (
ysql_bench, sixclients), transactions of the same kind, a write followed by a locking read that waits for another
writer, met no serialization failure.
Test
it_runs_statements_after_fetch_one_in_a_transaction_issue_2077covers #2077's case. In onetransaction it reads one row of a 50-row result with
fetch_onethree times, then creates atemporary table, inserts into it, reads one row of it with
fetch_one, counts the rows, andcommits. It passes on PostgreSQL and on YugabyteDB.
The test does not assert that no Close is sent. Asserting the message sequence needs a proxy that
reads the protocol, and the client-certificate CI job only accepts TLS connections, which such a
proxy cannot read.
馃 Generated with Claude Code