Greetings,

In recovery/t/051_effective_wal_level.pl, standby3, standby4, and standby5
are all init_from_backup()'d from 'my_backup', taken near the top of the
test, and started much later, after the primary has produced and recycled a
lot of WAL.  Nothing keeps the WAL they need to reach consistency: no slot
covering it, no wal_keep_size, no archive.  (standby5 has phys_slot, but a
physical slot only pins WAL from its own creation, not the older WAL needed
to replay from 'my_backup'.)  So each one starts only because the primary
happens not to have recycled that WAL yet.

That holds with the default WAL page layout but is fragile: anything that
consumes WAL address space a bit faster can push the needed segment out of
the retention window, and the standby then fails to start with "requested
WAL segment ... has already been removed".  These standbys are scaffolding
for the promotion, logical-decoding, and slot-synchronization tests, not a
test of WAL recycling.

The fix takes a fresh backup immediately before each of them, so the start
point is recent and within retained WAL regardless of what the earlier part
of the test produced.

It depends on retention timing, so there's no on-demand reproduction; the
change is justified by the structure (late standbys restoring from an early,
unpinned backup).  051 passes with injection points enabled, so all three
standby blocks run.

--
Bryan Green
EDB: https://www.enterprisedb.com
From a15cc5d2eeca750a0278673f77b342c2dba1f380 Mon Sep 17 00:00:00 2001
From: Bryan Green <[email protected]>
Date: Sat, 8 Aug 2026 15:57:52 -0500
Subject: [PATCH] Harden recovery/t/051_effective_wal_level against WAL
 recycling

standby3, standby4, and standby5 are initialized from 'my_backup', a
backup taken much earlier, and started after the primary has produced and
recycled a substantial amount of WAL.  Nothing keeps the WAL these
standbys need to reach a consistent state: there is no replication slot
covering it, no wal_keep_size, and no archive.  (standby5's phys_slot only
pins WAL from its own creation, not the older WAL needed to replay from
'my_backup'.)  So whether they can start depends on the primary not having
recycled that WAL yet.

This happens to hold with the default WAL page layout but is fragile: a
configuration that consumes WAL address space slightly faster can push the
needed segment out of the primary's retention window, leaving the standby
unable to start with "requested WAL segment ... has already been removed".
These standbys are scaffolding for the promotion, logical-decoding, and
slot-synchronization tests, not a test of WAL recycling.

Take a fresh backup immediately before each of these standbys so the start
point is recent and within the primary's retained WAL.

Co-authored-by: Mark Dilger <[email protected]>
---
 .../recovery/t/051_effective_wal_level.pl     | 20 ++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/src/test/recovery/t/051_effective_wal_level.pl 
b/src/test/recovery/t/051_effective_wal_level.pl
index 11bf4cb613..6f2f828104 100644
--- a/src/test/recovery/t/051_effective_wal_level.pl
+++ b/src/test/recovery/t/051_effective_wal_level.pl
@@ -242,9 +242,14 @@ test_wal_level($cascade, "replica|logical",
 $standby2->stop;
 $cascade->stop;
 
-# Initialize standby3 node and start it.
+# Initialize standby3 node and start it.  Take a fresh backup rather than
+# reusing the older 'my_backup': by now the primary has produced and recycled a
+# fair amount of WAL, and nothing here (no slot, no wal_keep_size, no archive)
+# keeps the WAL this standby needs to reach consistency, so reusing the old
+# backup is exposed to WAL recycling.
 my $standby3 = PostgreSQL::Test::Cluster->new('standby3');
-$standby3->init_from_backup($primary, 'my_backup', has_streaming => 1);
+$primary->backup('my_backup_standby3');
+$standby3->init_from_backup($primary, 'my_backup_standby3', has_streaming => 
1);
 $standby3->start;
 
 # Create logical slots on both nodes.
@@ -332,9 +337,11 @@ if (   $ENV{enable_injection_points} eq 'yes'
        # Test the race condition at end of the recovery between the startup 
and logical
        # decoding status change. This test requires injection points enabled.
 
-       # Initialize standby4 and start it.
+       # Initialize standby4 and start it.  Use a fresh backup, as for 
standby3,
+       # so the start point is within the primary's retained WAL.
        my $standby4 = PostgreSQL::Test::Cluster->new('standby4');
-       $standby4->init_from_backup($primary, 'my_backup', has_streaming => 1);
+       $primary->backup('my_backup_standby4');
+       $standby4->init_from_backup($primary, 'my_backup_standby4', 
has_streaming => 1);
        $standby4->start;
 
        # Both servers have one logical slot.
@@ -537,7 +544,10 @@ select pg_reload_conf();
        $primary->safe_psql('postgres',
                qq[select pg_create_physical_replication_slot('phys_slot')]);
        my $standby5 = PostgreSQL::Test::Cluster->new('standby5');
-       $standby5->init_from_backup($primary, 'my_backup', has_streaming => 1);
+       # Use a fresh backup, as for standby3/standby4: phys_slot only pins WAL 
from
+       # its creation onward, not the older WAL needed to replay from 
'my_backup'.
+       $primary->backup('my_backup_standby5');
+       $standby5->init_from_backup($primary, 'my_backup_standby5', 
has_streaming => 1);
        my $connstr = $primary->connstr;
        $standby5->append_conf(
                'postgresql.conf', qq[
-- 
2.49.0

Reply via email to