From 606e7d785f6179e54883ecfe67d8017f0b19d8f5 Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Mon, 24 Aug 2026 16:30:34 +0530 Subject: [PATCH] Test to reproduce assertion failure in GetSubscriptionRelations because of concurrent drop table. Test to reproduce assertion failure in GetSubscriptionRelations because of concurrent drop table. --- src/backend/catalog/pg_subscription.c | 3 + src/test/subscription/meson.build | 1 + .../t/059_subscription_rel_dropped.pl | 155 ++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 src/test/subscription/t/059_subscription_rel_dropped.pl diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index f1e8b624d8e..36870aa9419 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -31,6 +31,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" +#include "utils/injection_point.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/pg_lsn.h" @@ -684,6 +685,8 @@ GetSubscriptionRelations(Oid subid, bool tables, bool sequences, subrel = (Form_pg_subscription_rel) GETSTRUCT(tup); + INJECTION_POINT("subscription-relations-fetch", NULL); + /* Relation is either a sequence or a table */ relkind = get_rel_relkind(subrel->srrelid); Assert(relkind == RELKIND_SEQUENCE || relkind == RELKIND_RELATION || diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build index e71e95c6297..a9e3076b63b 100644 --- a/src/test/subscription/meson.build +++ b/src/test/subscription/meson.build @@ -48,6 +48,7 @@ tests += { 't/036_sequences.pl', 't/037_except.pl', 't/038_walsnd_shutdown_timeout.pl', + 't/059_subscription_rel_dropped.pl', 't/100_bugs.pl', ], }, diff --git a/src/test/subscription/t/059_subscription_rel_dropped.pl b/src/test/subscription/t/059_subscription_rel_dropped.pl new file mode 100644 index 00000000000..de71088152e --- /dev/null +++ b/src/test/subscription/t/059_subscription_rel_dropped.pl @@ -0,0 +1,155 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test GetSubscriptionRelations() against a relation dropped while it runs. +# +# GetSubscriptionRelations() scans pg_subscription_rel with the catalog snapshot +# taken when the scan started, and then looks up each relation's relkind through +# the syscache, which takes a newer snapshot. A relation whose drop commits in +# between is therefore still visible as a pg_subscription_rel row while its +# pg_class row is already gone, and get_rel_relkind() returns '\0' for it. +# +# That trips +# +# Assert(relkind == RELKIND_SEQUENCE || relkind == RELKIND_RELATION || +# relkind == RELKIND_PARTITIONED_TABLE); +# +# in an assert-enabled build. + +use strict; +use warnings FATAL => 'all'; +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 $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); +$node_publisher->init(allows_streaming => 'logical'); +$node_publisher->start; + +my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); +$node_subscriber->init(allows_streaming => 'logical'); +$node_subscriber->start; + +if (!$node_subscriber->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +# tab_keep stays in the subscription; tab_drop is the one dropped mid-scan. +for my $node ($node_publisher, $node_subscriber) +{ + $node->safe_psql( + 'postgres', qq{ + CREATE TABLE tab_keep (a int PRIMARY KEY); + CREATE TABLE tab_drop (a int PRIMARY KEY); + }); +} + +$node_publisher->safe_psql( + 'postgres', qq{ + CREATE PUBLICATION pub FOR TABLE tab_keep, tab_drop; + INSERT INTO tab_keep VALUES (1); +}); + +my $connstr = $node_publisher->connstr . ' dbname=postgres'; + +$node_subscriber->safe_psql( + 'postgres', " + CREATE SUBSCRIPTION sub + CONNECTION '$connstr application_name=sub' + PUBLICATION pub"); + +$node_subscriber->wait_for_subscription_sync($node_publisher, 'sub'); + +is($node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_keep"), + '1', 'initial data replicated'); + +$node_subscriber->safe_psql('postgres', 'CREATE EXTENSION injection_points'); + +############################################################################### +# Park the scan of pg_subscription_rel. +############################################################################### + +my $log_offset = -s $node_subscriber->logfile; + +my $refresh = $node_subscriber->background_psql('postgres'); +$refresh->set_query_timer_restart(); + +# Attach the injection point from the session that will run the REFRESH, and +# restrict it to that session, so that the apply worker cannot park at it too. +$refresh->query_safe("SELECT injection_points_set_local()"); +$refresh->query_safe( + "SELECT injection_points_attach('subscription-relations-fetch', 'wait')"); + +# Issue the REFRESH without waiting for it: the \echo marker comes back as soon +# as psql has read the input, while the command itself blocks in the scan. +$refresh->query_until( + qr/^issued$/m, qq[ + \\echo issued + ALTER SUBSCRIPTION sub REFRESH PUBLICATION; +]); + +$node_subscriber->poll_query_until('postgres', + "SELECT count(*) > 0 FROM pg_stat_activity WHERE wait_event = 'subscription-relations-fetch'" +) or die "timed out waiting for the REFRESH to reach the injection point"; + +pass('REFRESH is inside the pg_subscription_rel scan'); + +############################################################################### +# Drop the table while the scan is parked. This commits, removing both the +# pg_class row and the pg_subscription_rel row that the parked scan can still +# see. +############################################################################### + +$node_subscriber->safe_psql('postgres', "DROP TABLE tab_drop"); + +is( $node_subscriber->safe_psql( + 'postgres', qq{ + SELECT count(*) FROM pg_subscription_rel r + JOIN pg_class c ON c.oid = r.srrelid + WHERE c.relname = 'tab_drop'}), + '0', + 'dropped table is gone from pg_subscription_rel'); + +# Detach before waking up. The injection point sits inside the scan loop, so +# leaving it attached would park the next iteration again, and the wakeup below +# would have nothing left to release. +$node_subscriber->safe_psql('postgres', + "SELECT injection_points_detach('subscription-relations-fetch')"); + +$node_subscriber->safe_psql('postgres', + "SELECT injection_points_wakeup('subscription-relations-fetch')"); + +############################################################################### +# The relkind lookup for the dropped relation now returns '\0'. The scan must +# skip that row and let the REFRESH complete. +# +# Where the row is not skipped, the assertion in GetSubscriptionRelations() +# fails, which takes the backend down and makes the postmaster restart the +# cluster. +############################################################################### + +my $refresh_result; + +eval { + # Sending another query on the same session waits for the REFRESH to + # finish. + $refresh_result = $refresh->query_safe("SELECT 'refresh completed'"); +}; +my $refresh_error = $@; + +eval { $refresh->quit; }; + +# The headline check: the scan must not have failed an assertion. +my $trapped = $node_subscriber->log_contains(qr/TRAP: failed Assert/, $log_offset); + +ok(!$trapped, 'no assertion failure during the scan') + or diag( + "the scan reported a relation that is neither a table nor a sequence; " + . "GetSubscriptionRelations() needs to skip concurrently dropped relations"); + +done_testing(); -- 2.55.0