Hi, Hayato, you are right, thanks. v4-0001 sets the latch again if a barrier is still pending, as Manu suggested. With your change on top, DROP DATABASE waited 60 seconds with v2-0001 and 15 ms with v4-0001.
Manu, I took all of your test changes. On 19 and master the test fails without 0001 and passes with it. I call these v4 so they don't get mixed up with Fujii's v3. Thanks, Shihao
From c3e54aca0e68d3358d5c6df0e9a439f559332f8a Mon Sep 17 00:00:00 2001 From: shihao zhong <[email protected]> Date: Wed, 14 Oct 2026 08:22:27 -0400 Subject: [PATCH v4 2/2] Test barrier absorption with a stuck client Promote a standby while a backend there is blocked writing to a client that stopped reading, and drop a database while a backend is blocked reading COPY data that never comes. --- src/test/recovery/meson.build | 1 + .../recovery/t/061_barrier_blocked_client.pl | 89 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/test/recovery/t/061_barrier_blocked_client.pl diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build index b6ac9dd07a7..05b1552086a 100644 --- a/src/test/recovery/meson.build +++ b/src/test/recovery/meson.build @@ -69,6 +69,7 @@ tests += { 't/058_shutdown_crash_restart.pl', 't/059_remote_apply_status_interval.pl', 't/060_multixact_crashed_updater.pl', + 't/061_barrier_blocked_client.pl', ], }, } diff --git a/src/test/recovery/t/061_barrier_blocked_client.pl b/src/test/recovery/t/061_barrier_blocked_client.pl new file mode 100644 index 00000000000..662424e2faf --- /dev/null +++ b/src/test/recovery/t/061_barrier_blocked_client.pl @@ -0,0 +1,89 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Check that a backend blocked on a client write or read still absorbs +# ProcSignalBarriers, so that a stuck client cannot hold up promotion or +# DROP DATABASE. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $primary = PostgreSQL::Test::Cluster->new('primary'); +$primary->init(allows_streaming => 1); +$primary->start; +$primary->safe_psql('postgres', + 'CREATE TABLE copy_target (a int); CREATE DATABASE dropme;'); +$primary->backup('backup'); + +my $standby = PostgreSQL::Test::Cluster->new('standby'); +$standby->init_from_backup($primary, 'backup', has_streaming => 1); +$standby->start; + +# 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', + '--dbname' => $standby->connstr('postgres'), + '--command' => + "COPY (SELECT repeat('x', 1000) FROM generate_series(1, 1000000)) TO STDOUT" + ], + '<' => \$in, + '>' => \$out, + '2>' => \$err); + +$standby->poll_query_until('postgres', + "SELECT count(*) = 1 FROM pg_stat_activity WHERE wait_event = 'ClientWrite'" +) or die "timed out waiting for a backend to block in ClientWrite"; + +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'); + +$writer->kill_kill; + +# A client that stops sending. COPY FROM STDIN waits for data on psql's +# stdin, which we never write to, so the backend blocks in ClientRead. +my ($in2, $out2, $err2) = ('', '', ''); +my $reader = IPC::Run::start( + [ + 'psql', '--no-psqlrc', + '--dbname' => $primary->connstr('postgres'), + '--command' => 'COPY copy_target FROM STDIN' + ], + '<' => \$in2, + '>' => \$out2, + '2>' => \$err2); + +$primary->poll_query_until('postgres', + "SELECT count(*) = 1 FROM pg_stat_activity WHERE wait_event = 'ClientRead' AND state = 'active'" +) or die "timed out waiting for a backend to block in ClientRead"; + +# DROP DATABASE waits for every process to absorb a barrier. +my $timed_out = 0; +my $ret = $primary->psql( + 'postgres', 'DROP DATABASE dropme', + timeout => $PostgreSQL::Test::Utils::timeout_default, + timed_out => \$timed_out); +# 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; + +done_testing(); -- 2.37.1 (Apple Git-137.1)
From 564c8478a4095f8527dd3dbacd474f8124299df9 Mon Sep 17 00:00:00 2001 From: shihao zhong <[email protected]> Date: Wed, 14 Oct 2026 08:22:27 -0400 Subject: [PATCH v4 1/2] Absorb ProcSignalBarriers while waiting for the client A backend blocked writing to or reading from a client that stopped responding never absorbed barriers, so promotion, standby replay and DROP DATABASE waited for it without limit. Absorb them in that wait. We may be in the middle of a message, so make any error FATAL. Diagnosed-by: Manuel Reyes Bravo <[email protected]> --- src/backend/tcop/postgres.c | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index b6bdfe213fe..51c04b5480f 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -502,6 +502,43 @@ ReadCommand(StringInfo inBuf) return result; } +/* + * Absorb ProcSignalBarriers while blocked on a client read or write, so that + * a client that stops sending or reading can't hold up the processes waiting + * for them. We might be in the middle of a protocol message, so make any + * error FATAL and don't send it to the client. + */ +static void +ProcessClientBarrierInterrupt(void) +{ + bool save_ExitOnAnyError; + CommandDest save_whereToSendOutput; + + if (!ProcSignalBarrierPending || + InterruptHoldoffCount != 0 || CritSectionCount != 0) + return; + + save_ExitOnAnyError = ExitOnAnyError; + save_whereToSendOutput = whereToSendOutput; + ExitOnAnyError = true; + if (whereToSendOutput == DestRemote) + whereToSendOutput = DestNone; + + ProcessProcSignalBarrier(); + + /* + * A barrier callback may return false to be retried later. The barrier + * then stays pending but our latch is not set, so set it rather than go + * back to sleep on the socket. This can't be an Assert, because a new + * barrier may have arrived since we absorbed the last one. + */ + if (ProcSignalBarrierPending) + SetLatch(MyLatch); + + ExitOnAnyError = save_ExitOnAnyError; + whereToSendOutput = save_whereToSendOutput; +} + /* * ProcessClientReadInterrupt() - Process interrupts specific to client reads * @@ -544,6 +581,8 @@ ProcessClientReadInterrupt(bool blocked) else SetLatch(MyLatch); } + else if (blocked) + ProcessClientBarrierInterrupt(); errno = save_errno; } @@ -596,6 +635,8 @@ ProcessClientWriteInterrupt(bool blocked) else SetLatch(MyLatch); } + else if (blocked) + ProcessClientBarrierInterrupt(); errno = save_errno; } -- 2.37.1 (Apple Git-137.1)
