diff --git a/src/test/recovery/t/long_fork_reproducer.pl b/src/test/recovery/t/long_fork_reproducer.pl new file mode 100644 index 00000000000..319679a25f7 --- /dev/null +++ b/src/test/recovery/t/long_fork_reproducer.pl @@ -0,0 +1,112 @@ +# Reproduce the Long Fork anomaly between a primary and standby. +# This is a standalone reproducer, not an entry in the recovery test schedule. +# +# Scenario: +# * t0: x=0, y=0 on primary, standby1, and standby2. +# * Pause replay on standby1. +# * tx1 (synchronous_commit=remote_apply): UPDATE x=1; COMMIT. Waits on pause. +# * Standby2 receives tx1's WAL and applies it. We poll until standby2 has +# reached the primary's current flush LSN, then pause standby2. Standby2 +# now holds at (x=1, y=0). +# * tx2 (synchronous_commit=off): UPDATE y=1; COMMIT. Returns immediately. +# tx2's WAL ships to both standbys but neither applies it (both paused). +# * Read primary: a fresh snapshot sees tx1 as running and tx2 as committed, +# returning (x=0, y=1). +# * Read standby2: paused at the post-tx1 LSN, returning (x=1, y=0). +# +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->backup('bk'); + +my $standby1 = PostgreSQL::Test::Cluster->new('standby1'); +$standby1->init_from_backup($primary, 'bk', has_streaming => 1); +$standby1->start; + +my $standby2 = PostgreSQL::Test::Cluster->new('standby2'); +$standby2->init_from_backup($primary, 'bk', has_streaming => 1); +$standby2->start; + +$primary->safe_psql('postgres', + "ALTER SYSTEM SET synchronous_standby_names = 'standby1';"); +$primary->reload; + +$primary->poll_query_until('postgres', q[ + SELECT sync_state = 'sync' + FROM pg_stat_replication + WHERE application_name = 'standby1' +]) or die "standby1 never reached sync_state='sync'"; + +$primary->poll_query_until('postgres', q[ + SELECT count(*) = 1 + FROM pg_stat_replication + WHERE application_name = 'standby2' +]) or die "standby2 never connected"; + +$primary->safe_psql('postgres', q[ + CREATE TABLE t (k text PRIMARY KEY, v int NOT NULL); + INSERT INTO t VALUES ('x', 0), ('y', 0); +]); +$primary->wait_for_catchup($standby1); +$primary->wait_for_catchup($standby2); + +$standby1->safe_psql('postgres', 'SELECT pg_wal_replay_pause();'); + +my $tx1 = $primary->background_psql('postgres'); +$tx1->query_safe(q[ + SET application_name = 'long_fork_t1'; + SET synchronous_commit = 'remote_apply'; +]); +$tx1->query_until(qr/committing/, qq[ +BEGIN; +UPDATE t SET v = 1 WHERE k = 'x'; +\\echo committing +COMMIT; +]); + +$primary->poll_query_until('postgres', q[ + SELECT count(*) = 1 + FROM pg_stat_activity + WHERE application_name = 'long_fork_t1' + AND wait_event_type = 'IPC' + AND wait_event = 'SyncRep' +]) or die "tx1 never reached SyncRep wait"; + +my $lsn = $primary->safe_psql('postgres', + "SELECT pg_current_wal_flush_lsn();"); +chomp $lsn; + +$standby2->poll_query_until('postgres', + "SELECT pg_last_wal_replay_lsn() >= '$lsn'::pg_lsn") + or die "standby2 never applied tx1"; + +$standby2->safe_psql('postgres', 'SELECT pg_wal_replay_pause();'); + +$primary->safe_psql('postgres', q[ + SET synchronous_commit = off; + BEGIN; + UPDATE t SET v = 1 WHERE k = 'y'; + COMMIT; +]); + +my $m = $primary->safe_psql('postgres', + "SELECT string_agg(v::text, ',' ORDER BY k) FROM t;"); +my $s = $standby2->safe_psql('postgres', + "SELECT string_agg(v::text, ',' ORDER BY k) FROM t;"); + +is($m, '0,1', 'primary sees T2 but not T1'); +is($s, '1,0', 'standby sees T1 but not T2'); + +$standby1->safe_psql('postgres', 'SELECT pg_wal_replay_resume();'); +$standby2->safe_psql('postgres', 'SELECT pg_wal_replay_resume();'); +$tx1->quit; +$primary->wait_for_catchup($standby1); +$primary->wait_for_catchup($standby2); + +done_testing();