# Copyright (c) 2021-2025, PostgreSQL Global Development Group # Test replication statistics data in pg_stat_replication_slots is sane after # drop replication slot and restart. use strict; use warnings FATAL => 'all'; use File::Path qw(rmtree); use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; # Test set-up my $node = PostgreSQL::Test::Cluster->new('test'); $node->init(allows_streaming => 'logical'); $node->append_conf('postgresql.conf', 'synchronous_commit = on'); $node->start; # Check that replication slot stats are expected. sub test_slot_stats { local $Test::Builder::Level = $Test::Builder::Level + 1; my ($node, $expected, $msg) = @_; my $result = $node->safe_psql( 'postgres', qq[ SELECT slot_name, total_txns > 0 AS total_txn, total_bytes > 0 AS total_bytes FROM pg_stat_replication_slots ORDER BY slot_name]); is($result, $expected, $msg); } # Create table. $node->safe_psql('postgres', "CREATE TABLE test_repl_stat(col1 int)"); # Create replication slots. $node->safe_psql( 'postgres', qq[ SELECT pg_create_logical_replication_slot('regression_slot1', 'test_decoding'); SELECT pg_create_logical_replication_slot('regression_slot2', 'test_decoding'); SELECT pg_create_logical_replication_slot('regression_slot3', 'test_decoding'); SELECT pg_create_logical_replication_slot('regression_slot4', 'test_decoding'); ]); # Insert some data. $node->safe_psql('postgres', "INSERT INTO test_repl_stat values(generate_series(1, 5));"); $node->safe_psql( 'postgres', qq[ SELECT data FROM pg_logical_slot_get_changes('regression_slot1', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); SELECT data FROM pg_logical_slot_get_changes('regression_slot2', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); SELECT data FROM pg_logical_slot_get_changes('regression_slot3', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); SELECT data FROM pg_logical_slot_get_changes('regression_slot4', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); ]); # Wait for the statistics to be updated. $node->poll_query_until( 'postgres', qq[ SELECT count(slot_name) >= 4 FROM pg_stat_replication_slots WHERE slot_name ~ 'regression_slot' AND total_txns > 0 AND total_bytes > 0; ]) or die "Timed out while waiting for statistics to be updated"; # Test to drop one of the replication slot and verify replication statistics data is # fine after restart. $node->safe_psql('postgres', "SELECT pg_drop_replication_slot('regression_slot4')"); $node->stop; $node->start; # Verify statistics data present in pg_stat_replication_slots are sane after # restart. test_slot_stats( $node, qq(regression_slot1|t|t regression_slot2|t|t regression_slot3|t|t), 'check replication statistics are updated'); # Test to remove one of the replication slots and adjust # max_replication_slots accordingly to the number of slots. This leads # to a mismatch between the number of slots present in the stats file and the # number of stats present in shared memory. We verify # replication statistics data is fine after restart. $node->stop; my $datadir = $node->data_dir; my $slot3_replslotdir = "$datadir/pg_replslot/regression_slot3"; rmtree($slot3_replslotdir); $node->append_conf('postgresql.conf', 'max_replication_slots = 2'); $node->start; # Verify statistics data present in pg_stat_replication_slots are sane after # restart. test_slot_stats( $node, qq(regression_slot1|t|t regression_slot2|t|t), 'check replication statistics after removing the slot file'); # cleanup $node->safe_psql('postgres', "DROP TABLE test_repl_stat"); $node->safe_psql('postgres', "SELECT pg_drop_replication_slot('regression_slot1')"); $node->safe_psql('postgres', "SELECT pg_drop_replication_slot('regression_slot2')"); # shutdown $node->stop; # Test replication slot stats persistence in a single session. The slot # is dropped and created concurrently of a session peeking at its data # repeatedly, hence holding in its local cache a reference to the stats. $node->start; my $slot_name_restart = 'regression_slot5'; $node->safe_psql('postgres', "SELECT pg_create_logical_replication_slot('$slot_name_restart', 'test_decoding');" ); # Look at slot data, with a persistent connection. my $bpgsql = $node->background_psql('postgres', on_error_stop => 1); # Launch query and look at slot data, incrementing the refcount of the # stats entry. $bpgsql->query_safe( "SELECT pg_logical_slot_peek_binary_changes('$slot_name_restart', NULL, NULL)" ); # Drop the slot entry. The stats entry is not dropped yet as the previous # session still holds a reference to it. $node->safe_psql('postgres', "SELECT pg_drop_replication_slot('$slot_name_restart')"); # Create again the same slot. The stats entry is reinitialized, not marked # as dropped anymore. $node->safe_psql('postgres', "SELECT pg_create_logical_replication_slot('$slot_name_restart', 'test_decoding');" ); # Look again at the slot data. The local stats reference should be refreshed # to the reinitialized entry. $bpgsql->query_safe( "SELECT pg_logical_slot_peek_binary_changes('$slot_name_restart', NULL, NULL)" ); # Drop again the slot, the entry is not dropped yet as the previous session # still has a refcount on it. $node->safe_psql('postgres', "SELECT pg_drop_replication_slot('$slot_name_restart')"); # Shutdown the node, which should happen cleanly with the stats file written # to disk. Note that the background session created previously needs to be # hold *while* the node is shutting down to check that it drops the stats # entry of the slot before writing the stats file. $node->stop; # Make sure that the node is correctly shut down. Checking the control file # is not enough, as the node may detect that something is incorrect after the # control file has been updated and the shutdown checkpoint is finished, so # also check that the stats file has been written out. command_like( [ 'pg_controldata', $node->data_dir ], qr/Database cluster state:\s+shut down\n/, 'node shut down ok'); my $stats_file = "$datadir/pg_stat/pgstat.stat"; ok(-f "$stats_file", "stats file must exist after shutdown"); $bpgsql->quit; done_testing();