agora inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v1 4/4] Extra tests
217+ messages / 2 participants
[nested] [flat]

* [PATCH v1 4/4] Extra tests
@ 2025-08-31 09:10 Nikolay Shaplov <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Nikolay Shaplov @ 2025-08-31 09:10 UTC (permalink / raw)

Add more tests for trenary reloptions in dummy_index_am module
---
 src/test/modules/dummy_index_am/README        |  1 +
 .../modules/dummy_index_am/dummy_index_am.c   | 36 +++++++++++++-----
 .../dummy_index_am/expected/reloptions.out    | 38 +++++++++++++++++--
 .../modules/dummy_index_am/sql/reloptions.sql | 16 ++++++++
 src/test/regress/expected/reloptions.out      |  4 +-
 src/test/regress/sql/reloptions.sql           |  4 +-
 6 files changed, 81 insertions(+), 18 deletions(-)

diff --git a/src/test/modules/dummy_index_am/README b/src/test/modules/dummy_index_am/README
index 61510f02fae..78d1d84ed9a 100644
--- a/src/test/modules/dummy_index_am/README
+++ b/src/test/modules/dummy_index_am/README
@@ -6,6 +6,7 @@ access method, whose code is kept a maximum simple.
 
 This includes tests for all relation option types:
 - boolean
+- trenary
 - enum
 - integer
 - real
diff --git a/src/test/modules/dummy_index_am/dummy_index_am.c b/src/test/modules/dummy_index_am/dummy_index_am.c
index 94ef639b6fc..19826e50e6d 100644
--- a/src/test/modules/dummy_index_am/dummy_index_am.c
+++ b/src/test/modules/dummy_index_am/dummy_index_am.c
@@ -22,7 +22,7 @@
 PG_MODULE_MAGIC;
 
 /* parse table for fillRelOptions */
-static relopt_parse_elt di_relopt_tab[6];
+static relopt_parse_elt di_relopt_tab[8];
 
 /* Kind of relation options for dummy index */
 static relopt_kind di_relopt_kind;
@@ -40,6 +40,8 @@ typedef struct DummyIndexOptions
 	int			option_int;
 	double		option_real;
 	bool		option_bool;
+	trenary		option_trenary1;
+	trenary		option_trenary2;
 	DummyAmEnum option_enum;
 	int			option_string_val_offset;
 	int			option_string_null_offset;
@@ -96,23 +98,37 @@ create_reloptions_table(void)
 	di_relopt_tab[2].opttype = RELOPT_TYPE_BOOL;
 	di_relopt_tab[2].offset = offsetof(DummyIndexOptions, option_bool);
 
+	add_trenary_reloption(di_relopt_kind, "option_trenary1",
+					   "First trenary option for dummy_index_am",
+					   TRENARY_UNSET, NULL, AccessExclusiveLock);
+	di_relopt_tab[3].optname = "option_trenary1";
+	di_relopt_tab[3].opttype = RELOPT_TYPE_TRENARY;
+	di_relopt_tab[3].offset = offsetof(DummyIndexOptions, option_trenary1);
+
+	add_trenary_reloption(di_relopt_kind, "option_trenary2",
+					   "Second trenary option for dummy_index_am",
+					   TRENARY_TRUE, "do_not_know_yet", AccessExclusiveLock);
+	di_relopt_tab[4].optname = "option_trenary2";
+	di_relopt_tab[4].opttype = RELOPT_TYPE_TRENARY;
+	di_relopt_tab[4].offset = offsetof(DummyIndexOptions, option_trenary2);
+
 	add_enum_reloption(di_relopt_kind, "option_enum",
 					   "Enum option for dummy_index_am",
 					   dummyAmEnumValues,
 					   DUMMY_AM_ENUM_ONE,
 					   "Valid values are \"one\" and \"two\".",
 					   AccessExclusiveLock);
-	di_relopt_tab[3].optname = "option_enum";
-	di_relopt_tab[3].opttype = RELOPT_TYPE_ENUM;
-	di_relopt_tab[3].offset = offsetof(DummyIndexOptions, option_enum);
+	di_relopt_tab[5].optname = "option_enum";
+	di_relopt_tab[5].opttype = RELOPT_TYPE_ENUM;
+	di_relopt_tab[5].offset = offsetof(DummyIndexOptions, option_enum);
 
 	add_string_reloption(di_relopt_kind, "option_string_val",
 						 "String option for dummy_index_am with non-NULL default",
 						 "DefaultValue", &validate_string_option,
 						 AccessExclusiveLock);
