From 92caf8f7bb7e64e7b91cba089a17d7255ab94004 Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Date: Mon, 28 Sep 2026 17:38:13 +0530
Subject: [PATCH v2 1/2] Recheck table sync state after refresh

An apply worker can cache a table's SYNCDONE state and then wait for
the subscription lock while ALTER SUBSCRIPTION ... REFRESH PUBLICATION
removes that table.  The worker then tries to mark the absent catalog
row READY and errors out.  With disable_on_error, that disables the
entire subscription, including unrelated tables.

Two refreshes can also remove and re-add the table before the apply
worker gets the lock.  Completing the old sync would then mark the new
INIT row READY and skip its initial copy.

Re-read the table state after taking the subscription lock, and skip
the READY transition unless it is still SYNCDONE.

This table-side issue predates the sequence-side fix in dca73f7dd03.
---
 src/backend/replication/logical/tablesync.c | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 4015e861a64..6c211ec0e62 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -433,9 +433,8 @@ ProcessSyncingTablesForApply(XLogRecPtr current_lsn)
 			if (current_lsn >= rstate->lsn)
 			{
 				char		originname[NAMEDATALEN];
-
-				rstate->state = SUBREL_STATE_READY;
-				rstate->lsn = current_lsn;
+				char		current_relstate;
+				XLogRecPtr	statelsn;
 
 				/*
 				 * Remove the tablesync origin tracking if exists.
@@ -455,6 +454,19 @@ ProcessSyncingTablesForApply(XLogRecPtr current_lsn)
 				LockSharedObject(SubscriptionRelationId, MyLogicalRepWorker->subid,
 								 0, AccessShareLock);
 
+				/* The table may have been removed or re-added meanwhile. */
+				current_relstate = GetSubscriptionRelState(MyLogicalRepWorker->subid,
+														   rstate->relid, &statelsn);
+				if (current_relstate != SUBREL_STATE_SYNCDONE)
+				{
+					elog(DEBUG1, "skipping READY transition for relation %u of subscription \"%s\" as it is no longer SYNCDONE",
+						 rstate->relid, MySubscription->name);
+					continue;
+				}
+
+				rstate->state = SUBREL_STATE_READY;
+				rstate->lsn = current_lsn;
+
 				if (!rel)
 					rel = table_open(SubscriptionRelRelationId, RowExclusiveLock);
 
-- 
2.34.1

