PostgreSQL: force wire-level BEGIN when starting a transaction - #1221
Conversation
pgjdbc's setAutoCommit(false) only flips a client-side flag - the actual
BEGIN is deferred and piggybacked onto the next statement sent
(QueryExecutorImpl#sendQueryPreamble). Rails 7.1+ lazy transactions assume
begin_db_transaction opens a server-side transaction once materialized,
so materialize_transactions was a wire-level no-op under AR-JDBC and the
server stayed IDLE.
Flush the deferred BEGIN by issuing a no-op SELECT 1 right after
setAutoCommit(false); pgjdbc prepends its BEGIN to it in the same
round-trip, matching the cost of the pg gem's exec("BEGIN").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
headius
left a comment
There was a problem hiding this comment.
Looks simple enough and good to note that it has no more overhead than the equivalent exec("BEGIN") (I immediately had a concern about the additional round-trip).
Approved by me but would be good to get input from the other active PG contributors @skunkworker @JesseChavez.
|
A note on the failing Rails-suite checks — they are inherited from The
On this PR, the PS=true job actually completes the full suite for the first time in a while — 9481 runs, 43 failures, 74 errors (log). Those break down as:
The PS=false job died ~2 minutes in from connection-pool exhaustion, the same long-standing issue as the The |
|
Thank you @ryudoawaru! I will talk with other contributors and see about getting some releases out. Do you have other fixes coming? |
|
Thanks for merging! Two things I'm looking at next: forward-porting this fix to master, and #1212 — I've tracked down the root cause with a reproduction and posted the analysis there. Let me know if something else has priority for the releases. |
Problem
On PostgreSQL,
begin_db_transactionends up callingConnection#setAutoCommit(false)— but in pgjdbc that only flips a client-side flag. The actualBEGINis deferred and piggybacked onto the next statement sent (see pgjdbc'sQueryExecutorImpl#sendQueryPreamble).Rails 7.1+ transactions are lazy on their side too:
materialize_transactionscallsbegin_db_transactionand expects a transaction to actually be open on the server afterwards. Under AR-JDBC that materialization was a wire-level no-op — the server session staysIDLEuntil the next query, so anything that inspects the raw transaction state right after materialization (e.g. Rails' ownadapter_test.rbviaraw_transaction_open?) sees no transaction.Fix
After
setAutoCommit(false)(and setting isolation, when given), issue a no-opSELECT 1. pgjdbc prepends its deferredBEGINto it in the same round-trip, so the cost matches the pg gem'sexec("BEGIN")— one round-trip, just moved from the first in-transaction query tobegin_db_transactionwhere AR expects it.Includes a test that asserts the server-side transaction state is
OPENright afterbegin_db_transaction(it reportsIDLEwithout the fix).Verification
Environment: JRuby 9.4.14.0, PostgreSQL 18.4, pgjdbc 42.7.11, macOS.
rake test_postgresql: 306 tests, 0 failures, 0 errors (includes the new test; fails without the patch)rails:test_postgresql, AR7-2-stable), on top of72-stable+ Expose PG-compatible status/transaction_status/async_exec on the JDBC connection (+ CI Postgres 14) #1220:test/cases/adapter_test.rb: 69 runs, 0 failures, 0 errors — the 4 remaining failures there (assert raw_transaction_open?right aftermaterialize_transactions, lines 533/543/553/577) are exactly what this patch fixesPREPARED_STATEMENTS=false: 67 runs, 0 failures, 0 errorstest/cases/transactions_test.rb: 105 runs, 0 failures, 0 errorsRelated: #1173, #1218, #1220.
🤖 Generated with Claude Code