nocfbot-test.patch
application/octet-stream
Filename: nocfbot-test.patch
Type: application/octet-stream
Part: 0
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index f012e99c9d7..a91082efb9c 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -787,6 +787,14 @@ ReplicationSlotRelease(void)
* decoding be disabled.
*/
ReplicationSlotDropAcquired(is_logical);
+
+ /*
+ * The slot's array entry is now free and may be reused by another
+ * backend at any moment. This injection point lets tests open that
+ * window deterministically and verify we never touch the (now stale)
+ * slot pointer afterwards.
+ */
+ INJECTION_POINT("ephemeral-slot-release-after-drop", NULL);
}
/*
diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl
index 97d11f98b59..fa5a0222dcd 100644
--- a/src/test/recovery/t/006_logical_decoding.pl
+++ b/src/test/recovery/t/006_logical_decoding.pl
@@ -275,6 +275,80 @@ is( $node_primary->safe_psql(
qq(Check that reset timestamp is later after resetting stats for slot '$stats_test_slot1' again.)
);
+# Regression test for the race in ReplicationSlotRelease() when releasing an
+# ephemeral slot. Releasing an ephemeral slot first drops it via
+# ReplicationSlotDropAcquired(), which frees the slot's array entry for reuse.
+# ReplicationSlotRelease() must not touch that (now stale) slot pointer
+# afterwards, or it can corrupt an unrelated slot that grabbed the freed entry.
+# This needs an injection point, so it only runs on builds that support them.
+SKIP:
+{
+ skip "Injection points not supported by this build", 2
+ if $ENV{enable_injection_points} ne 'yes';
+ skip "Extension injection_points not installed", 2
+ unless $node_primary->check_extension('injection_points');
+
+ $node_primary->safe_psql('postgres', q(CREATE EXTENSION injection_points));
+
+ # Freeze a backend right after it drops an ephemeral slot, i.e. once the
+ # slot's array entry has been freed and is available for reuse.
+ $node_primary->safe_psql('postgres',
+ q{SELECT injection_points_attach('ephemeral-slot-release-after-drop', 'wait')}
+ );
+
+ # Create a logical slot with a non-existent output plugin. The slot is
+ # created as RS_EPHEMERAL; the bogus plugin makes creation ERROR out, and
+ # the error path calls ReplicationSlotRelease(), which drops the ephemeral
+ # slot and then blocks on the injection point. on_error_stop => 0 keeps the
+ # session alive past the ERROR so it can still report 'done_release'.
+ my $dropper =
+ $node_primary->background_psql('postgres', on_error_stop => 0);
+ $dropper->query_until(
+ qr/start_drop/, q(
+\echo start_drop
+SELECT pg_create_logical_replication_slot('test_slot_dropped', 'no_such_plugin', false);
+\echo done_release
+));
+
+ # Wait until the backend is parked on the injection point, with the array
+ # entry of 'test_slot_dropped' freed and up for grabs.
+ $node_primary->wait_for_event('client backend',
+ 'ephemeral-slot-release-after-drop');
+
+ # A second backend creates a new persistent slot, which reuses the just
+ # freed array entry -- the very entry the frozen backend still points at.
+ $node_primary->safe_psql('postgres',
+ q{SELECT pg_create_physical_replication_slot('test_slot_created', true, false)}
+ );
+
+ my $before = $node_primary->safe_psql('postgres',
+ q{SELECT inactive_since FROM pg_replication_slots WHERE slot_name = 'test_slot_created'}
+ );
+
+ # Let the frozen backend finish releasing. With the bug it now scribbles on
+ # the reused entry; with the fix it leaves it untouched.
+ $node_primary->safe_psql('postgres',
+ q{SELECT injection_points_wakeup('ephemeral-slot-release-after-drop')});
+ $dropper->query_until(qr/done_release/, '');
+
+ my $after = $node_primary->safe_psql('postgres',
+ q{SELECT inactive_since FROM pg_replication_slots WHERE slot_name = 'test_slot_created'}
+ );
+
+ is($after, $before,
+ 'releasing an ephemeral slot must not modify a slot that reused its entry'
+ );
+
+ my $slot = $node_primary->safe_psql('postgres',
+ q{SELECT slot_name, slot_type FROM pg_replication_slots WHERE slot_name = 'test_slot_created'}
+ );
+ is($slot, "test_slot_created|physical", 'reused slot is intact');
+
+ $dropper->quit;
+ $node_primary->safe_psql('postgres',
+ q{SELECT injection_points_detach('ephemeral-slot-release-after-drop')});
+}
+
# done with the node
$node_primary->stop;