-	di_relopt_tab[4].optname = "option_string_val";
-	di_relopt_tab[4].opttype = RELOPT_TYPE_STRING;
-	di_relopt_tab[4].offset = offsetof(DummyIndexOptions,
+	di_relopt_tab[6].optname = "option_string_val";
+	di_relopt_tab[6].opttype = RELOPT_TYPE_STRING;
+	di_relopt_tab[6].offset = offsetof(DummyIndexOptions,
 									   option_string_val_offset);
 
 	/*
@@ -123,9 +139,9 @@ create_reloptions_table(void)
 						 NULL,	/* description */
 						 NULL, &validate_string_option,
 						 AccessExclusiveLock);
-	di_relopt_tab[5].optname = "option_string_null";
-	di_relopt_tab[5].opttype = RELOPT_TYPE_STRING;
-	di_relopt_tab[5].offset = offsetof(DummyIndexOptions,
+	di_relopt_tab[7].optname = "option_string_null";
+	di_relopt_tab[7].opttype = RELOPT_TYPE_STRING;
+	di_relopt_tab[7].offset = offsetof(DummyIndexOptions,
 									   option_string_null_offset);
 }
 
diff --git a/src/test/modules/dummy_index_am/expected/reloptions.out b/src/test/modules/dummy_index_am/expected/reloptions.out
index c873a80bb75..bcf164f5bf7 100644
--- a/src/test/modules/dummy_index_am/expected/reloptions.out
+++ b/src/test/modules/dummy_index_am/expected/reloptions.out
@@ -18,6 +18,8 @@ SET client_min_messages TO 'notice';
 CREATE INDEX dummy_test_idx ON dummy_test_tab
   USING dummy_index_am (i) WITH (
   option_bool = false,
+  option_trenary1,
+  option_trenary2 = off,
   option_int = 5,
   option_real = 3.1,
   option_enum = 'two',
@@ -31,16 +33,20 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
          unnest         
 ------------------------
  option_bool=false
+ option_trenary1=true
+ option_trenary2=off
  option_int=5
  option_real=3.1
  option_enum=two
  option_string_val=null
  option_string_null=val
-(6 rows)
+(8 rows)
 
 -- ALTER INDEX .. SET
 ALTER INDEX dummy_test_idx SET (option_int = 10);
 ALTER INDEX dummy_test_idx SET (option_bool = true);
+ALTER INDEX dummy_test_idx SET (option_trenary1 = false);
+ALTER INDEX dummy_test_idx SET (option_trenary2 = Do_Not_Know_YET);
 ALTER INDEX dummy_test_idx SET (option_real = 3.2);
 ALTER INDEX dummy_test_idx SET (option_string_val = 'val2');
 ALTER INDEX dummy_test_idx SET (option_string_null = NULL);
@@ -49,19 +55,23 @@ ALTER INDEX dummy_test_idx SET (option_enum = 'three');
 ERROR:  invalid value for enum option "option_enum": three
 DETAIL:  Valid values are "one" and "two".
 SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
-         unnest          
--------------------------
+             unnest              
+---------------------------------
  option_int=10
  option_bool=true
+ option_trenary1=false
+ option_trenary2=do_not_know_yet
  option_real=3.2
  option_string_val=val2
  option_string_null=null
  option_enum=one
-(6 rows)
+(8 rows)
 
 -- ALTER INDEX .. RESET
 ALTER INDEX dummy_test_idx RESET (option_int);
 ALTER INDEX dummy_test_idx RESET (option_bool);
+ALTER INDEX dummy_test_idx RESET (option_trenary1);
+ALTER INDEX dummy_test_idx RESET (option_trenary2);
 ALTER INDEX dummy_test_idx RESET (option_real);
 ALTER INDEX dummy_test_idx RESET (option_enum);
 ALTER INDEX dummy_test_idx RESET (option_string_val);
@@ -100,6 +110,26 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 (1 row)
 
 ALTER INDEX dummy_test_idx RESET (option_bool);
+-- Trenary
+ALTER INDEX dummy_test_idx SET (option_trenary1 = 4); -- error
+ERROR:  invalid value for trenary option "option_trenary1": 4
+ALTER INDEX dummy_test_idx SET (option_trenary1 = 1); -- ok, as true
+ALTER INDEX dummy_test_idx SET (option_trenary1 = 3.4); -- error
+ERROR:  invalid value for trenary option "option_trenary1": 3.4
+ALTER INDEX dummy_test_idx SET (option_trenary1 = 'val4'); -- error
+ERROR:  invalid value for trenary option "option_trenary1": val4
+ALTER INDEX dummy_test_idx SET (option_trenary1 = 'do_not_know_yet'); -- error. Valid for trenary2 not for trenary1
+ERROR:  invalid value for trenary option "option_trenary1": do_not_know_yet
+ALTER INDEX dummy_test_idx SET (option_trenary2 = 'do_not_know_yet'); -- ok
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
+             unnest              
+---------------------------------
+ option_trenary1=1
+ option_trenary2=do_not_know_yet
+(2 rows)
+
+ALTER INDEX dummy_test_idx RESET (option_trenary1);
+ALTER INDEX dummy_test_idx RESET (option_trenary2);
 -- Float
 ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok
 ALTER INDEX dummy_test_idx SET (option_real = true); -- error
diff --git a/src/test/modules/dummy_index_am/sql/reloptions.sql b/src/test/modules/dummy_index_am/sql/reloptions.sql
index 6749d763e6a..7e752b12d16 100644
--- a/src/test/modules/dummy_index_am/sql/reloptions.sql
+++ b/src/test/modules/dummy_index_am/sql/reloptions.sql
@@ -18,6 +18,8 @@ SET client_min_messages TO 'notice';
 CREATE INDEX dummy_test_idx ON dummy_test_tab
   USING dummy_index_am (i) WITH (
   option_bool = false,
+  option_trenary1,
+  option_trenary2 = off,
   option_int = 5,
   option_real = 3.1,
   option_enum = 'two',
@@ -30,6 +32,8 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 -- ALTER INDEX .. SET
 ALTER INDEX dummy_test_idx SET (option_int = 10);
 ALTER INDEX dummy_test_idx SET (option_bool = true);
+ALTER INDEX dummy_test_idx SET (option_trenary1 = false);
+ALTER INDEX dummy_test_idx SET (option_trenary2 = Do_Not_Know_YET);
 ALTER INDEX dummy_test_idx SET (option_real = 3.2);
 ALTER INDEX dummy_test_idx SET (option_string_val = 'val2');
 ALTER INDEX dummy_test_idx SET (option_string_null = NULL);
@@ -40,6 +44,8 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 -- ALTER INDEX .. RESET
 ALTER INDEX dummy_test_idx RESET (option_int);
 ALTER INDEX dummy_test_idx RESET (option_bool);
+ALTER INDEX dummy_test_idx RESET (option_trenary1);
+ALTER INDEX dummy_test_idx RESET (option_trenary2);
 ALTER INDEX dummy_test_idx RESET (option_real);
 ALTER INDEX dummy_test_idx RESET (option_enum);
 ALTER INDEX dummy_test_idx RESET (option_string_val);
@@ -60,6 +66,16 @@ ALTER INDEX dummy_test_idx SET (option_bool = 3.4); -- error
 ALTER INDEX dummy_test_idx SET (option_bool = 'val4'); -- error
 SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 ALTER INDEX dummy_test_idx RESET (option_bool);
+-- Trenary
+ALTER INDEX dummy_test_idx SET (option_trenary1 = 4); -- error
+ALTER INDEX dummy_test_idx SET (option_trenary1 = 1); -- ok, as true
+ALTER INDEX dummy_test_idx SET (option_trenary1 = 3.4); -- error
+ALTER INDEX dummy_test_idx SET (option_trenary1 = 'val4'); -- error
+ALTER INDEX dummy_test_idx SET (option_trenary1 = 'do_not_know_yet'); -- error. Valid for trenary2 not for trenary1
+ALTER INDEX dummy_test_idx SET (option_trenary2 = 'do_not_know_yet'); -- ok
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
+ALTER INDEX dummy_test_idx RESET (option_trenary1);
+ALTER INDEX dummy_test_idx RESET (option_trenary2);
 -- Float
 ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok
 ALTER INDEX dummy_test_idx SET (option_real = true); -- error
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index e32431ca067..54b3424ef2b 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -98,7 +98,7 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {fillfactor=13,autovacuum_enabled=false}
 (1 row)
 
--- Tests for future (FIXME) trenary options
+-- Tests for trenary options
 -- behave as boolean option: accept unassigned name and truncated value
 DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate);
@@ -116,7 +116,7 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {vacuum_truncate=fals}
 (1 row)
 
--- preferred "true" alias is used when storing
+-- preferred "true" alias is stored in pg_class
 DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 051142eed5f..1971d460672 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -59,7 +59,7 @@ UPDATE pg_class
 ALTER TABLE reloptions_test RESET (illegal_option);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 
--- Tests for future (FIXME) trenary options
+-- Tests for trenary options
 
 -- behave as boolean option: accept unassigned name and truncated value
 DROP TABLE reloptions_test;
@@ -70,7 +70,7 @@ DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 
--- preferred "true" alias is used when storing
+-- preferred "true" alias is stored in pg_class
 DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
-- 
2.39.2


--nextPart6784952.tM3a2QDmDi--

--nextPart6187903.UjTJXf6HLC
Content-Type: application/pgp-signature; name="signature.asc"
Content-Description: This is a digitally signed message part.
Content-Transfer-Encoding: 7Bit

-----BEGIN PGP SIGNATURE-----

iQEzBAABCgAdFiEE+sk3ebqQKlezKOi8PMbfuIHAGpgFAmi0gCMACgkQPMbfuIHA
Gpi3Dgf/dQxIJ7WM8rL4szT+d+7gCw1BUzA7H4SkLtGM8E+akUvbjt6lFpCKN7fP
dHWTY7NeHsLy8hXB7Z+P4edaHAqNQ7pXtESuC0jBB1Ta5r9f0mUz/tNvyaoj1YPV
SES411gQwABIJ/ZXoqrcAHgmJoyAKZcFWU9HBAhJyL3Da/o7L86wgro4a6ov7cgP
1mJa6WjH1qf1v4PPrZYxLMmU5v3uacNDFowNsQ8+uHs+slUWjkw4VYatdReWla0a
4qBnMaht1Xky3f55YfB7Gx28CBUUPMREUVKFOrcopq95kZ9WASoTJfU9Y80IDPWb
KVcJvYDk5TEL1t48GpFkjZbL9od51w==
=yrfm
-----END PGP SIGNATURE-----

--nextPart6187903.UjTJXf6HLC--








^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-03 14:03 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:03 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 167 +++++++++++++++++-------
 1 file changed, 118 insertions(+), 49 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e23b366a87d..866341c2cfb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2570,11 +2596,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subid = form->oid;
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	/* DROP hook for the subscription being removed */
 	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
@@ -2583,20 +2604,68 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
+
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
 
 	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
-- 
2.34.1


--3eAVwo4vDNlHc8RC
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v3-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--3eAVwo4vDNlHc8RC--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication
@ 2026-07-03 14:46 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-03 14:46 UTC (permalink / raw)

Apply the same RangeVarGetRelidExtended() style retry loop to
AlterPublication()'s tables/schemas branch that was added for subscriptions
in commit XXXX.

Previously, this branch resolved the publication name and checked ownership
at the top of AlterPublication(), then locked and re-read by OID. This left a
window where concurrent DDL could have modified the ownership and/or the name
resolution

Now the tables/schemas branch has its own complete retry loop: name
resolution, ownership check, and lock acquisition all inside the loop.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/publicationcmds.c | 105 +++++++++++++++++--------
 1 file changed, 72 insertions(+), 33 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..dfd707bc7d7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -39,6 +39,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
@@ -1662,54 +1663,92 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
-	tup = SearchSysCacheCopy1(PUBLICATIONNAME,
-							  CStringGetDatum(stmt->pubname));
+	if (stmt->options)
+	{
+		tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+								  CStringGetDatum(stmt->pubname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("publication \"%s\" does not exist",
-						stmt->pubname)));
+		if (!HeapTupleIsValid(tup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("publication \"%s\" does not exist",
+							stmt->pubname)));
 
-	pubform = (Form_pg_publication) GETSTRUCT(tup);
+		pubform = (Form_pg_publication) GETSTRUCT(tup);
 
-	/* must be owner */
-	if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
-					   stmt->pubname);
+		/* must be owner */
+		if (!object_ownercheck(PublicationRelationId, pubform->oid,
+							   GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+						   stmt->pubname);
 
-	if (stmt->options)
 		AlterPublicationOptions(pstate, stmt, rel, tup);
+	}
 	else
 	{
 		List	   *relations = NIL;
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
-		Oid			pubid = pubform->oid;
+		Oid			pubid;
 
-		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
-								   &exceptrelations, &schemaidlist);
+		/*
+		 * Lock the publication so nobody else can do anything with it.
+		 *
+		 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+		 * name, check ownership, and lock inside a loop. If invalidation
+		 * messages arrive (indicating concurrent DDL), we retry. We keep the
+		 * lock held across retries and only release it if the name resolves
+		 * to a different OID on the next iteration.
+		 */
+		{
+			Oid			oldPubId = InvalidOid;
+			bool		retry = false;
 
-		CheckAlterPublication(stmt, tup, relations, schemaidlist);
+			for (;;)
+			{
+				uint64		inval_count = SharedInvalidMessageCounter;
 
-		heap_freetuple(tup);
+				tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+										  CStringGetDatum(stmt->pubname));
 
-		/* Lock the publication so nobody else can do anything with it. */
-		LockDatabaseObject(PublicationRelationId, pubid, 0,
-						   AccessExclusiveLock);
+				if (!HeapTupleIsValid(tup))
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("publication \"%s\" does not exist",
+									stmt->pubname)));
 
