Skip to content

Fix sql.begin() sending BEGIN on an unreserved connection - #1218

Open
v0idpwn wants to merge 2 commits into
porsager:masterfrom
v0idpwn:chore/fix-pipeline-boundary-check
Open

Fix sql.begin() sending BEGIN on an unreserved connection#1218
v0idpwn wants to merge 2 commits into
porsager:masterfrom
v0idpwn:chore/fix-pipeline-boundary-check

Conversation

@v0idpwn

@v0idpwn v0idpwn commented Sep 9, 2026

Copy link
Copy Markdown

sql.begin() could send BEGIN to the server without reserving the
connection.

Depending on pool size, this errored with UNSAFE_TRANSACTION or
TypeError. The transaction stayed open on the server either way, and
the connection went back to the pool, so unrelated queries then ran
inside it.

With max_pipeline: 0 this error happened consistently. With the default
max_pipeline of 100 the same thing happened only when BEGIN was the
query that filled a connection's pipeline, which is why the failure was
intermittent.

The issue is in execute(): after writing the query it returns an &&
chain that states whether the connection can accept more queries, and
one of its terms is sent.length < max_pipeline. The onexecute hook,
which is how sql.begin() reserves the connection, was the last term of
that chain. Whenever the capacity term was false the chain
short-circuited and the hook never ran, even though BEGIN had already
been written. With max_pipeline: 0 that term is false for every query,
so the hook never ran at all.

Move the hook ahead of the capacity terms so it runs whenever the query
was written. Reserving the connection is a consequence of having sent
BEGIN, unrelated to the query limit in the connection.

Add tests for BEGIN at the pipeline boundary and with max_pipeline: 0.

Fixes #1189 #1210

sql.begin() could send BEGIN to the server without reserving the
connection.

Depending on pool size, this errored with UNSAFE_TRANSACTION or
TypeError.  The transaction stayed open on the server either way, and
the connection went back to the pool, so unrelated queries then ran
inside it.

With max_pipeline: 0 this error happened consistently.  With the default
max_pipeline of 100 the same thing happened only when BEGIN was the
query that filled a connection's pipeline, which is why the failure was
intermittent.

The issue is in execute(): after writing the query it returns an &&
chain that states whether the connection can accept more queries, and
one of its terms is sent.length < max_pipeline.  The onexecute hook,
which is how sql.begin() reserves the connection, was the last term of
that chain.  Whenever the capacity term was false the chain
short-circuited and the hook never ran, even though BEGIN had already
been written.  With max_pipeline: 0 that term is false for every query,
so the hook never ran at all.

Move the hook ahead of the capacity terms so it runs whenever the query
was written.  Reserving the connection is a consequence of having sent
BEGIN, unrelated to the query limit in the connection.

Add tests for BEGIN at the pipeline boundary and with max_pipeline: 0.

Fixes porsager#1189
execute() must keep returning a falsy value for BEGIN so the pool puts
the connection in the full queue rather than busy.  Otherwise a query
issued while BEGIN is in flight is pipelined behind it and runs inside
the transaction.
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.

BEGIN can reach PostgreSQL without reserving the transaction connection

1 participant