From c6516844b6a7f37c656681f63ec8416433d56f0f Mon Sep 17 00:00:00 2001 From: Mats Kindahl Date: Thu, 30 Apr 2026 07:06:34 +0200 Subject: pg_rewind: test rewind across UUID-mismatched TLI Add a test for the case where the target has gone through three timelines (TLI1 -> TLI2 -> TLI3) while the source independently promoted from TLI1 to a numerically identical but UUID-distinct TLI2 (call it TLI2'). Without UUID detection, findCommonAncestorTimeline accepts TLI2 as the common ancestor and begins its WAL scan from the TLI2 shutdown checkpoint. That scan misses the 'x' INSERT that happened earlier in TLI2, so the source page is never copied and 'x' survives the rewind. With the UUID fix in the previous commit the algorithm detects the TLI2 / TLI2' mismatch, backs up to TLI1 as the true common ancestor, and starts the WAL scan from the last TLI1 checkpoint. The scan covers the 'x' INSERT, the source page (containing 'b') is copied, and the rewound cluster ends up with only 'b' and 'origin' as expected. The target has deliberately no insert on TLI3 to ensure the test actually exercises the UUID-based ancestor search rather than passing by coincidence (as it would if a TLI3 insert put the page into the scan range even on unpatched code). --- src/bin/pg_rewind/t/005_same_timeline.pl | 121 +++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/src/bin/pg_rewind/t/005_same_timeline.pl b/src/bin/pg_rewind/t/005_same_timeline.pl index 539d05f57a1..7d49f60ebba 100644 --- a/src/bin/pg_rewind/t/005_same_timeline.pl +++ b/src/bin/pg_rewind/t/005_same_timeline.pl @@ -108,4 +108,125 @@ $node_a->teardown_node; $node_b->teardown_node; $node_origin->teardown_node; +# Test that pg_rewind correctly rewinds across a TLI mismatch buried in a shared +# prefix of the timeline history. The target has gone through three timelines +# (TLI 1 -> TLI 2 -> TLI 3) while the source independently promoted from TLI 1 +# to what is numerically TLI 2 but with a different UUID (TLI 2'). The deepest +# common ancestor is therefore TLI 1, and pg_rewind must rewind the target all +# the way back to the end of TLI 1. +# +# origin (TLI 1) --+-- node_x --promote--> TLI 2 -- node_a --promote--> TLI 3 +# | (target: TLI 1->TLI 2->TLI 3) +# +-- node_b --promote--> TLI 2' +# (source: TLI 1->TLI 2') +# +# findCommonAncestorTimeline walks forward: TLI 1 entries match (UUID=0 on +# both sides), then TLI 2 vs TLI 2' match on tli and begin but differ on +# UUID, signalling independent promotions. The algorithm therefore backs up +# to TLI 1 as the common ancestor and sets the divergence point to the end +# of TLI 1. + +my $node_origin2 = PostgreSQL::Test::Cluster->new('origin2'); +$node_origin2->init(allows_streaming => 1); +$node_origin2->append_conf('postgresql.conf', "wal_keep_size = 320MB\n"); +$node_origin2->start; + +$node_origin2->safe_psql('postgres', "CREATE TABLE tbl (val text)"); +$node_origin2->safe_psql('postgres', "INSERT INTO tbl VALUES ('origin')"); +$node_origin2->safe_psql('postgres', 'CHECKPOINT'); + +# node_x and node_b both start from the same TLI 1 baseline. +my $node_x = PostgreSQL::Test::Cluster->new('node_x'); +$node_origin2->backup('backup_x'); +$node_x->init_from_backup($node_origin2, 'backup_x', has_streaming => 1); +$node_x->set_standby_mode(); +$node_x->start; + +my $node_b2 = PostgreSQL::Test::Cluster->new('node_b2'); +$node_origin2->backup('backup_b2'); +$node_b2->init_from_backup($node_origin2, 'backup_b2', has_streaming => 1); +$node_b2->set_standby_mode(); +$node_b2->start; + +# Both standbys must be caught up to the same LSN before origin stops, so +# that TLI 2 and TLI 2' both begin at the same WAL position. +$node_origin2->wait_for_catchup($node_x); +$node_origin2->wait_for_catchup($node_b2); +$node_origin2->stop; + +# Promote node_x to TLI 2 (UUID-X) and insert a row. node_b2 is still on +# TLI 1 and has not yet seen any TLI 2 WAL. +$node_x->promote; +$node_x->safe_psql('postgres', "INSERT INTO tbl VALUES ('x')"); + +# Build node_a2 as a standby of node_x, then promote it to TLI 3. +my $node_a2 = PostgreSQL::Test::Cluster->new('node_a2'); +$node_x->backup('backup_a2'); +$node_a2->init_from_backup($node_x, 'backup_a2', has_streaming => 1); +$node_a2->set_standby_mode(); +$node_a2->start; + +$node_x->wait_for_catchup($node_a2); +$node_x->stop; + +$node_a2->promote; + +# Now promote node_b2 independently from TLI 1 to TLI 2' (UUID-B, != UUID-X). +$node_b2->promote; +$node_b2->safe_psql('postgres', "INSERT INTO tbl VALUES ('b')"); + +# Rewind node_a2 (TLI 1->TLI 2->TLI 3) from node_b2 (TLI 1->TLI 2') in +# local mode. The rewind must reach back to the end of TLI 1. +# +# node_a2 was initialised from a streaming backup of node_x taken after +# node_x had already completed segment 4 of TLI 2; that segment therefore +# does not appear in node_a2's pg_wal. pg_rewind's backward scan for the +# last checkpoint before the divergence point needs that segment, so we +# point restore_command at node_x's pg_wal and use --restore-target-wal. +# +# Note: no row is inserted on TLI 3. This is intentional: the only +# post-divergence table modification in the target's WAL is the 'x' INSERT +# on TLI 2. On unpatched code the WAL scan would start from the TLI 2 +# shutdown checkpoint (just before TLI 3), miss that earlier insert, and +# leave 'x' in place instead of replacing it with 'b'. +my $node_x_waldir = $node_x->data_dir . "/pg_wal"; +$node_a2->append_conf('postgresql.conf', + "restore_command = 'cp \"$node_x_waldir/%f\" \"%p\"'\n"); + +$node_a2->stop; +$node_b2->stop; + +my $node_a2_pgdata = $node_a2->data_dir; +my $tmp_folder2 = PostgreSQL::Test::Utils::tempdir; +copy("$node_a2_pgdata/postgresql.conf", + "$tmp_folder2/node_a2-postgresql.conf.tmp"); + +command_ok( + [ + 'pg_rewind', + '--debug', + '--source-pgdata' => $node_b2->data_dir, + '--target-pgdata' => $node_a2_pgdata, + '--no-sync', + '--restore-target-wal', + '--config-file' => "$tmp_folder2/node_a2-postgresql.conf.tmp", + ], + 'pg_rewind rewinds across mismatched TLI 2 / TLI 2-prime to TLI 1'); + +move("$tmp_folder2/node_a2-postgresql.conf.tmp", + "$node_a2_pgdata/postgresql.conf"); + +# node_a2 should now mirror node_b2: rows from TLI 2 and TLI 3 are gone, +# replaced by node_b2's TLI 2' row. +$node_a2->start; +my $result2 = + $node_a2->safe_psql('postgres', "SELECT val FROM tbl ORDER BY val"); +is($result2, "b\norigin", + 'rewound node reflects source history, not target TLI 2/TLI 3 data'); + +$node_a2->teardown_node; +$node_b2->teardown_node; +$node_x->teardown_node; +$node_origin2->teardown_node; + done_testing(); -- 2.43.0