-		/*
-		 * It is possible that by the time we acquire the lock on publication,
-		 * concurrent DDL has removed it. We can test this by checking the
-		 * existence of publication. We get the tuple again to avoid the risk
-		 * of any publication option getting changed.
-		 */
-		tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
-		if (!HeapTupleIsValid(tup))
-			ereport(ERROR,
-					errcode(ERRCODE_UNDEFINED_OBJECT),
-					errmsg("publication \"%s\" does not exist",
-						   stmt->pubname));
+				pubform = (Form_pg_publication) GETSTRUCT(tup);
+				pubid = pubform->oid;
+
+				if (!object_ownercheck(PublicationRelationId, pubid,
+									   GetUserId()))
+					aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+								   stmt->pubname);
+
+				if (retry)
+				{
+					if (pubid == oldPubId)
+						break;
+					UnlockDatabaseObject(PublicationRelationId, oldPubId, 0,
+										 AccessExclusiveLock);
+				}
+
+				LockDatabaseObject(PublicationRelationId, pubid, 0,
+								   AccessExclusiveLock);
+
+				if (inval_count == SharedInvalidMessageCounter)
+					break;
+
+				retry = true;
+				oldPubId = pubid;
+				heap_freetuple(tup);
+			}
+		}
+
+		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
+								   &exceptrelations, &schemaidlist);
+
+		CheckAlterPublication(stmt, tup, relations, schemaidlist);
 
 		relations = list_concat(relations, exceptrelations);
 		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-- 
