Lock before setting relhassubclass on RELKIND_PARTITIONED_INDEX.
authorNoah Misch <[email protected]>
Fri, 28 Jun 2024 02:21:05 +0000 (19:21 -0700)
committerNoah Misch <[email protected]>
Fri, 28 Jun 2024 02:21:05 +0000 (19:21 -0700)
Commit 5b562644fec696977df4a82790064e8287927891 added a comment that
SetRelationHasSubclass() callers must hold this lock.  When commit
17f206fbc824d2b4b14480199ca9ff7dea417eda extended use of this column to
partitioned indexes, it didn't take the lock.  As the latter commit
message mentioned, we currently never reset a partitioned index to
relhassubclass=f.  That largely avoids harm from the lock omission.  The
cause for fixing this now is to unblock introducing a rule about locks
required to heap_update() a pg_class row.  This might cause more
deadlocks.  It gives minor user-visible benefits:

- If an ALTER INDEX SET TABLESPACE runs concurrently with ALTER TABLE
  ATTACH PARTITION or CREATE PARTITION OF, one transaction blocks
  instead of failing with "tuple concurrently updated".  (Many cases of
  DDL concurrency still fail that way.)

- Match ALTER INDEX ATTACH PARTITION in choosing to lock the index.

While not user-visible today, we'll need this if we ever make something
set the flag to false for a partitioned index, like ANALYZE does today
for tables.  Back-patch to v12 (all supported versions), the plan for
the commit relying on the new rule.  In back branches, add
LockOrStrongerHeldByMe() instead of adding a LockHeldByMe() parameter.

Reviewed (in an earlier version) by Robert Haas.

Discussion: https://postgr.es/m/20240611024525[email protected]

src/backend/catalog/index.c
src/backend/commands/indexcmds.c
src/backend/commands/tablecmds.c
src/backend/storage/lmgr/lmgr.c
src/backend/storage/lmgr/lock.c
src/include/storage/lmgr.h
src/include/storage/lock.h

index 55fdde4b242ae908d3384c37b1e4406b046cf1fd..a819b4197cee703ae3f56ca51e0b3382de9e5f1e 100644 (file)
@@ -1058,6 +1058,7 @@ index_create(Relation heapRelation,
    if (OidIsValid(parentIndexRelid))
    {
        StoreSingleInheritance(indexRelationId, parentIndexRelid, 1);
+       LockRelationOid(parentIndexRelid, ShareUpdateExclusiveLock);
        SetRelationHasSubclass(parentIndexRelid, true);
    }
 
index 309389e20d2189afd2274af8b16f476e8f065179..2caab88aa58024ae8ca766eeb45fb8e9fa415ec8 100644 (file)
@@ -4355,7 +4355,10 @@ IndexSetParentIndex(Relation partitionIdx, Oid parentOid)
 
    /* set relhassubclass if an index partition has been added to the parent */
    if (OidIsValid(parentOid))
+   {
+       LockRelationOid(parentOid, ShareUpdateExclusiveLock);
        SetRelationHasSubclass(parentOid, true);
+   }
 
    /* set relispartition correctly on the partition */
    update_relispartition(partRelid, OidIsValid(parentOid));
index 66cda26a25f6e294b02d3c777604e18fb9a22173..8fcb188323425a931006b2a079b76f3fc3b31502 100644 (file)
@@ -3483,8 +3483,15 @@ findAttrByName(const char *attributeName, const List *columns)
  * SetRelationHasSubclass
  *     Set the value of the relation's relhassubclass field in pg_class.
  *
- * NOTE: caller must be holding an appropriate lock on the relation.
- * ShareUpdateExclusiveLock is sufficient.
+ * It's always safe to set this field to true, because all SQL commands are
+ * ready to see true and then find no children.  On the other hand, commands
+ * generally assume zero children if this is false.
+ *
+ * Caller must hold any self-exclusive lock until end of transaction.  If the
+ * new value is false, caller must have acquired that lock before reading the
+ * evidence that justified the false value.  That way, it properly waits if
+ * another backend is simultaneously concluding no need to change the tuple
+ * (new and old values are true).
  *
  * NOTE: an important side-effect of this operation is that an SI invalidation
  * message is sent out to all backends --- including me --- causing plans
@@ -3499,6 +3506,11 @@ SetRelationHasSubclass(Oid relationId, bool relhassubclass)
    HeapTuple   tuple;
    Form_pg_class classtuple;
 
+   Assert(CheckRelationOidLockedByMe(relationId,
+                                     ShareUpdateExclusiveLock, false) ||
+          CheckRelationOidLockedByMe(relationId,
+                                     ShareRowExclusiveLock, true));
+
    /*
     * Fetch a modifiable copy of the tuple, modify it, update pg_class.
     */
index fe3cda2f88a72925700539c705c7fa8ab26d9a08..094522acb414d86376360424f7121c6b7f9dc4b6 100644 (file)
@@ -335,32 +335,22 @@ CheckRelationLockedByMe(Relation relation, LOCKMODE lockmode, bool orstronger)
                         relation->rd_lockInfo.lockRelId.dbId,
                         relation->rd_lockInfo.lockRelId.relId);
 
