Edge cases in v3 rich mode (master 061065e28f + v3, --enable-cassert) ===================================================================== Each case is a sequence of simple Query messages on ONE connection, run with the raw client: python3 rfq-wire-client.py script '-c ready_for_query_message=rich' -- 'sql1' 'sql2' ... and the value shown is the key in the 'Z' that closes that statement. No assertion failure and no crash in any of them. 1. 'T' is 0 while a temporary table exists (SET LOCAL) ------------------------------------------------------ BEGIN SET LOCAL ready_for_query_message = plain CREATE TEMP TABLE loc(a int) COMMIT -> T=0 (expected 1) SELECT 1 -> T=0 SELECT count(*) FROM pg_class WHERE relname = 'loc' AND relpersistence = 't' -> 1 PreCommit_Namespace() runs while the setting is still plain, so it does not call CheckSessionTempTables(). The setting goes back to rich during commit, but assign_ready_for_query_message() only rechecks when IsTransactionState() is true, and it is not at that point. 2. 'T' is 0 after the setting becomes rich through a reload ----------------------------------------------------------- Connect WITHOUT the options parameter (the setting comes from the config): CREATE TEMP TABLE rel(a int) ALTER SYSTEM SET ready_for_query_message = rich SELECT pg_reload_conf() SELECT pg_sleep(0.5) SHOW ready_for_query_message -> rich, T=0 (expected 1) SELECT 1 -> T=0 The SIGHUP is processed between commands, outside a transaction, so the assign hook skips the check. A plain SET does recheck, and gets T=1. 3. 'H' stays 1 after a procedure that commits inside a FOR loop --------------------------------------------------------------- CREATE PROCEDURE p_loop() LANGUAGE plpgsql AS $$ DECLARE r record; BEGIN FOR r IN SELECT g FROM generate_series(1, 3) g LOOP COMMIT; END LOOP; END $$ CALL p_loop() -> H=1 (expected 0) SELECT count(*) FROM pg_cursors -> 0, and H=1 SELECT 1 -> H=1, until CLOSE ALL or DISCARD ALL The COMMIT inside the loop goes through HoldPinnedPortals() -> HoldPortal(), which increments active_with_hold_portal_count. That portal does not have CURSOR_OPT_HOLD, so PortalDrop() never decrements it. 4. 'H' is 0 while a WITH HOLD cursor is open -------------------------------------------- DECLARE alive CURSOR WITH HOLD FOR SELECT 42 BEGIN DECLARE bad CURSOR WITH HOLD FOR SELECT 1 / (g - 3) FROM generate_series(1, 5) g COMMIT -> ERROR: division by zero, H=0 (expected 1) SELECT count(*) FROM pg_cursors WHERE name = 'alive' -> 1 SELECT 1 -> H=0 HoldPortal() increments the counter only after PersistHoldablePortal() returns. Here that call fails, but the portal already has a holdStore, so PortalDrop() decrements for it during cleanup: 1 -> 0 with 'alive' still open. 5. 'l' moves without a commit record ------------------------------------ On a cluster with data checksums (the initdb default): CREATE TABLE hb AS SELECT g FROM generate_series(1, 20000) g -> l = 0/019CDE98, equal to the end_lsn of its COMMIT record (pg_walinspect) CHECKPOINT (from another session) SELECT count(*) FROM hb -> l = 0/01A85E10 pg_waldump between those two positions shows 91 FPI_FOR_HINT and 88 PRUNE_ON_ACCESS records, all with tx: 0, written by that read-only SELECT. RecordTransactionCommit() sets XactLastCommitEnd = XactLastRecEnd also for a transaction without an xid that wrote WAL (xact.c, after the !markXidCommitted branch). So 'l' is never lower than the last commit, but it is not always "the last commit LSN" the docs describe. If 'l' is meant for read-your-writes on a standby, the effect is waiting a bit longer, not reading stale data. What does match: right after a commit with an xid, 'l' equals the end_lsn of that COMMIT record; a ROLLBACK and another session's commit leave it alone; and with the WAL position moved above 32 bits (pg_resetwal -l), 'l' decodes to AB/CD01FA50, equal to pg_current_wal_insert_lsn(). 6. Cost of rich mode when every transaction touches a temporary table --------------------------------------------------------------------- pgbench, 8 clients, 10 s per sample, 10 samples per variant, all variants interleaved and rotated, builds without asserts, pinned to one CCX; two independent runs. Workload file (one statement per transaction): CREATE TEMP TABLE IF NOT EXISTS tt(a int); SELECT count(*) FROM tt; v3 plain vs v3 rich, this workload: run 1 -14.6% [95% CI -16.3..-13.3] run 2 -13.0% [95% CI -15.4..-11.5] Each of those commits sets XACT_FLAGS_ACCESSEDTEMPNAMESPACE, so in rich mode PreCommit_Namespace() runs the pg_depend index scan on every commit. Without temporary tables there is no measurable cost: pgbench -S -M prepared, v3 plain vs v3 rich, -0.4% [95% CI -3.1..+1.6] and -1.4% [95% CI -5.0..+2.1]; master vs v3 plain, +0.8% [95% CI -1.3..+3.5] and +0.9% [95% CI -2.6..+4.8]. 7. Smaller observations ----------------------- - Inside an explicit transaction, 'T' does not change until COMMIT: BEGIN; CREATE TEMP TABLE t(a int) -> that 'Z' (status T) still has T=0. - Any temporary object sets 'T', not only tables: CREATE TEMP VIEW -> T=1. - With PGOPTIONS='-c ready_for_query_message=rich', libpq_pipeline fails 7 of 24, all "trace match": the new fe-trace.c output is correct (no "mismatched message length" any more), but the expected trace files have the plain 5-byte ReadyForQuery.