2.34.1


--b+bwjVhifbzgesmw--





^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread

* [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription
@ 2026-07-06 04:44 Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 217+ messages in thread

From: Bertrand Drouvot @ 2026-07-06 04:44 UTC (permalink / raw)

Following the approach of RangeVarGetRelidExtended() for relations, add a
retry loop that includes name resolution, ownership check, and lock
acquisition in AlterSubscription() and DropSubscription().

The loop records SharedInvalidMessageCounter, resolves the subscription name
to an OID, checks ownership, then locks the subscription. If the invalidation
counter changed (indicating concurrent DDL), we save the current OID and
retry. On the next iteration, if the name still resolves to the same OID,
we're done (already holding the correct lock). If it resolves to a different
OID, we release the old lock and acquire the new one.

This mirrors RangeVarGetRelidExtended()'s behavior: the lock is kept across
retries to avoid a window where another session could have committed concurrent
DDL modifying the ownership and/or the name resolution.

Author: Bertrand Drouvot <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Hayato Kuroda (Fujitsu) <[email protected]>
Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg
---
 src/backend/commands/subscriptioncmds.c | 189 +++++++++++++++++-------
 1 file changed, 134 insertions(+), 55 deletions(-)
 100.0% src/backend/commands/

diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c9e7fbdb47b..615c3921bc3 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@
 #include "replication/walsender.h"
 #include "replication/worker_internal.h"
 #include "storage/lmgr.h"
+#include "storage/sinval.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
@@ -1592,23 +1593,67 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 
 	rel = table_open(SubscriptionRelationId, RowExclusiveLock);
 
-	/* Fetch the existing tuple. */
-	tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId),
-							  CStringGetDatum(stmt->subname));
+	/*
+	 * Lock the subscription so nobody else can do anything with it.
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
+	 */
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
+			tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME,
+									  ObjectIdGetDatum(MyDatabaseId),
+									  CStringGetDatum(stmt->subname));
 
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
+			if (!HeapTupleIsValid(tup))
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_OBJECT),
+						 errmsg("subscription \"%s\" does not exist",
+								stmt->subname)));
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer. So we're done. If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			heap_freetuple(tup);
+		}
+	}
 
 	/* parse and check options */
 	switch (stmt->kind)