-   if (LockHeldByMe(&tag, lockmode))
-       return true;
+   return LockHeldByMe(&tag, lockmode, orstronger);
+}
 
-   if (orstronger)
-   {
-       LOCKMODE    slockmode;
+/*
+ *     CheckRelationOidLockedByMe
+ *
+ * Like the above, but takes an OID as argument.
+ */
+bool
+CheckRelationOidLockedByMe(Oid relid, LOCKMODE lockmode, bool orstronger)
+{
+   LOCKTAG     tag;
 
-       for (slockmode = lockmode + 1;
-            slockmode <= MaxLockMode;
-            slockmode++)
-       {
-           if (LockHeldByMe(&tag, slockmode))
-           {
-#ifdef NOT_USED
-               /* Sometimes this might be useful for debugging purposes */
-               elog(WARNING, "lock mode %s substituted for %s on relation %s",
-                    GetLockmodeName(tag.locktag_lockmethodid, slockmode),
-                    GetLockmodeName(tag.locktag_lockmethodid, lockmode),
-                    RelationGetRelationName(relation));
-#endif
-               return true;
-           }
-       }
-   }
+   SetLocktagRelationOid(&tag, relid);
 
-   return false;
+   return LockHeldByMe(&tag, lockmode, orstronger);
 }
 
 /*
index f68c595c8a41f6e2ffac4d64d5e54a9e7059437a..0400a5077799ffd11fd8a025fc3fd9490bb62b55 100644 (file)
@@ -578,11 +578,17 @@ DoLockModesConflict(LOCKMODE mode1, LOCKMODE mode2)
 }
 
 /*
- * LockHeldByMe -- test whether lock 'locktag' is held with mode 'lockmode'
- *     by the current transaction
+ * LockHeldByMe -- test whether lock 'locktag' is held by the current
+ *     transaction
+ *
+ * Returns true if current transaction holds a lock on 'tag' of mode
+ * 'lockmode'.  If 'orstronger' is true, a stronger lockmode is also OK.
+ * ("Stronger" is defined as "numerically higher", which is a bit
+ * semantically dubious but is OK for the purposes we use this for.)
  */
 bool
-LockHeldByMe(const LOCKTAG *locktag, LOCKMODE lockmode)
+LockHeldByMe(const LOCKTAG *locktag,
+            LOCKMODE lockmode, bool orstronger)
 {
    LOCALLOCKTAG localtag;
    LOCALLOCK  *locallock;
@@ -598,7 +604,23 @@ LockHeldByMe(const LOCKTAG *locktag, LOCKMODE lockmode)
                                          &localtag,
                                          HASH_FIND, NULL);
 
-   return (locallock && locallock->nLocks > 0);
+   if (locallock && locallock->nLocks > 0)
+       return true;
+
+   if (orstronger)
+   {
+       LOCKMODE    slockmode;
+
+       for (slockmode = lockmode + 1;
+            slockmode <= MaxLockMode;
+            slockmode++)
+       {
+           if (LockHeldByMe(locktag, slockmode, false))
+               return true;
+       }
+   }
+
+   return false;
 }
 
 #ifdef USE_ASSERT_CHECKING
index 22b7856ef1fa0b993cc69559cd0622a0533a6521..ce15125ac3bc0d0acd77ccce4a7fee5fadd3226d 100644 (file)
@@ -48,6 +48,8 @@ extern bool ConditionalLockRelation(Relation relation, LOCKMODE lockmode);
 extern void UnlockRelation(Relation relation, LOCKMODE lockmode);
 extern bool CheckRelationLockedByMe(Relation relation, LOCKMODE lockmode,
                                    bool orstronger);
+extern bool CheckRelationOidLockedByMe(Oid relid, LOCKMODE lockmode,
+                                      bool orstronger);
 extern bool LockHasWaitersRelation(Relation relation, LOCKMODE lockmode);
 
 extern void LockRelationIdForSession(LockRelId *relid, LOCKMODE lockmode);
index 0017d4b8680072970de39c586f64fb1c688d1630..cc1f6e78c396c1a6c590d167e08a898e54bc3d9f 100644 (file)
@@ -567,7 +567,8 @@ extern void LockReleaseAll(LOCKMETHODID lockmethodid, bool allLocks);
 extern void LockReleaseSession(LOCKMETHODID lockmethodid);
 extern void LockReleaseCurrentOwner(LOCALLOCK **locallocks, int nlocks);
 extern void LockReassignCurrentOwner(LOCALLOCK **locallocks, int nlocks);
-extern bool LockHeldByMe(const LOCKTAG *locktag, LOCKMODE lockmode);
+extern bool LockHeldByMe(const LOCKTAG *locktag,
+                        LOCKMODE lockmode, bool orstronger);
 #ifdef USE_ASSERT_CHECKING
 extern HTAB *GetLockMethodLocalHash(void);
 #endif