Hi Fujii,
> I think it's better to backpatch this to v19, since commit 67c20979ce7
> introduced the unnecessary barrier wait. Thoughts?
+1, and thanks for the comment fixes. Only the comments changed from
0003, and they read right to me. I ran v3 on REL_19_STABLE: it applies
cleanly there and on master, and make check and the recovery suite pass.
Unpatched 19 hung 12/12 on promotion with one backend stuck writing to a
client; with v3 it promoted 12/12 in 10-27 ms. Unpatched 18 promoted
12/12 in the same time, so 19 is the only branch that needs it.
v3 only covers promotion without logical slots. Shihao's 0001, which
came with his report, is the general fix for when the wait is real.
Since 0001 and his test will need backpatching too, I ran them on 15
through master. Shihao, the full numbers are in the attachment; in
short:
- 060 is now taken on every branch (060_multixact_crashed_updater.pl
went in today), so the test needs another number. 061 is free
everywhere. Renumbered and otherwise unchanged, it passes with 0001
on 17 through master, recovery suite included.
- On 15 and 16 it dies before testing anything: psql there runs
FETCH_COUNT through a cursor, so the backend never blocks. psql()
there also returns 0 on timeout, so the DROP DATABASE check can pass
while DROP DATABASE is still stuck.
- Once v3 is in, promotion with no slot doesn't wait at all, so the
promotion subtest passes without 0001.
The attached diff is a suggestion for all three: a COPY TO STDOUT writer,
a timed_out check, and a logical slot on the standby (19 and master
only). With it the test fails without 0001 and passes with it on 15
through master. The COPY writer also helps on its own: on unpatched
master the promotion subtest caught the hang 5/5 with it, against 1/5
as it stands. It's your test, so take whatever is useful.
Hayato, I could reproduce what you found: with your change, DROP DATABASE
stays blocked behind a backend in COPY FROM STDIN, 6/6, with or without
v3. So it's in 0001's absorb path, and v3 neither causes nor fixes it.
It's latent today: every barrier callback, in 19 and master, returns
true or throws ERROR, and none of about 1800 calls over the recovery
suite returned false. If 0001 wants to guard against it, setting the
latch again when ProcSignalBarrierPending is still set (second attached
diff) cleared your reproducer 3/3. A plain Assert there could fire
spuriously, since a new barrier can arrive right after the call.
Regards,
Manu
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.
--- a/src/test/recovery/t/061_barrier_blocked_client.pl
+++ b/src/test/recovery/t/061_barrier_blocked_client.pl
@@ -23,17 +23,25 @@
$standby->init_from_backup($primary, 'backup', has_streaming => 1);
$standby->start;
-# A client that stops reading. psql fetches the result in chunks and blocks
-# writing to a pipe that we never read, so the backend fills its socket
-# buffer and blocks in ClientWrite.
+# With a logical slot on the standby, promotion keeps logical decoding enabled
+# and waits for every process to absorb a barrier. Without one it doesn't
+# wait at all.
+$primary->safe_psql('postgres',
+ "SELECT pg_create_logical_replication_slot('primary_slot', 'test_decoding')");
+$primary->wait_for_replay_catchup($standby);
+$standby->create_logical_slot_on_standby($primary, 'standby_slot', 'postgres');
+
+# A client that stops reading. psql writes the COPY data to a pipe that we
+# never read and blocks, so the backend fills its socket buffer and blocks in
+# ClientWrite. (Not a FETCH_COUNT query: before v17 psql runs that through a
+# cursor, and the backend goes idle between fetches.)
my ($in, $out, $err) = ('', '', '');
my $writer = IPC::Run::start(
[
'psql', '--no-psqlrc',
- '--set' => 'FETCH_COUNT=100',
'--dbname' => $standby->connstr('postgres'),
'--command' =>
- "SELECT repeat('x', 1000) FROM generate_series(1, 1000000)"
+ "COPY (SELECT repeat('x', 1000) FROM generate_series(1, 1000000)) TO STDOUT"
],
'<' => \$in,
'>' => \$out,
@@ -43,7 +51,6 @@
"SELECT count(*) = 1 FROM pg_stat_activity WHERE wait_event = 'ClientWrite'"
) or die "timed out waiting for a backend to block in ClientWrite";
-# Promotion waits for every process to absorb a barrier.
my $promoted = $standby->safe_psql('postgres',
"SELECT pg_promote(true, $PostgreSQL::Test::Utils::timeout_default)");
is($promoted, 't', 'promotion completes with a backend blocked on a write');
@@ -73,7 +80,9 @@
'postgres', 'DROP DATABASE dropme',
timeout => $PostgreSQL::Test::Utils::timeout_default,
timed_out => \$timed_out);
-is($ret, 0, 'DROP DATABASE completes with a backend blocked on a read');
+# Check timed_out too: before v17, psql() returns 0 when it times out.
+ok(!$timed_out && $ret == 0,
+ 'DROP DATABASE completes with a backend blocked on a read');
$reader->kill_kill;
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -525,6 +525,13 @@
ProcessProcSignalBarrier();
+ /*
+ * A barrier callback that returned false wants to be retried, but our
+ * latch is not set, so make sure we don't go back to sleep on the socket.
+ */
+ if (ProcSignalBarrierPending)
+ SetLatch(MyLatch);
+
ExitOnAnyError = save_ExitOnAnyError;
whereToSendOutput = save_whereToSendOutput;
}