@@ -1686,25 +1731,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
 			orig_conninfo_needed = false;
 	}
 
-	heap_freetuple(tup);
-
-	/* Lock the subscription so nobody else can do anything with it. */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
-
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * DROP or ALTER may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
-
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
-
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-
 	/*
 	 * Skip ACL checks on the subscription's foreign server, if any. If
 	 * changing the server (or replacing it with a raw connection), then the
@@ -2567,38 +2593,91 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 		return;
 	}
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
-	subid = form->oid;
-
-	/* must be owner */
-	if (!object_ownercheck(SubscriptionRelationId, subid, GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
-					   stmt->subname);
-
 	ReleaseSysCache(tup);
 
 	/*
 	 * Lock the subscription so nobody else can do anything with it (including
 	 * the replication workers).
+	 *
+	 * Like RangeVarGetRelidExtended() does for relations, we resolve the
+	 * name, check ownership, and lock inside a loop. If invalidation messages
+	 * arrive (indicating concurrent DDL), we retry. We keep the lock held
+	 * across retries and only release it if the name resolves to a different
+	 * OID on the next iteration.
 	 */
-	LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock);
+	{
+		Oid			oldSubId = InvalidOid;
+		bool		retry = false;
 
-	/* DROP hook for the subscription being removed */
-	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
+		for (;;)
+		{
+			uint64		inval_count = SharedInvalidMessageCounter;
 
-	/*
-	 * Re-read the subscription tuple after acquiring the lock. A concurrent
-	 * ALTER or DROP may have committed before we acquired the lock.
-	 */
-	tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid));
+			tup = SearchSysCache2(SUBSCRIPTIONNAME,
+								  ObjectIdGetDatum(MyDatabaseId),
+								  CStringGetDatum(stmt->subname));
 
