From b36746ffb63f20af2bbd636d9ab6beadbee8b9ec Mon Sep 17 00:00:00 2001 From: Hayato Kuroda Date: Mon, 7 Sep 2026 17:52:21 +0900 Subject: [PATCH] Reproducer: race condition with CREATE SUBSCRIPTION --- src/backend/commands/subscriptioncmds.c | 2 + src/test/subscription/meson.build | 1 + .../subscription/t/040_refresh_table_race.pl | 123 ++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 src/test/subscription/t/040_refresh_table_race.pl diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index abec24c7a85..708262b34ce 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -1840,6 +1840,8 @@ AlterSubscription_refresh_table(Subscription *sub, List *relations) */ CheckRefreshTableNotInOtherSubscriptions(rel, sub, subrelids, relids); + INJECTION_POINT("subscription-refresh-table-after-subscription-check", NULL); + /* * The set is settled, so open what is about to be truncated. The locks * are already held. diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build index 37f480bcac2..49bfbcbfd40 100644 --- a/src/test/subscription/meson.build +++ b/src/test/subscription/meson.build @@ -49,6 +49,7 @@ tests += { 't/037_except.pl', 't/038_walsnd_shutdown_timeout.pl', 't/039_refresh_table.pl', + 't/040_refresh_table_race.pl', 't/100_bugs.pl', ], }, diff --git a/src/test/subscription/t/040_refresh_table_race.pl b/src/test/subscription/t/040_refresh_table_race.pl new file mode 100644 index 00000000000..ddbaf87f84a --- /dev/null +++ b/src/test/subscription/t/040_refresh_table_race.pl @@ -0,0 +1,123 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test concurrent ALTER SUBSCRIPTION ... REFRESH TABLE operations. +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +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; +$node_subscriber->start; + +# Setup publisher +$node_publisher->safe_psql( + 'postgres', qq( + CREATE TABLE tab_race (a int primary key) PARTITION BY RANGE (a); + CREATE TABLE tab_race_1 PARTITION OF tab_race + FOR VALUES FROM (1) TO (12); + INSERT INTO tab_race SELECT generate_series(1, 10); + CREATE PUBLICATION tap_pub_race_leaf FOR TABLE tab_race_1; + CREATE PUBLICATION tap_pub_race_root + FOR TABLE tab_race WHERE (a > 5) + WITH (publish_via_partition_root = true); +)); + +# Setup subscriber +my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; + +$node_subscriber->safe_psql( + 'postgres', qq( + CREATE EXTENSION injection_points; + CREATE TABLE tab_race (a int primary key) PARTITION BY RANGE (a); + CREATE TABLE tab_race_1 PARTITION OF tab_race + FOR VALUES FROM (1) TO (12); + CREATE SUBSCRIPTION tap_sub_race_leaf + CONNECTION '$publisher_connstr' + PUBLICATION tap_pub_race_leaf; +)); +$node_subscriber->wait_for_subscription_sync($node_publisher, + 'tap_sub_race_leaf'); + +is($node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_race_1"), + '10', + 'subscriber has the complete baseline before refresh'); + +# Disable the leaf subscription before altering the publication +$node_subscriber->safe_psql('postgres', + "ALTER SUBSCRIPTION tap_sub_race_leaf DISABLE"); +$node_subscriber->poll_query_until('postgres', + "SELECT count(*) = 0 FROM pg_stat_subscription WHERE subname = 'tap_sub_race_leaf' AND pid IS NOT NULL" +) or die "Timed out waiting for subscription workers to stop"; + +# The initial copy above supplied all rows. Limit subsequent copies to 1..5, +# so rows 6..10 cannot be restored if the concurrent refresh removes them. +$node_publisher->safe_psql('postgres', + "ALTER PUBLICATION tap_pub_race_leaf SET TABLE tab_race_1 WHERE (a <= 5)"); + +# Pause after REFRESH TABLE's final check for subscriptions that also feed the +# leaf. The refresh holds AccessExclusiveLock on the leaf at this point, but +# not on its partitioned root. +$node_subscriber->safe_psql( + 'postgres', + "SELECT injection_points_attach('subscription-refresh-table-after-subscription-check', 'wait')" +); +my $refresh = $node_subscriber->background_psql('postgres'); +$refresh->query_until( + qr/starting_refresh_table/, q{ + \echo starting_refresh_table + ALTER SUBSCRIPTION tap_sub_race_leaf REFRESH TABLE tab_race_1; +}); +$node_subscriber->wait_for_event('client backend', + 'subscription-refresh-table-after-subscription-check'); + +# This subscription locks only the partitioned root, so it can be registered +# after the refresh's final check despite feeding the locked leaf via routing. +$node_subscriber->safe_psql( + 'postgres', qq( + CREATE SUBSCRIPTION tap_sub_race_root + CONNECTION '$publisher_connstr' + PUBLICATION tap_pub_race_root + WITH (copy_data = false, enabled = false); +)); +is( $node_subscriber->safe_psql( + 'postgres', + "SELECT r.srsubstate FROM pg_subscription_rel r JOIN pg_subscription s ON s.oid = r.srsubid WHERE s.subname = 'tap_sub_race_root'" + ), + 'r', + 'concurrent root subscription is registered as ready'); + +# Resume the refresh. The state of tab_race_1 will be 'i' +$node_subscriber->safe_psql( + 'postgres', + "SELECT injection_points_wakeup('subscription-refresh-table-after-subscription-check')" +); +ok($refresh->quit, 'concurrent leaf refresh completed'); + +# Enable both subscriptions and initial sync will be triggered again. +$node_subscriber->safe_psql( + 'postgres', qq( + ALTER SUBSCRIPTION tap_sub_race_leaf ENABLE; + ALTER SUBSCRIPTION tap_sub_race_root ENABLE; +)); +$node_subscriber->wait_for_subscription_sync($node_publisher, + 'tap_sub_race_leaf'); +$node_publisher->wait_for_catchup('tap_sub_race_root'); + +# Verify that the root subscription applies new changes even though it cannot +# restore rows 6..10 that were removed after it was marked READY. +$node_publisher->safe_psql('postgres', "INSERT INTO tab_race VALUES (11)"); +$node_publisher->wait_for_catchup('tap_sub_race_root'); +is($node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_race_1"), + '11', + 'rows maintained by both subscriptions survive the concurrent refresh'); + +$node_subscriber->stop('fast'); +$node_publisher->stop('fast'); + +done_testing(); -- 2.52.0