From bc40a1d0d470917e44d06caab52b482e907f2d78 Mon Sep 17 00:00:00 2001 From: Vitaly Davydov Date: Mon, 6 Oct 2025 16:20:45 +0300 Subject: [PATCH 1/2] Newly created replication slot may be invalidated by checkpoint Commit 2090edc6f32f652a2c995ca5f7e65748ae1e4c5d introduced a change that the minimal restart_lsn is obtained at the start of checkpoint creation. If a replication slot is created and performs a WAL reservation concurrently, the WAL segment contains the new slot's restart_lsn could be removed by the ongoing checkpoint. Add a perl test to reproduce this scenario. Author: suyu.cmj Author: Vitaly Davydov --- src/backend/replication/slot.c | 3 + .../recovery/t/049_invalidate_new_slot.pl | 188 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 src/test/recovery/t/049_invalidate_new_slot.pl diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c index b9e2b115dab..ee0ab2d8c83 100644 --- a/src/backend/replication/slot.c +++ b/src/backend/replication/slot.c @@ -57,6 +57,7 @@ #include "utils/builtins.h" #include "utils/guc_hooks.h" #include "utils/varlena.h" +#include "utils/injection_point.h" /* * Replication slot on-disk data structure. @@ -1443,6 +1444,8 @@ ReplicationSlotReserveWal(void) slot->data.restart_lsn = restart_lsn; SpinLockRelease(&slot->mutex); + INJECTION_POINT("slot-reserve-wal-delay"); + /* prevent WAL removal as fast as possible */ ReplicationSlotsComputeRequiredLSN(); diff --git a/src/test/recovery/t/049_invalidate_new_slot.pl b/src/test/recovery/t/049_invalidate_new_slot.pl new file mode 100644 index 00000000000..a69df941762 --- /dev/null +++ b/src/test/recovery/t/049_invalidate_new_slot.pl @@ -0,0 +1,188 @@ +# This test checks that the new slot maybe invalidated by checkpoint +# + +use strict; +use warnings; +use Config; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +my $res; + +# Setup primary node +my $node = PostgreSQL::Test::Cluster->new('primary'); +$node->init(allows_streaming => 1); +$node->append_conf('postgresql.conf', q{wal_level = logical}); +$node->start(); +$node->safe_psql('postgres', q{create extension injection_points;}); + +my $createslot = $node->background_psql('postgres'); +my $checkpoint = $node->background_psql('postgres'); + +sub physreplslot_start_create() +{ + $createslot->query_until( + qr/start-create-slot/, + q(\echo start-create-slot + select pg_create_physical_replication_slot('standby1', true); + )); +} + +sub logreplslot_start_create() +{ + $createslot->query_until( + qr/start-create-slot/, + q(\echo start-create-slot + select pg_log_standby_snapshot(); + select pg_create_logical_replication_slot('standby1', 'pgoutput'); + )); +} + +sub physreplslot_create() +{ + $node->safe_psql('postgres', q{ + select pg_create_physical_replication_slot('standby1', true) + }); +} + +sub checkpoint_start() +{ + $checkpoint->query_until( + qr/start-checkpoint/, + q(\echo start-checkpoint + checkpoint; + )); +} + +sub injection_point_attach($$) +{ + my ($injectpname, $event) = @_; + + $node->safe_psql('postgres', "select injection_points_attach('$injectpname','$event')"); +} + +sub injection_point_detach($) +{ + my $injectpname = shift; + + $node->safe_psql('postgres', "select injection_points_detach('$injectpname');"); +} + +sub injection_point_wait($$) +{ + my ($subsystem, $injectpname) = @_; + + note("waiting for $injectpname injection point"); + $node->wait_for_event($subsystem, $injectpname); + note("injection_point $injectpname is reached"); +} + +sub injection_point_wakeup($) +{ + my $injectpname = shift; + + $node->safe_psql('postgres', "select injection_points_wakeup('$injectpname');"); +} + +sub get_slot_invalidation_state($) +{ + my $slotname = shift; + + return $node->safe_psql('postgres', + "select invalidation_reason from pg_replication_slots where slot_name='$slotname'"); +} + +# ------------------------------------------------------------------------------ +# Test: start physical slot creation before checkpoint (before new redo lsn assignment) +# ------------------------------------------------------------------------------ + +injection_point_attach('slot-reserve-wal-delay', 'wait'); +injection_point_attach('checkpoint-before-old-wal-removal', 'wait'); + +physreplslot_start_create(); +$node->advance_wal(3); +checkpoint_start(); +injection_point_wait('client backend', 'slot-reserve-wal-delay'); +injection_point_wait('checkpointer', 'checkpoint-before-old-wal-removal'); +injection_point_wakeup('slot-reserve-wal-delay'); +injection_point_wakeup('checkpoint-before-old-wal-removal'); + +eval { $createslot->quit(); }; +eval { $checkpoint->quit(); }; + +is(get_slot_invalidation_state('standby1'), "", "slot is not invalidated by checkpoint (1)"); + +injection_point_detach('slot-reserve-wal-delay'); +injection_point_detach('checkpoint-before-old-wal-removal'); +$node->safe_psql('postgres', "select pg_drop_replication_slot('standby1')"); + +# ------------------------------------------------------------------------------ +# Test: start physical slot creation during checkpoint (after new redo lsn assignment) +# ------------------------------------------------------------------------------ + +injection_point_attach('checkpoint-before-old-wal-removal', 'wait'); + +$node->advance_wal(3); +checkpoint_start(); +injection_point_wait('checkpointer', 'checkpoint-before-old-wal-removal'); +physreplslot_create(); +injection_point_wakeup('checkpoint-before-old-wal-removal'); + +eval { $checkpoint->quit(); }; + +is(get_slot_invalidation_state('standby1'), "", "slot is not invalidated by checkpoint (2)"); + +injection_point_detach('checkpoint-before-old-wal-removal'); +$node->safe_psql('postgres', "select pg_drop_replication_slot('standby1')"); + +# ------------------------------------------------------------------------------ +# Test: start logical slot creation before checkpoint (before new redo lsn assignment) +# ------------------------------------------------------------------------------ + +injection_point_attach('slot-reserve-wal-delay', 'wait'); +injection_point_attach('checkpoint-before-old-wal-removal', 'wait'); + +logreplslot_start_create(); +$node->advance_wal(3); +checkpoint_start(); +injection_point_wait('client backend', 'slot-reserve-wal-delay'); +injection_point_wait('checkpointer', 'checkpoint-before-old-wal-removal'); +injection_point_wakeup('slot-reserve-wal-delay'); +injection_point_wakeup('checkpoint-before-old-wal-removal'); + +eval { $createslot->quit(); }; +eval { $checkpoint->quit(); }; + +is(get_slot_invalidation_state('standby1'), "", "slot is not invalidated by checkpoint (3)"); + +injection_point_detach('slot-reserve-wal-delay'); +injection_point_detach('checkpoint-before-old-wal-removal'); +$node->safe_psql('postgres', "select pg_drop_replication_slot('standby1')"); + +# ------------------------------------------------------------------------------ +# Test: start logical slot creation during checkpoint (after new redo lsn assignment) +# ------------------------------------------------------------------------------ + +injection_point_attach('checkpoint-before-old-wal-removal', 'wait'); + +$node->advance_wal(3); +checkpoint_start(); +injection_point_wait('checkpointer', 'checkpoint-before-old-wal-removal'); +logreplslot_start_create(); +injection_point_wakeup('checkpoint-before-old-wal-removal'); + +eval { $createslot->quit(); }; +eval { $checkpoint->quit(); }; + +is(get_slot_invalidation_state('standby1'), "", "slot is not invalidated by checkpoint (4)"); + +injection_point_detach('checkpoint-before-old-wal-removal'); +$node->safe_psql('postgres', "select pg_drop_replication_slot('standby1')"); + +done_testing(); -- 2.34.1