-	if (!HeapTupleIsValid(tup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("subscription \"%s\" does not exist",
-						stmt->subname)));
+			if (!HeapTupleIsValid(tup))
+			{
+				if (retry)
+					UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+									   AccessExclusiveLock);
+				table_close(rel, NoLock);
+
+				if (!stmt->missing_ok)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_OBJECT),
+							 errmsg("subscription \"%s\" does not exist",
+									stmt->subname)));
+				else
+					ereport(NOTICE,
+							(errmsg("subscription \"%s\" does not exist, skipping",
+									stmt->subname)));
+
+				return;
+			}
+
+			form = (Form_pg_subscription) GETSTRUCT(tup);
+			subid = form->oid;
+
+			if (!object_ownercheck(SubscriptionRelationId, subid,
+								   GetUserId()))
+			{
+				ReleaseSysCache(tup);
+				aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
+							   stmt->subname);
+			}
+
+			/*
+			 * If upon retry we get the same OID, the invalidation messages
+			 * did not change the final answer.  So we're done.  If we got a
+			 * different OID, unlock the old one and lock the new one below.
+			 */
+			if (retry)
+			{
+				if (subid == oldSubId)
+					break;
+				UnlockSharedObject(SubscriptionRelationId, oldSubId, 0,
+								   AccessExclusiveLock);
+			}
+
+			LockSharedObject(SubscriptionRelationId, subid, 0,
+							 AccessExclusiveLock);
+
+			/* If no invalidation messages, we're done. */
+			if (inval_count == SharedInvalidMessageCounter)
+				break;
+
+			/* Something may have changed, retry. */
+			retry = true;
+			oldSubId = subid;
+			ReleaseSysCache(tup);
+		}
+	}
+
+	/* DROP hook for the subscription being removed */
+	InvokeObjectDropHook(SubscriptionRelationId, subid, 0);
 
-	form = (Form_pg_subscription) GETSTRUCT(tup);
 	subowner = form->subowner;
 	subserver = form->subserver;
 	subconflictlogrelid = form->subconflictlogrelid;
-- 
2.34.1


--b+bwjVhifbzgesmw
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch"



^ permalink  raw  reply  [nested|flat] 217+ messages in thread


end of thread, other threads:[~2026-07-06 04:44 UTC | newest]

Thread overview: 217+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2025-08-31 09:10 [PATCH v1 4/4] Extra tests Nikolay Shaplov <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:03 [PATCH v3 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v3 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-03 14:46 [PATCH v4 4/4] Add invalidation-based retry loop for AlterPublication Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>
2026-07-06 04:44 [PATCH v4 3/4] Add invalidation-based retry loop for Alter/Drop Subscription Bertrand Drouvot <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox