Measurements for v3 ("Avoid unnecessary end-of-recovery barrier waits"), Hayato's point, and Shihao's test on the back branches ========================================================================================== 2026-10-15. Builds: --enable-cassert --enable-debug --enable-tap-tests (+ injection points where available), CFLAGS=-O1. Clusters on tmpfs, fsync=off: timings compare variants only. Branch tips: master 8f2b598b4bc, REL_19_STABLE 0fb192f1b02 (sections 1-2; differs from the tip 86ce68d9e6f only in nbtsearch.c and analyzejoins.c) and 86ce68d9e6f (section 3), REL_18_STABLE 10b2d9986b9, REL_17_STABLE 96823bfd7c9, REL_16_STABLE 7792528fd69, REL_15_STABLE 72053f183e0, REL_14_STABLE 46ef0bdd0b1. "0001" = Shihao's v2-0001, "0002" = Shihao's v2-0002 test. 1. v3 and the backpatch to v19 ------------------------------ - v3 vs 0003: identical code once comments are stripped; only the comments changed. The new comments match the code, and the DisableLogicalDecoding() reference holds: it emits the same barrier without waiting, for the same reason. - git apply --check (no fuzz): clean on REL_19_STABLE (0fb192f1b02 and 86ce68d9e6f) and master. - REL_19_STABLE + v3: make check, all 239 tests pass; src/test/recovery 58 files, 793 tests, PASS (051_effective_wal_level included). Promotion with one standby backend blocked in ClientWrite (client stopped reading), fresh primary + standby per run, pg_promote(false). HUNG = not promoted after 10 s; each hung run was then released by pg_terminate_backend() of that backend in 12-16 ms. branch variant wal_level / case result promote ms min/med/max 19 unpatched replica, no slots HUNG 12/12 - 19 v3 replica, no slots PROMOTED 12/12 10/25/27 18 unpatched replica, no slots PROMOTED 12/12 10/25/27 19 unpatched logical HUNG 4/4 - 19 v3 logical PROMOTED 4/4 11/18/26 19 v3 backend stuck sending PROMOTED 4/4 11/18/26 an ERROR message 19 unpatched idle backend PROMOTED 6/6 11/20/28 19 v3 idle backend PROMOTED 6/6 11/12/27 19 v3 standby logical slot HUNG 3/3 - (wait is needed there) 19 0001+v3 standby logical slot PROMOTED 3/3 10/11/11 The 10 vs 25 ms split comes from the 10 ms polling plus psql round trips; it shows on 18 too. 2. Hayato's point (a barrier callback returning false) ------------------------------------------------------ His change, verbatim: ProcessBarrierSmgrRelease() returns false once under ExitOnAnyError, i.e. on its first call from 0001's ProcessClientBarrierInterrupt(). (a) Real with that change. 0002, 30 s timeout: 0001 PASS 2/2 0001 + his change FAIL 2/2 (DROP DATABASE subtest; "still waiting for backend with PID ... to accept ProcSignalBarrier", the waited PID being the COPY FROM STDIN backend) 0001 + his change + v3 FAIL 2/2 (same subtest) DROP DATABASE against a backend in COPY FROM STDIN, 10 s limit: 0001 DONE 4/4 (15 ms); + his change BLOCKED 6/6; + his change + v3 BLOCKED 6/6; unpatched BLOCKED 2/2. (b) Independent of v3, which neither causes nor fixes it. After a false return ProcSignalBarrierPending stays set (ResetProcSignalBarrierBits()) but the latch does not, so secure_read() goes back to WaitEventSetWait() on the socket. The same trick in ProcessBarrierUpdateXLogLogicalInfo() (the barrier promotion emits), backend blocked in ClientWrite, wal_level=replica: 0001 PROMOTED 4/4 0001 + trick HUNG 6/6 0001 + trick + v3 PROMOTED 6/6 (no wait when turning it off) 0001 + trick + v3, standby slot HUNG 4/4 (c) Latent: no callback returns false today. Code: REL_19_STABLE has ProcessBarrierSmgrRelease() and ProcessBarrierUpdateXLogLogicalInfo(), whose only exit is "return true". master adds AbsorbDataChecksumsBarrier(): every return is "return true", the other exits are ERRORs (FATAL under 0001's ExitOnAnyError). Run: 0001 + v3 + a write_stderr() after every callback call, whole src/test/recovery suite plus 0002: 535 SMGRRELEASE and 1257 UPDATE_XLOG_LOGICAL_INFO calls, none returned false. (A first probe used elog(LOG); errfinish() runs CHECK_FOR_INTERRUPTS(), re-entered the absorb loop and broke 051. That was the probe: unpatched, v3, 0001 and 0001+v3 pass 051 3/3 each, and so does the elog-free probe.) (d) Two possible changes to ProcessClientBarrierInterrupt(), after ProcessProcSignalBarrier(): - a comment recording the invariant (no behaviour change); - "if (ProcSignalBarrierPending) SetLatch(MyLatch);". With his change on top: 0002 PASS 3/3, DROP DATABASE DONE 3/3, and the logical-info trick PROMOTED 3/3 (without the SetLatch: FAIL / BLOCKED / HUNG). 0001 + SetLatch alone passes 0002 2/2. A plain Assert(!ProcSignalBarrierPending) there could fire spuriously: a new barrier can be signalled right after the call (harmlessly, since the handler sets the latch). 3. 0002 on the back branches ---------------------------- t/060_ is taken on every branch from 14 to master: 060_multixact_crashed_updater.pl was backpatched today. 061 is free on all of them and follows the highest number (060). The meson.build hunk then goes after the 060 line (16 and later; 14 and 15 have no meson.build there). 0002 renumbered to 061 and otherwise unchanged. "alone" = only this test, 30 s timeout: branch applies 0001 applies unpatched 0001 (alone) recovery suite with 0001 + 061 master clean clean FAIL PASS 2/2 PASS (60 files, 816 tests) 19 clean clean FAIL PASS 2/2 PASS (59 files, 795 tests) 18 clean clean FAIL PASS 2/2 PASS (54 files, 647 tests) 17 clean clean FAIL PASS 2/2 PASS (52 files, 625 tests) 16 clean clean dies dies 2/2 only 061 fails (exit 29) 15 clean clean dies dies only 061 fails (exit 29) 14 clean clean dies dies only 061 fails (exit 29) "dies" = the test stops before checking anything (below), so on 14-16 a failure says nothing about the fix. - 15 and 16: the test dies with "timed out waiting for a backend to block in ClientWrite". psql there implements FETCH_COUNT with a cursor (DECLARE _psql_cursor), not chunked rows, so the backend goes idle between fetches and never blocks. - 15 and 16 also: PostgreSQL::Test::Cluster::psql() returns 0 when it times out (there the undef $ret goes through ">> 8"), so on unpatched 16 the DROP DATABASE subtest passed while DROP DATABASE sat for 30 s on "still waiting for backend ...". 17 and later return undef. - 14: no WaitForProcSignalBarrier() caller outside procsignal.c, so nothing to hang on. - On 15 and 14 the stand-alone runs after the first one could not start ("tmp_check/... File exists"; those branches don't use TESTDATADIR). The suites above are clean runs. Possible changes (Shihao's call), measured: (i) stuck writer = psql running COPY (SELECT ...) TO STDOUT instead of the FETCH_COUNT query; psql writes COPY data straight to its stdout, which nobody reads; (ii) the DROP DATABASE check is ok(!$timed_out && $ret == 0, ...); (iii) 19 and master: a logical slot on the standby before promoting. Once v3 is in, promotion with no slot doesn't wait at all, so the promotion subtest no longer exercises 0001: 19 + v3 without 0001 passes it 2/2, with zero "still waiting". branch version unpatched 0001 recovery suite with 0001 + 061 16 (i)+(ii) DROP DATABASE FAIL 2/2 PASS 3/3 PASS (45 files, 538 tests) 15 (i)+(ii) DROP DATABASE FAIL 2/2 PASS 3/3 PASS (43 files, 446 tests) 17 (i)+(ii) DROP DATABASE FAIL 1/1 PASS 2/2 PASS (52 files, 625 tests) 18 (i)+(ii) DROP DATABASE FAIL 1/1 PASS 1/1 PASS (54 files, 647 tests) 19 all three see below PASS (59 files, 796 tests) master all three see below PASS (60 files, 817 tests) Promotion subtest on unpatched master, 5 runs each: catches the hang 1/5 as posted, 5/5 with (i). DROP DATABASE subtest: 5/5 both ways. 19 with all three: v3 without 0001: promotion FAIL 3/3 (the wait is back), DROP DATABASE FAIL 3/3; 0001 + v3: PASS 3/3; unpatched FAIL 1/1; 0001 alone PASS 1/1. master with all three: unpatched promotion and DROP DATABASE FAIL 1/1; 0001 PASS 1/1.