Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wr87Y-000AFy-11 for pgsql-hackers@arkaria.postgresql.org; Tue, 04 Aug 2026 05:55:24 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wr87V-003G76-0X for pgsql-hackers@arkaria.postgresql.org; Tue, 04 Aug 2026 05:55:21 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wr87U-003G6y-1m for pgsql-hackers@lists.postgresql.org; Tue, 04 Aug 2026 05:55:21 +0000 Received: from mail-wr1-x433.google.com ([2a00:1450:4864:20::433]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wr87R-000000007MV-3JHX for pgsql-hackers@lists.postgresql.org; Tue, 04 Aug 2026 05:55:20 +0000 Received: by mail-wr1-x433.google.com with SMTP id ffacd0b85a97d-47db714766aso321194f8f.0 for ; Mon, 03 Aug 2026 22:55:18 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1785822915; x=1786427715; darn=lists.postgresql.org; h=in-reply-to:content-disposition:content-type:mime-version :references:message-id:subject:cc:to:from:date:from:to:cc:subject :date:message-id:reply-to:content-type; bh=MhEd4lhvkDcSHLgdji6ky837RDkD5GPyd1eBf4TyUB8=; b=MjZy+xCTELw+gY344gj0drLNWbK/qP3+ew27mqYNrjJdm//7gKdfYVknr44y+IObeP tvAqw7mAxgYGCjtNshR8qidQtPCuQ4jPg57mv1Ux8xNubMLb/tdnW4qiuM4+cyA4+mMC TBTZmFojE75JSE0g4z5IUmXJt9jzrgPPHgLWrLVpvClQXvoxJgse07OgFS9eHUTY4zMN HWrfBm+3N/wiyui757UlRFFfziljVg5T/lyoJlv+DRvaSiZZACcPGsz7NHxxy5nk0Rh6 uYUx6Z/1AxxjsqUbFs4aj3TL/733AP6kDuzTtzSPhQWKeNdGN7zAJg0AhmFZ7mwkmcmP JAUQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1785822915; x=1786427715; h=in-reply-to:content-disposition:content-type:mime-version :references:message-id:subject:cc:to:from:date:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=MhEd4lhvkDcSHLgdji6ky837RDkD5GPyd1eBf4TyUB8=; b=aQ9uVhuNf+bozfryD6BvfoQzWQTN6yV4H4CHUmkzbkaiyIokpTNHYij3ZcFhE1SDps sSqSlA77qJsKIfz7z01Pm12nrnZ/3yMIlB0SKZmGxNDJ/Ulfg0vAKvFtMRNBTd/DkKiD liwWQhK4NiZddg6ejy1m2lpuC3Y0PEC0kgat1Mm4eXmqRNSh+gYndU6QkJdqiRr+6FBI pZDe56cIRaz9EscAbwYXQpC0Oa42w+a+bgj+buPHpeFI/lQ7YnVIWSyN0vxm7f1l88RF 0g2vbhYQfl3ch+0D/liJdunRUWOWzR53AdaV6BqmZzzBxiQGINuaM0cJlERYtz+D/G9H fr1A== X-Forwarded-Encrypted: i=1; AHgh+Rout5XAdMmQK8CphRHm0NywpOHksEx0mDQFg5OU1rckBR0y6mDIJsGG5O9M2U+9R2X5vhCUIBrZSOZed6S5@lists.postgresql.org X-Gm-Message-State: AOJu0Yy6k7X1D/74SXrM13BZ8VFbhqDzAXxvbO1IAkKxSdhNtHYfS0Ak qgmRGFV1Tr80LyQxCN9MSYyiRkEN1dG+84pp0v5udH1K/SHMbqkc9iCv X-Gm-Gg: AR+sD10U0IQMyT1HfowZS9Kl6UkFf6aHDBHmYl0E5yYCF2yrR0IfZtSu8O6bUmoi7Z3 mcXEmB2HmSVAi3OSLPpDkFpRrYJn7gEs5SrNxJzX0FYPCXO0MITCoSjMBt0262Tkwop4q5pClfg +ZE/R8dSC0sRoDK9dxDskWW4QVU33nW4b3MlaXw86m/b20qG4swcIhRO83ZWQEjo1P5qS5aEQsp UEENyy8tZEd1EEFpRX8wSPXYNcnPS+/Pc/yQCxAjP9D00FoWAtmVQT8wDOxHLEEJ+GPSMKo8s4+ RaQgMs9UQpFt+f1XbfJ7cvyydIQWtYFRhI4mBZmgHOYyxyLnFq3VbHpqdR4Y4MsyL42OrZinPdg NNRFbceNg0OT0wQaz3RbT28GYrSLcq/4546LEhbv2G20kAjaAj4XCRVxm6LJ+XrrFO1iC6RwiVn 9aM1qsdKVM+tUDi+Mue4Uphocf/s2D4WLkhcSmw7E7H5JLrA6JhTCgHMAC+2l9aXoJ4TId5wh1P r5u80qA8gYWUow9g9yHAcH7+cI7UxuP8/ttVmkrkyGc4L5B X-Received: by 2002:a05:6000:46c9:b0:47f:8a21:b760 with SMTP id ffacd0b85a97d-47fe81a46afmr3768483f8f.4.1785822915144; Mon, 03 Aug 2026 22:55:15 -0700 (PDT) Received: from bdtpg (ec2-15-237-197-144.eu-west-3.compute.amazonaws.com. [15.237.197.144]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-47fd458adc9sm42475511f8f.27.2026.08.03.22.55.14 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 03 Aug 2026 22:55:14 -0700 (PDT) Date: Tue, 4 Aug 2026 05:55:12 +0000 From: Bertrand Drouvot To: Amit Kapila Cc: "Zhijie Hou (Fujitsu)" , Dilip Kumar , "Hayato Kuroda (Fujitsu)" , "pgsql-hackers@lists.postgresql.org" Subject: Re: Re-read subscription state after lock in AlterSubscription Message-ID: References: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="1HOA+h/avFjxXHxm" Content-Disposition: inline In-Reply-To: List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --1HOA+h/avFjxXHxm Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Hi, On Mon, Jul 06, 2026 at 02:53:39PM +0000, Bertrand Drouvot wrote: > Hi, > > On Mon, Jul 06, 2026 at 03:07:24PM +0530, Amit Kapila wrote: > > DROP SUBSCRIPTION however has its own dedicated code path and does not go through > get_object_address(): 0003 adds the retry loop for it. And if DROP already uses > the retry loop then ALTER should probably use it too (also done in 0003 and 0004). Mandatory rebase attached. Regards, -- Bertrand Drouvot PostgreSQL Contributors Team RDS Open Source Databases Amazon Web Services: https://aws.amazon.com --1HOA+h/avFjxXHxm Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v5-0001-Re-read-subscription-state-after-lock-in-AlterSub.patch" From 4ff620ab3401573443fec13488386f84c896533b Mon Sep 17 00:00:00 2001 From: Bertrand Drouvot Date: Fri, 3 Jul 2026 12:28:42 +0000 Subject: [PATCH v5 1/4] Re-read subscription state after lock in AlterSubscription AlterSubscription() reads the subscription's catalog state via GetSubscription() before acquiring AccessExclusiveLock on the subscription object. A concurrent session that commits a DROP or ALTER between the read and the lock acquisition leaves the other session acting with stale information once it unblocks. Fix by moving the GetSubscription() call, the password_required privilege check, and the local variable assignments to after LockSharedObject(), with a re-read of the subscription tuple to ensure we operate on current catalog state. Remark: The ownership check is intentionally not re-done after the lock because AlterSubscriptionOwner() does not take AccessExclusiveLock on the subscription object: it only takes RowExclusiveLock on the pg_subscription catalog table. This means ownership can change regardless of our lock, making a re-check after lock acquisition pointless. The existing "tuple concurrently updated" error from CatalogTupleUpdate() already provides a protection if ownership changes concurrently. Author: Bertrand Drouvot Reviewed-by: Dilip Kumar Reviewed-by: Hayato Kuroda (Fujitsu) Reviewed-by: Zhijie Hou Reviewed-by: Amit Kapila Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg --- src/backend/commands/subscriptioncmds.c | 30 +++++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) 100.0% src/backend/commands/ diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index d3eef2a3efa..a2973aa229e 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -1728,6 +1728,25 @@ 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 @@ -1737,11 +1756,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, */ sub = GetSubscription(subid, false, orig_conninfo_needed, false); - retain_dead_tuples = sub->retaindeadtuples; - origin = sub->origin; - max_retention = sub->maxretention; - retention_active = sub->retentionactive; - /* * Don't allow non-superuser modification of a subscription with * password_required=false. @@ -1752,8 +1766,10 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, errmsg("password_required=false is superuser-only"), errhint("Subscriptions with the password_required option set to false may only be created or modified by the superuser."))); - /* Lock the subscription so nobody else can do anything with it. */ - LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock); + retain_dead_tuples = sub->retaindeadtuples; + origin = sub->origin; + max_retention = sub->maxretention; + retention_active = sub->retentionactive; /* Form a new tuple. */ memset(values, 0, sizeof(values)); -- 2.34.1 --1HOA+h/avFjxXHxm Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v5-0002-Re-read-subscription-state-after-lock-in-DropSubs.patch" From 483d4cc028dab24cdfb46fdec76a1e12d78bf1e7 Mon Sep 17 00:00:00 2001 From: Bertrand Drouvot Date: Fri, 3 Jul 2026 11:54:29 +0000 Subject: [PATCH v5 2/4] Re-read subscription state after lock in DropSubscription Similarly to what has been done for AlterSubscription() in XXXX, re-read the subscription tuple after LockSharedObject() in DropSubscription(). A concurrent DROP or ALTER may have committed while we were waiting for the lock. Without a re-read, DropSubscription would deal with invalid data, which currently produces a confusing "tuple concurrently updated" elog() from CatalogTupleDelete(). Author: Bertrand Drouvot Reviewed-by: Zhijie Hou Reviewed-by: Amit Kapila Discussion: https://postgr.es/m/akZUpiDa1UfmzYxL%40bdtpg --- src/backend/commands/subscriptioncmds.c | 40 ++++++++++++++++++------- 1 file changed, 29 insertions(+), 11 deletions(-) 100.0% src/backend/commands/ diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index a2973aa229e..9bb9fa90503 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -2601,25 +2601,15 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) return; } - datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, - Anum_pg_subscription_subconninfo, &isnull); - if (!isnull) - subconninfo = TextDatumGetCString(datum); - form = (Form_pg_subscription) GETSTRUCT(tup); subid = form->oid; - subowner = form->subowner; - subserver = form->subserver; - subconflictlogrelid = form->subconflictlogrelid; - must_use_password = !superuser_arg(subowner) && form->subpasswordrequired; /* 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); + ReleaseSysCache(tup); /* * Lock the subscription so nobody else can do anything with it (including @@ -2627,6 +2617,34 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) */ LockSharedObject(SubscriptionRelationId, subid, 0, AccessExclusiveLock); + /* DROP hook for the subscription being removed */ + InvokeObjectDropHook(SubscriptionRelationId, subid, 0); + + /* + * 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)); + + if (!HeapTupleIsValid(tup)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("subscription \"%s\" does not exist", + stmt->subname))); + + form = (Form_pg_subscription) GETSTRUCT(tup); + subowner = form->subowner; + subserver = form->subserver; + subconflictlogrelid = form->subconflictlogrelid; + must_use_password = !superuser_arg(subowner) && form->subpasswordrequired; + + datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subconninfo, &isnull); + if (!isnull) + subconninfo = TextDatumGetCString(datum); + else + subconninfo = NULL; + /* Get subname */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subname); -- 2.34.1 --1HOA+h/avFjxXHxm Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v5-0003-Add-invalidation-based-retry-loop-for-Alter-Drop-.patch" From cc7334ab73d69e9b289f69b905a416b8caf30c75 Mon Sep 17 00:00:00 2001 From: Bertrand Drouvot Date: Mon, 6 Jul 2026 04:44:13 +0000 Subject: [PATCH v5 3/4] Add invalidation-based retry loop for Alter/Drop Subscription 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 Reviewed-by: Dilip Kumar Reviewed-by: Hayato Kuroda (Fujitsu) 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 9bb9fa90503..4acefbdcf7a 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -51,6 +51,7 @@ #include "replication/worker_internal.h" #include "storage/lmgr.h" #include "storage/lock.h" +#include "storage/sinval.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/guc.h" @@ -1634,23 +1635,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) @@ -1728,25 +1773,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 @@ -2601,38 +2627,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 --1HOA+h/avFjxXHxm Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v5-0004-Add-invalidation-based-retry-loop-for-AlterPublic.patch" From c84584a8d74f00b3acaf249b5f8e666d2c24075f Mon Sep 17 00:00:00 2001 From: Bertrand Drouvot Date: Fri, 3 Jul 2026 14:46:42 +0000 Subject: [PATCH v5 4/4] Add invalidation-based retry loop for AlterPublication 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 Reviewed-by: Dilip Kumar Reviewed-by: Hayato Kuroda (Fujitsu) 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 --1HOA+h/avFjxXHxm--