From 7afcf8a34aa479d2d81cc350352832b60ccaf844 Mon Sep 17 00:00:00 2001 From: Zhijie Hou Date: Thu, 29 Oct 2026 13:24:32 +0800 Subject: [PATCH v1] Avoid resetting subscription two_phase state to pending when already enabled. In AlterSubscription(), executing ALTER SUBSCRIPTION ... SET (two_phase = true) unconditionally set subtwophasestate to LOGICALREP_TWOPHASE_STATE_PENDING ('p'), even when two_phase was already in LOGICALREP_TWOPHASE_STATE_ENABLED ('e') state. Because two_phase remains enabled on the publisher's replication slot and prepared transactions may already exist on the subscriber, overwriting the state to 'p' broke checks that rely on subtwophasestate being 'e': 1. A subsequent ALTER SUBSCRIPTION ... SET (two_phase = false) skipped the LookupGXactBySubid() check and disabled two_phase while prepared transactions still existed on the subscriber. When those transactions were later committed on the publisher, the publisher re-sent all of their changes as regular transactions instead of COMMIT PREPARED, leading to self-deadlocks, duplicate data, or orphaned prepared transactions. 2. ALTER SUBSCRIPTION ... REFRESH PUBLICATION (and SET/ADD PUBLICATION) with copy_data = true was allowed even though the publisher's slot still had two_phase enabled, allowing prepared transactions on newly added tables to be streamed during initial table synchronization and applied as empty prepares. Fix this by not resetting subtwophasestate to PENDING when two_phase is already in ENABLED state. Author: Zhijie Hou Backpatch-through: 18 --- src/backend/commands/subscriptioncmds.c | 19 +++++++++++++------ src/test/subscription/t/021_twophase.pl | 10 ++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 36bb20a1f6e..560b601a407 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -1963,12 +1963,19 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, errmsg("cannot disable \"two_phase\" when prepared transactions exist"), errhint("Resolve these transactions and try again."))); - /* Change system catalog accordingly */ - values[Anum_pg_subscription_subtwophasestate - 1] = - CharGetDatum(opts.twophase ? - LOGICALREP_TWOPHASE_STATE_PENDING : - LOGICALREP_TWOPHASE_STATE_DISABLED); - replaces[Anum_pg_subscription_subtwophasestate - 1] = true; + /* + * Change system catalog accordingly, but do not reset the + * state to PENDING if two_phase is already enabled. + */ + if (!opts.twophase || + sub->twophasestate != LOGICALREP_TWOPHASE_STATE_ENABLED) + { + values[Anum_pg_subscription_subtwophasestate - 1] = + CharGetDatum(opts.twophase ? + LOGICALREP_TWOPHASE_STATE_PENDING : + LOGICALREP_TWOPHASE_STATE_DISABLED); + replaces[Anum_pg_subscription_subtwophasestate - 1] = true; + } } if (IsSet(opts.specified_opts, SUBOPT_FAILOVER)) diff --git a/src/test/subscription/t/021_twophase.pl b/src/test/subscription/t/021_twophase.pl index f755e748e9c..eb85e8e43d3 100644 --- a/src/test/subscription/t/021_twophase.pl +++ b/src/test/subscription/t/021_twophase.pl @@ -447,6 +447,16 @@ $node_subscriber->safe_psql('postgres', $node_subscriber->poll_query_until('postgres', "SELECT count(*) = 0 FROM pg_stat_activity WHERE backend_type = 'logical replication apply worker'" ); + +# Verify that setting two_phase to true when it is already enabled does not +# overwrite subtwophasestate to 'p' (pending). +$node_subscriber->safe_psql('postgres', + "ALTER SUBSCRIPTION tap_sub_copy SET (two_phase = true);"); +$result = $node_subscriber->safe_psql('postgres', + "SELECT subtwophasestate FROM pg_subscription WHERE subname = 'tap_sub_copy';" +); +is($result, qq(e), 'two-phase subscription option remains enabled'); + $node_subscriber->safe_psql( 'postgres', " ALTER SUBSCRIPTION tap_sub_copy SET (two_phase = false); -- 2.54.0 (Apple Git-157)