diff --git a/src/test/recovery/Makefile b/src/test/recovery/Makefile index 9c4102b6b2c..dfd3617c84c 100644 --- a/src/test/recovery/Makefile +++ b/src/test/recovery/Makefile @@ -9,7 +9,8 @@ # #------------------------------------------------------------------------- -EXTRA_INSTALL=contrib/pg_prewarm \ +EXTRA_INSTALL=contrib/pageinspect \ + contrib/pg_prewarm \ contrib/pg_stat_statements \ contrib/test_decoding \ src/test/modules/injection_points \ diff --git a/src/test/recovery/t/034_create_database.pl b/src/test/recovery/t/034_create_database.pl index 194d1837e23..bff6c65a32b 100644 --- a/src/test/recovery/t/034_create_database.pl +++ b/src/test/recovery/t/034_create_database.pl @@ -42,4 +42,133 @@ is($result, "0", "check that there are no tables from template on new database after crash" ); +# Check that the VM and FSM contents of relations copied with the +# WAL_LOG strategy are preserved on a standby. +# +# The copy WAL-logs each page via log_newpage_buffer(). VM and FSM +# pages never maintain pd_lower/pd_upper after PageInit(), so logging +# them with page_std=true would omit their whole content area from the +# FPI and replay would zero-fill it. When the promoted standby later +# vacuums the table, replaying the VM-setting PRUNE records on the old +# primary (where the bits are already set) would be a no-op that leaves +# the VM buffer clean, tripping Assert(BufferIsDirty(vmbuffer)). + +# full_page_writes=off guarantees that the VM-setting record generated +# below carries no FPI; the large checkpoint_timeout prevents WAL writes +# while the old primary is demoted, so no rewind is needed then. +my $node_primary = PostgreSQL::Test::Cluster->new('primary'); +$node_primary->init(allows_streaming => 1); +$node_primary->append_conf( + 'postgresql.conf', qq{ +full_page_writes = off +autovacuum = off +checkpoint_timeout = 1h +}); +$node_primary->start; + +# Create a source database with a table whose VM bits are all set +# (all-visible and all-frozen) and whose FSM is filled. +$node_primary->safe_psql('postgres', "CREATE DATABASE src_db"); +$node_primary->safe_psql( + 'src_db', qq{ +CREATE EXTENSION pageinspect; +CREATE TABLE test (a int); +INSERT INTO test SELECT generate_series(1, 10000); +VACUUM FREEZE test; +}); + +# Check that the VM page has real content beyond the 24-byte page +# header, so that the comparison below is meaningful. +my $vm_hex = $node_primary->safe_psql('src_db', + "SELECT encode(get_raw_page('test', 'vm', 0), 'hex')"); +like(substr($vm_hex, 48), + qr/[^0]/, 'VM bits are set on src_db before CREATE DATABASE'); + +# Flush and take a base backup; the backup copies the VM/FSM files +# verbatim. +$node_primary->safe_psql('postgres', "CHECKPOINT"); +my $backup_name = 'my_backup'; +$node_primary->backup($backup_name); +my $node_standby = PostgreSQL::Test::Cluster->new('standby'); +$node_standby->init_from_backup($node_primary, $backup_name, + has_streaming => 1); +$node_standby->start; + +# Create dst_db from src_db: the primary lands the copied forks on +# disk, while the standby replays the FPIs. +$node_primary->safe_psql('postgres', + "CREATE DATABASE dst_db TEMPLATE src_db STRATEGY WAL_LOG"); +$node_primary->wait_for_catchup($node_standby); + +# Compare the VM and FSM pages with pageinspect. +my $p_vm = $node_primary->safe_psql('dst_db', + "SELECT encode(get_raw_page('test', 'vm', 0), 'hex')"); +my $s_vm = $node_standby->safe_psql('dst_db', + "SELECT encode(get_raw_page('test', 'vm', 0), 'hex')"); +is($s_vm, $p_vm, 'new database: VM page identical on both nodes'); + +my $p_fsm = $node_primary->safe_psql('dst_db', + "SELECT encode(get_raw_page('test', 'fsm', 0), 'hex')"); +my $s_fsm = $node_standby->safe_psql('dst_db', + "SELECT encode(get_raw_page('test', 'fsm', 0), 'hex')"); +is($s_fsm, $p_fsm, 'new database: FSM page identical on both nodes'); + +# Switch over and check that the old primary survives replaying the +# PRUNE records generated by vacuuming the table on the new primary. + +# Move the redo point past the CREATE DATABASE records, so that crash +# recovery of the old primary does not replay the CREATE DATABASE FPIs, +# and wait until the standby has replayed the checkpoint. +$node_primary->safe_psql('postgres', "CHECKPOINT"); +$node_primary->wait_for_catchup($node_standby); + +# Promote the standby, then demote the old primary to a standby of the +# new primary: its WAL ends exactly at the promotion fork point, so no +# rewind is needed. The slot preserves the WAL generated below. +$node_standby->promote; + +$node_primary->stop('immediate'); +$node_standby->safe_psql('postgres', + "SELECT pg_create_physical_replication_slot('old_primary')"); +my $new_primary_connstr = $node_standby->connstr; +$node_primary->append_conf( + 'postgresql.conf', qq{ +primary_conninfo = '$new_primary_connstr application_name=old_primary' +primary_slot_name = 'old_primary' +}); +$node_primary->set_standby_mode; +$node_primary->start; + +# Wait until the new standby is fully caught up. +$node_standby->wait_for_catchup($node_primary); + +# Note the log offset now: the crash can follow the VACUUM within +# milliseconds. +my $log_offset = -s $node_primary->logfile; + +# If the VM bitmap is all zero on the new primary, vacuum sets the bits +# again, generating PRUNE records without an FPI for the VM buffer. +$node_standby->safe_psql('dst_db', + "VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) test"); + +# Replay of the PRUNE records on the new standby would then be a no-op +# that leaves the VM buffer clean and crashes on +# Assert(BufferIsDirty(vmbuffer)), in which case the standby never +# replays past the target LSN and this poll times out. The crash +# follows the VACUUM within milliseconds, so a short timeout suffices. +my $target_lsn = $node_standby->lsn('flush'); +my $saved_timeout = $PostgreSQL::Test::Utils::timeout_default; +$PostgreSQL::Test::Utils::timeout_default = 15; +my $caught_up = $node_standby->poll_query_until( + 'postgres', + "SELECT '$target_lsn' <= replay_lsn AND state = 'streaming' + FROM pg_catalog.pg_stat_replication + WHERE application_name = 'old_primary'"); +$PostgreSQL::Test::Utils::timeout_default = $saved_timeout; +ok($caught_up, 'new standby catches up after replaying PRUNE records'); +ok(!$node_primary->log_contains(qr/TRAP:.*BufferIsDirty/, $log_offset), + 'no assertion crash during replay'); + +$node_primary->stop; + done_testing();