For inplace update, send nontransactional invalidations.
authorNoah Misch <[email protected]>
Wed, 17 Dec 2025 00:13:54 +0000 (16:13 -0800)
committerNoah Misch <[email protected]>
Wed, 17 Dec 2025 00:13:56 +0000 (16:13 -0800)
The inplace update survives ROLLBACK.  The inval didn't, so another
backend's DDL could then update the row without incorporating the
inplace update.  In the test this fixes, a mix of CREATE INDEX and ALTER
TABLE resulted in a table with an index, yet relhasindex=f.  That is a
source of index corruption.

Back-patch to v14 - v17.  This is a back-patch of commits:

243e9b40f1b2dd09d6e5bf91ebf6e822a2cd3704
  (main change, on master, before v18 branched)
0bada39c83a150079567a6e97b1a25a198f30ea3
  (defect fix, on master, before v18 branched)
bae8ca82fd00603ebafa0658640d6e4dfe20af92
  (cosmetics from post-commit review, on REL_18_STABLE)

It reverses commit c1099dd745b0135960895caa8892a1873ac6cbe5, my revert
of the original back-patch of 243e9b4.

This back-patch omits the non-comment heap_decode() changes.  I find
those changes removed harmless code that was last necessary in v13.  See
discussion thread for details.  The back branches aren't the place to
remove such code.

Like the original back-patch, this doesn't change WAL, because these
branches use end-of-recovery SIResetAll().  All branches change the ABI
of extern function PrepareToInvalidateCacheTuple().  No PGXN extension
calls that, and there's no apparent use case in extensions.  Expect
".abi-compliance-history" edits to follow.

Reviewed-by: Paul A Jungwirth <[email protected]>
Reviewed-by: Surya Poondla <[email protected]>
Reviewed-by: Ilyasov Ian <[email protected]>
Reviewed-by: Nitin Motiani <[email protected]> (in earlier versions)
Reviewed-by: Andres Freund <[email protected]> (in earlier versions)
Discussion: https://postgr.es/m/20240523000548[email protected]
Backpatch-through: 14-17

13 files changed:
src/backend/access/heap/README.tuplock
src/backend/access/heap/heapam.c
src/backend/access/transam/xact.c
src/backend/catalog/index.c
src/backend/replication/logical/decode.c
src/backend/utils/cache/catcache.c
src/backend/utils/cache/inval.c
src/backend/utils/cache/syscache.c
src/include/utils/catcache.h
src/include/utils/inval.h
src/test/isolation/expected/inplace-inval.out
src/test/isolation/specs/inplace-inval.spec
src/tools/pgindent/typedefs.list

index 750684d33989c090dbcc8e6bae6d44c23be80d12..ad835ff4820afef2c453c712c4b2381655ee6d04 100644 (file)
@@ -201,3 +201,35 @@ wider than four bytes, and current readers don't need consistency across
 fields.  Hence, they get by with just fetching each field once.  XXX such a
 caller may also read a value that has not reached WAL; see
 systable_inplace_update_finish().
+
+During logical decoding, caches reflect an inplace update no later than the
+next XLOG_XACT_INVALIDATIONS.  That record witnesses the end of a command.
+Tuples of its cmin are then visible to decoding, as are inplace updates of any
+lower LSN.  Inplace updates of a higher LSN may also be visible, even if those
+updates would have been invisible to a non-historic snapshot matching
+decoding's historic snapshot.  (In other words, decoding may see inplace
+updates that were not visible to a similar snapshot taken during original
+transaction processing.)  That's a consequence of inplace update violating
+MVCC: there are no snapshot-specific versions of inplace-updated values.  This
+all makes it hard to reason about inplace-updated column reads during logical
+decoding, but the behavior does suffice for relhasindex.  A relhasindex=t in
+CREATE INDEX becomes visible no later than the new pg_index row.  While it may
+be visible earlier, that's harmless.  Finding zero indexes despite
+relhasindex=t is normal in more cases than this, e.g. after DROP INDEX.
+Example of a case that meaningfully reacts to the inplace inval:
+
+CREATE TABLE cat (c int) WITH (user_catalog_table = true);
+CREATE TABLE normal (d int);
+...
+CREATE INDEX ON cat (c)\; INSERT INTO normal VALUES (1);
+
+If the output plugin reads "cat" during decoding of the INSERT, it's fair to
+want that read to see relhasindex=t and use the new index.
+
+An alternative would be to have decoding of XLOG_HEAP_INPLACE immediately
+execute its invals.  That would behave more like invals during original
+transaction processing.  It would remove the decoding-specific delay in e.g. a
+decoding plugin witnessing a relfrozenxid change.  However, a good use case
+for that is unlikely, since the plugin would still witness relfrozenxid
+changes prematurely.  Hence, inplace update takes the trivial approach of
+delegating to XLOG_XACT_INVALIDATIONS.
index 38ff22a7ed6cfe8a6d80706d081a2039f21420b1..bb393a2b3b855d2293e0da60e97449a887394667 100644 (file)
@@ -6307,6 +6307,19 @@ heap_inplace_lock(Relation relation,
 
    Assert(BufferIsValid(buffer));
 
+   /*
+    * Register shared cache invals if necessary.  Other sessions may finish
+    * inplace updates of this tuple between this step and LockTuple().  Since
+    * inplace updates don't change cache keys, that's harmless.
+    *
+    * While it's tempting to register invals only after confirming we can
+    * return true, the following obstacle precludes reordering steps that
+    * way.  Registering invals might reach a CatalogCacheInitializeCache()
+    * that locks "buffer".  That would hang indefinitely if running after our
+    * own LockBuffer().  Hence, we must register invals before LockBuffer().
+    */
+   CacheInvalidateHeapTupleInplace(relation, oldtup_ptr);
+
    LockTuple(relation, &oldtup.t_self, InplaceUpdateTupleLock);
    LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
 
@@ -6402,6 +6415,7 @@ heap_inplace_lock(Relation relation,
    if (!ret)
    {
        UnlockTuple(relation, &oldtup.t_self, InplaceUpdateTupleLock);
+       ForgetInplace_Inval();
        InvalidateCatalogSnapshot();
    }
    return ret;
@@ -6430,6 +6444,16 @@ heap_inplace_update_and_unlock(Relation relation,
    if (oldlen != newlen || htup->t_hoff != tuple->t_data->t_hoff)
        elog(ERROR, "wrong tuple length");
 
+   /*
+    * Unlink relcache init files as needed.  If unlinking, acquire
+    * RelCacheInitLock until after associated invalidations.  By doing this
+    * in advance, if we checkpoint and then crash between inplace
+    * XLogInsert() and inval, we don't rely on StartupXLOG() ->
+    * RelationCacheInitFileRemove().  That uses elevel==LOG, so replay would
+    * neglect to PANIC on EIO.
+    */
+   PreInplace_Inval();
+
    /* NO EREPORT(ERROR) from here till changes are logged */
    START_CRIT_SECTION();
 
@@ -6473,17 +6497,24 @@ heap_inplace_update_and_unlock(Relation relation,
        PageSetLSN(BufferGetPage(buffer), recptr);
    }
 
+   LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
+
+   /*
+    * Send invalidations to shared queue.  SearchSysCacheLocked1() assumes we
+    * do this before UnlockTuple().
+    */
+   AtInplace_Inval();
+
    END_CRIT_SECTION();
+   UnlockTuple(relation, &tuple->t_self, InplaceUpdateTupleLock);
 
-   heap_inplace_unlock(relation, oldtup, buffer);
+   AcceptInvalidationMessages();   /* local processing of just-sent inval */
 
    /*
-    * Send out shared cache inval if necessary.  Note that because we only
-    * pass the new version of the tuple, this mustn't be used for any
-    * operations that could change catcache lookup keys.  But we aren't
-    * bothering with index updates either, so that's true a fortiori.
-    *
-    * XXX ROLLBACK discards the invalidation.  See test inplace-inval.spec.
+    * Queue a transactional inval, for logical decoding and for third-party
+    * code that might have been relying on it since long before inplace
+    * update adopted immediate invalidation.  See README.tuplock section
+    * "Reading inplace-updated columns" for logical decoding details.
     */
    if (!IsBootstrapProcessingMode())
        CacheInvalidateHeapTuple(relation, tuple, NULL);
@@ -6498,6 +6529,7 @@ heap_inplace_unlock(Relation relation,
 {
    LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
    UnlockTuple(relation, &oldtup->t_self, InplaceUpdateTupleLock);
+   ForgetInplace_Inval();
 }
 
 /*
index 3f0f711307d9082407b648a4e38cf438efa24c1d..395650d6406dbfd5130c8db284220a5ff18c5def 100644 (file)
@@ -1276,14 +1276,24 @@ RecordTransactionCommit(void)
 
        /*
         * Transactions without an assigned xid can contain invalidation
-        * messages (e.g. explicit relcache invalidations or catcache
-        * invalidations for inplace updates); standbys need to process those.
-        * We can't emit a commit record without an xid, and we don't want to
-        * force assigning an xid, because that'd be problematic for e.g.
-        * vacuum.  Hence we emit a bespoke record for the invalidations. We
-        * don't want to use that in case a commit record is emitted, so they
-        * happen synchronously with commits (besides not wanting to emit more
-        * WAL records).
+        * messages.  While inplace updates do this, this is not known to be
+        * necessary; see comment at inplace CacheInvalidateHeapTuple().
+        * Extensions might still rely on this capability, and standbys may
+        * need to process those invals.  We can't emit a commit record
+        * without an xid, and we don't want to force assigning an xid,
+        * because that'd be problematic for e.g. vacuum.  Hence we emit a
+        * bespoke record for the invalidations. We don't want to use that in
+        * case a commit record is emitted, so they happen synchronously with
+        * commits (besides not wanting to emit more WAL records).
+        *
+        * XXX Every known use of this capability is a defect.  Since an XID
+        * isn't controlling visibility of the change that prompted invals,
+        * other sessions need the inval even if this transactions aborts.
+        *
+        * ON COMMIT DELETE ROWS does a nontransactional index_build(), which
+        * queues a relcache inval, including in transactions without an xid
+        * that had read the (empty) table.  Standbys don't need any ON COMMIT
+        * DELETE ROWS invals, but we've not done the work to withhold them.
         */
        if (nmsgs != 0)
        {
index 33405bbb21cc639dc6a9c42984c2722e12bf26ba..359bf34f48e94f4e4793cf0004b529f0f7fa1363 100644 (file)
@@ -2911,12 +2911,19 @@ index_update_stats(Relation rel,
    if (dirty)
    {
        systable_inplace_update_finish(state, tuple);
-       /* the above sends a cache inval message */
+       /* the above sends transactional and immediate cache inval messages */
    }
    else
    {
        systable_inplace_update_cancel(state);
-       /* no need to change tuple, but force relcache inval anyway */
+
+       /*
+        * While we didn't change relhasindex, CREATE INDEX needs a
+        * transactional inval for when the new index's catalog rows become
+        * visible.  Other CREATE INDEX and REINDEX code happens to also queue
+        * this inval, but keep this in case rare callers rely on this part of
+        * our API contract.
+        */
        CacheInvalidateRelcacheByTuple(tuple);
    }
 
index 8c27c53285777fffc2621e3bd80fc0f7139ff86c..c6858da9b0bd593acecd5161f28da0f73f0a9bce 100644 (file)
@@ -559,20 +559,13 @@ DecodeHeapOp(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
            /*
             * Inplace updates are only ever performed on catalog tuples and
             * can, per definition, not change tuple visibility.  Since we
-            * don't decode catalog tuples, we're not interested in the
+            * also don't decode catalog tuples, we're not interested in the
             * record's contents.
-            *
-            * In-place updates can be used either by XID-bearing transactions
-            * (e.g.  in CREATE INDEX CONCURRENTLY) or by XID-less
-            * transactions (e.g.  VACUUM).  In the former case, the commit
-            * record will include cache invalidations, so we mark the
-            * transaction as catalog modifying here. Currently that's
-            * redundant because the commit will do that as well, but once we
-            * support decoding in-progress relations, this will be important.
             */
            if (!TransactionIdIsValid(xid))
                break;
 
+           /* PostgreSQL 13 was the last to need these actions. */
            SnapBuildProcessChange(builder, xid, buf->origptr);
            ReorderBufferXidSetCatalogChanges(ctx->reorder, xid, buf->origptr);
            break;
index 6cb6885bb9bd4d780e01924e33aeb759157b197f..68905b006e20d8a2698a0df8ac9b52c8070a2d99 100644 (file)
@@ -2190,7 +2190,8 @@ void
 PrepareToInvalidateCacheTuple(Relation relation,
                              HeapTuple tuple,
                              HeapTuple newtuple,
-                             void (*function) (int, uint32, Oid))
+                             void (*function) (int, uint32, Oid, void *),
+                             void *context)
 {
    slist_iter  iter;
    Oid         reloid;
@@ -2231,7 +2232,7 @@ PrepareToInvalidateCacheTuple(Relation relation,
        hashvalue = CatalogCacheComputeTupleHashValue(ccp, ccp->cc_nkeys, tuple);
        dbid = ccp->cc_relisshared ? (Oid) 0 : MyDatabaseId;
 
-       (*function) (ccp->id, hashvalue, dbid);
+       (*function) (ccp->id, hashvalue, dbid, context);
 
        if (newtuple)
        {
@@ -2240,7 +2241,7 @@ PrepareToInvalidateCacheTuple(Relation relation,
            newhashvalue = CatalogCacheComputeTupleHashValue(ccp, ccp->cc_nkeys, newtuple);
 
            if (newhashvalue != hashvalue)
-               (*function) (ccp->id, newhashvalue, dbid);
+               (*function) (ccp->id, newhashvalue, dbid, context);
        }
    }
 }
index 9d764ad81f99e1befa58eb7e6f5c9bd353778407..879340831f1e2e3adea22b89440029acd741b445 100644 (file)
  * worth trying to avoid sending such inval traffic in the future, if those
  * problems can be overcome cheaply.
  *
+ * When making a nontransactional change to a cacheable object, we must
+ * likewise send the invalidation immediately, before ending the change's
+ * critical section.  This includes inplace heap updates, relmap, and smgr.
+ *
  * When wal_level=logical, write invalidations into WAL at each command end to
  * support the decoding of the in-progress transactions.  See
  * CommandEndInvalidationMessages.
@@ -154,7 +158,7 @@ typedef struct InvalidationListHeader
 } InvalidationListHeader;
 
 /*----------------
- * Invalidation info is divided into two lists:
+ * Transactional invalidation info is divided into two lists:
  * 1) events so far in current command, not yet reflected to caches.
  * 2) events in previous commands of current transaction; these have
  *    been reflected to local caches, and must be either broadcast to
@@ -170,26 +174,36 @@ typedef struct InvalidationListHeader
  *----------------
  */
 
-typedef struct TransInvalidationInfo
+/* fields common to both transactional and inplace invalidation */
+typedef struct InvalidationInfo
 {
-   /* Back link to parent transaction's info */
-   struct TransInvalidationInfo *parent;
-
-   /* Subtransaction nesting depth */
-   int         my_level;
-
    /* head of current-command event list */
    InvalidationListHeader CurrentCmdInvalidMsgs;
 
+   /* init file must be invalidated? */
+   bool        RelcacheInitFileInval;
+} InvalidationInfo;
+
+/* subclass adding fields specific to transactional invalidation */
+typedef struct TransInvalidationInfo
+{
+   /* Base class */
+   struct InvalidationInfo ii;
+
    /* head of previous-commands event list */
    InvalidationListHeader PriorCmdInvalidMsgs;
 
-   /* init file must be invalidated? */
-   bool        RelcacheInitFileInval;
+   /* Back link to parent transaction's info */
+   struct TransInvalidationInfo *parent;
+
+   /* Subtransaction nesting depth */
+   int         my_level;
 } TransInvalidationInfo;
 
 static TransInvalidationInfo *transInvalInfo = NULL;
 
+static InvalidationInfo *inplaceInvalInfo = NULL;
+
 static SharedInvalidationMessage *SharedInvalidMessagesArray;
 static int numSharedInvalidMessagesArray;
 static int maxSharedInvalidMessagesArray;
@@ -505,9 +519,12 @@ ProcessInvalidationMessagesMulti(InvalidationListHeader *hdr,
 static void
 RegisterCatcacheInvalidation(int cacheId,
                             uint32 hashValue,
-                            Oid dbId)
+                            Oid dbId,
+                            void *context)
 {
-   AddCatcacheInvalidationMessage(&transInvalInfo->CurrentCmdInvalidMsgs,
+   InvalidationInfo *info = (InvalidationInfo *) context;
+
+   AddCatcacheInvalidationMessage(&info->CurrentCmdInvalidMsgs,
                                   cacheId, hashValue, dbId);
 }
 
@@ -517,10 +534,9 @@ RegisterCatcacheInvalidation(int cacheId,
  * Register an invalidation event for all catcache entries from a catalog.
  */
 static void
-RegisterCatalogInvalidation(Oid dbId, Oid catId)
+RegisterCatalogInvalidation(InvalidationInfo *info, Oid dbId, Oid catId)
 {
-   AddCatalogInvalidationMessage(&transInvalInfo->CurrentCmdInvalidMsgs,
-                                 dbId, catId);
+   AddCatalogInvalidationMessage(&info->CurrentCmdInvalidMsgs, dbId, catId);
 }
 
 /*
@@ -529,10 +545,9 @@ RegisterCatalogInvalidation(Oid dbId, Oid catId)
  * As above, but register a relcache invalidation event.
  */
 static void
-RegisterRelcacheInvalidation(Oid dbId, Oid relId)
+RegisterRelcacheInvalidation(InvalidationInfo *info, Oid dbId, Oid relId)
 {
-   AddRelcacheInvalidationMessage(&transInvalInfo->CurrentCmdInvalidMsgs,
-                                  dbId, relId);
+   AddRelcacheInvalidationMessage(&info->CurrentCmdInvalidMsgs, dbId, relId);
 
    /*
     * Most of the time, relcache invalidation is associated with system
@@ -549,7 +564,7 @@ RegisterRelcacheInvalidation(Oid dbId, Oid relId)
     * as well.  Also zap when we are invalidating whole relcache.
     */
    if (relId == InvalidOid || RelationIdIsInInitFile(relId))
-       transInvalInfo->RelcacheInitFileInval = true;
+       info->RelcacheInitFileInval = true;
 }
 
 /*
@@ -559,10 +574,9 @@ RegisterRelcacheInvalidation(Oid dbId, Oid relId)
  * Only needed for catalogs that don't have catcaches.
  */
 static void
-RegisterSnapshotInvalidation(Oid dbId, Oid relId)
+RegisterSnapshotInvalidation(InvalidationInfo *info, Oid dbId, Oid relId)
 {
-   AddSnapshotInvalidationMessage(&transInvalInfo->CurrentCmdInvalidMsgs,
-                                  dbId, relId);
+   AddSnapshotInvalidationMessage(&info->CurrentCmdInvalidMsgs, dbId, relId);
 }
 
 /*
@@ -752,14 +766,18 @@ AcceptInvalidationMessages(void)
  * PrepareInvalidationState
  *     Initialize inval lists for the current (sub)transaction.
  */
-static void
+static InvalidationInfo *
 PrepareInvalidationState(void)
 {
    TransInvalidationInfo *myInfo;
 
+   Assert(IsTransactionState());
+   /* Can't queue transactional message while collecting inplace messages. */
+   Assert(inplaceInvalInfo == NULL);
+
    if (transInvalInfo != NULL &&
        transInvalInfo->my_level == GetCurrentTransactionNestLevel())
-       return;
+       return (InvalidationInfo *) transInvalInfo;
 
    myInfo = (TransInvalidationInfo *)
        MemoryContextAllocZero(TopTransactionContext,
@@ -775,6 +793,29 @@ PrepareInvalidationState(void)
           myInfo->my_level > transInvalInfo->my_level);
 
    transInvalInfo = myInfo;
+   return (InvalidationInfo *) myInfo;
+}
+
+/*
+ * PrepareInplaceInvalidationState
+ *     Initialize inval data for an inplace update.
+ *
+ * See previous function for more background.
+ */
+static InvalidationInfo *
+PrepareInplaceInvalidationState(void)
+{
+   InvalidationInfo *myInfo;
+
+   Assert(IsTransactionState());
+   /* limit of one inplace update under assembly */
+   Assert(inplaceInvalInfo == NULL);
+
+   /* gone after WAL insertion CritSection ends, so use current context */
+   myInfo = (InvalidationInfo *) palloc0(sizeof(InvalidationInfo));
+
+   inplaceInvalInfo = myInfo;
+   return myInfo;
 }
 
 /*
@@ -870,7 +911,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
     * after we send the SI messages.  However, we need not do anything unless
     * we committed.
     */
-   *RelcacheInitFileInval = transInvalInfo->RelcacheInitFileInval;
+   *RelcacheInitFileInval = transInvalInfo->ii.RelcacheInitFileInval;
 
    /*
     * Walk through TransInvalidationInfo to collect all the messages into a
@@ -882,7 +923,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
     */
    oldcontext = MemoryContextSwitchTo(CurTransactionContext);
 
-   ProcessInvalidationMessagesMulti(&transInvalInfo->CurrentCmdInvalidMsgs,
+   ProcessInvalidationMessagesMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
                                     MakeSharedInvalidMessagesArray);
    ProcessInvalidationMessagesMulti(&transInvalInfo->PriorCmdInvalidMsgs,
                                     MakeSharedInvalidMessagesArray);
@@ -972,7 +1013,9 @@ ProcessCommittedInvalidationMessages(SharedInvalidationMessage *msgs,
 void
 AtEOXact_Inval(bool isCommit)
 {
-   /* Quick exit if no messages */
+   inplaceInvalInfo = NULL;
+
+   /* Quick exit if no transactional messages */
    if (transInvalInfo == NULL)
        return;
 
@@ -986,16 +1029,16 @@ AtEOXact_Inval(bool isCommit)
         * after we send the SI messages.  However, we need not do anything
         * unless we committed.
         */
-       if (transInvalInfo->RelcacheInitFileInval)
+       if (transInvalInfo->ii.RelcacheInitFileInval)
            RelationCacheInitFilePreInvalidate();
 
        AppendInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,
-                                  &transInvalInfo->CurrentCmdInvalidMsgs);
+                                  &transInvalInfo->ii.CurrentCmdInvalidMsgs);
 
        ProcessInvalidationMessagesMulti(&transInvalInfo->PriorCmdInvalidMsgs,
                                         SendSharedInvalidMessages);
 
-       if (transInvalInfo->RelcacheInitFileInval)
+       if (transInvalInfo->ii.RelcacheInitFileInval)
            RelationCacheInitFilePostInvalidate();
    }
    else
@@ -1010,6 +1053,57 @@ AtEOXact_Inval(bool isCommit)
    numSharedInvalidMessagesArray = 0;
 }
 
+/*
+ * PreInplace_Inval
+ *     Process queued-up invalidation before inplace update critical section.
+ *
+ * Tasks belong here if they are safe even if the inplace update does not
+ * complete.  Currently, this just unlinks a cache file, which can fail.  The
+ * sum of this and AtInplace_Inval() mirrors AtEOXact_Inval(isCommit=true).
+ */
+void
+PreInplace_Inval(void)
+{
+   Assert(CritSectionCount == 0);
+
+   if (inplaceInvalInfo && inplaceInvalInfo->RelcacheInitFileInval)
+       RelationCacheInitFilePreInvalidate();
+}
+
+/*
+ * AtInplace_Inval
+ *     Process queued-up invalidations after inplace update buffer mutation.
+ */
+void
+AtInplace_Inval(void)
+{
+   Assert(CritSectionCount > 0);
+
+   if (inplaceInvalInfo == NULL)
+       return;
+
+   ProcessInvalidationMessagesMulti(&inplaceInvalInfo->CurrentCmdInvalidMsgs,
+                                    SendSharedInvalidMessages);
+
+   if (inplaceInvalInfo->RelcacheInitFileInval)
+       RelationCacheInitFilePostInvalidate();
+
+   inplaceInvalInfo = NULL;
+   /* inplace doesn't use SharedInvalidMessagesArray */
+}
+
+/*
+ * ForgetInplace_Inval
+ *     Alternative to PreInplace_Inval()+AtInplace_Inval(): discard queued-up
+ *     invalidations.  This lets inplace update enumerate invalidations
+ *     optimistically, before locking the buffer.
+ */
+void
+ForgetInplace_Inval(void)
+{
+   inplaceInvalInfo = NULL;
+}
+
 /*
  * AtEOSubXact_Inval
  *     Process queued-up invalidation messages at end of subtransaction.
@@ -1032,9 +1126,20 @@ void
 AtEOSubXact_Inval(bool isCommit)
 {
    int         my_level;
-   TransInvalidationInfo *myInfo = transInvalInfo;
+   TransInvalidationInfo *myInfo;
 
-   /* Quick exit if no messages. */
+   /*
+    * Successful inplace update must clear this, but we clear it on abort.
+    * Inplace updates allocate this in CurrentMemoryContext, which has
+    * lifespan <= subtransaction lifespan.  Hence, don't free it explicitly.
+    */
+   if (isCommit)
+       Assert(inplaceInvalInfo == NULL);
+   else
+       inplaceInvalInfo = NULL;
+
+   /* Quick exit if no transactional messages. */
+   myInfo = transInvalInfo;
    if (myInfo == NULL)
        return;
 
@@ -1068,8 +1173,8 @@ AtEOSubXact_Inval(bool isCommit)
                                   &myInfo->PriorCmdInvalidMsgs);
 
        /* Pending relcache inval becomes parent's problem too */
-       if (myInfo->RelcacheInitFileInval)
-           myInfo->parent->RelcacheInitFileInval = true;
+       if (myInfo->ii.RelcacheInitFileInval)
+           myInfo->parent->ii.RelcacheInitFileInval = true;
 
        /* Pop the transaction state stack */
        transInvalInfo = myInfo->parent;
@@ -1116,7 +1221,7 @@ CommandEndInvalidationMessages(void)
    if (transInvalInfo == NULL)
        return;
 
-   ProcessInvalidationMessages(&transInvalInfo->CurrentCmdInvalidMsgs,
+   ProcessInvalidationMessages(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
                                LocalExecuteInvalidationMessage);
 
    /* WAL Log per-command invalidation messages for wal_level=logical */
@@ -1124,26 +1229,21 @@ CommandEndInvalidationMessages(void)
        LogLogicalInvalidations();
 
    AppendInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,
-                              &transInvalInfo->CurrentCmdInvalidMsgs);
+                              &transInvalInfo->ii.CurrentCmdInvalidMsgs);
 }
 
 
 /*
- * CacheInvalidateHeapTuple
- *     Register the given tuple for invalidation at end of command
- *     (ie, current command is creating or outdating this tuple).
- *     Also, detect whether a relcache invalidation is implied.
- *
- * For an insert or delete, tuple is the target tuple and newtuple is NULL.
- * For an update, we are called just once, with tuple being the old tuple
- * version and newtuple the new version.  This allows avoidance of duplicate
- * effort during an update.
+ * CacheInvalidateHeapTupleCommon
+ *     Common logic for end-of-command and inplace variants.
  */
-void
-CacheInvalidateHeapTuple(Relation relation,
-                        HeapTuple tuple,
-                        HeapTuple newtuple)
+static void
+CacheInvalidateHeapTupleCommon(Relation relation,
+                              HeapTuple tuple,
+                              HeapTuple newtuple,
+                              InvalidationInfo *(*prepare_callback) (void))
 {
+   InvalidationInfo *info;
    Oid         tupleRelId;
    Oid         databaseId;
    Oid         relationId;
@@ -1167,11 +1267,8 @@ CacheInvalidateHeapTuple(Relation relation,
    if (IsToastRelation(relation))
        return;
 
-   /*
-    * If we're not prepared to queue invalidation messages for this
-    * subtransaction level, get ready now.
-    */
-   PrepareInvalidationState();
+   /* Allocate any required resources. */
+   info = prepare_callback();
 
    /*
     * First let the catcache do its thing
@@ -1180,11 +1277,12 @@ CacheInvalidateHeapTuple(Relation relation,
    if (RelationInvalidatesSnapshotsOnly(tupleRelId))
    {
        databaseId = IsSharedRelation(tupleRelId) ? InvalidOid : MyDatabaseId;
-       RegisterSnapshotInvalidation(databaseId, tupleRelId);
+       RegisterSnapshotInvalidation(info, databaseId, tupleRelId);
    }
    else
        PrepareToInvalidateCacheTuple(relation, tuple, newtuple,
-                                     RegisterCatcacheInvalidation);
+                                     RegisterCatcacheInvalidation,
+                                     (void *) info);
 
    /*
     * Now, is this tuple one of the primary definers of a relcache entry? See
@@ -1257,7 +1355,48 @@ CacheInvalidateHeapTuple(Relation relation,
    /*
     * Yes.  We need to register a relcache invalidation event.
     */
-   RegisterRelcacheInvalidation(databaseId, relationId);
+   RegisterRelcacheInvalidation(info, databaseId, relationId);
+}
+
+/*
+ * CacheInvalidateHeapTuple
+ *     Register the given tuple for invalidation at end of command
+ *     (ie, current command is creating or outdating this tuple) and end of
+ *     transaction.  Also, detect whether a relcache invalidation is implied.
+ *
+ * For an insert or delete, tuple is the target tuple and newtuple is NULL.
+ * For an update, we are called just once, with tuple being the old tuple
+ * version and newtuple the new version.  This allows avoidance of duplicate
+ * effort during an update.
+ */
+void
+CacheInvalidateHeapTuple(Relation relation,
+                        HeapTuple tuple,
+                        HeapTuple newtuple)
+{
+   CacheInvalidateHeapTupleCommon(relation, tuple, newtuple,
+                                  PrepareInvalidationState);
+}
+
+/*
+ * CacheInvalidateHeapTupleInplace
+ *     Register the given tuple for nontransactional invalidation pertaining
+ *     to an inplace update.  Also, detect whether a relcache invalidation is
+ *     implied.
+ *
+ * Like CacheInvalidateHeapTuple(), but for inplace updates.
+ *
+ * Just before and just after the inplace update, the tuple's cache keys must
+ * match those in key_equivalent_tuple.  Cache keys consist of catcache lookup
+ * key columns and columns referencing pg_class.oid values,
+ * e.g. pg_constraint.conrelid, which would trigger relcache inval.
+ */
+void
+CacheInvalidateHeapTupleInplace(Relation relation,
+                               HeapTuple key_equivalent_tuple)
+{
+   CacheInvalidateHeapTupleCommon(relation, key_equivalent_tuple, NULL,
+                                  PrepareInplaceInvalidationState);
 }
 
 /*
@@ -1276,14 +1415,13 @@ CacheInvalidateCatalog(Oid catalogId)
 {
    Oid         databaseId;
 
-   PrepareInvalidationState();
-
    if (IsSharedRelation(catalogId))
        databaseId = InvalidOid;
    else
        databaseId = MyDatabaseId;
 
-   RegisterCatalogInvalidation(databaseId, catalogId);
+   RegisterCatalogInvalidation(PrepareInvalidationState(),
+                               databaseId, catalogId);
 }
 
 /*
@@ -1301,15 +1439,14 @@ CacheInvalidateRelcache(Relation relation)
    Oid         databaseId;
    Oid         relationId;
 
-   PrepareInvalidationState();
-
    relationId = RelationGetRelid(relation);
    if (relation->rd_rel->relisshared)
        databaseId = InvalidOid;
    else
        databaseId = MyDatabaseId;
 
-   RegisterRelcacheInvalidation(databaseId, relationId);
+   RegisterRelcacheInvalidation(PrepareInvalidationState(),
+                                databaseId, relationId);
 }
 
 /*
@@ -1322,9 +1459,8 @@ CacheInvalidateRelcache(Relation relation)
 void
 CacheInvalidateRelcacheAll(void)
 {
-   PrepareInvalidationState();
-
-   RegisterRelcacheInvalidation(InvalidOid, InvalidOid);
+   RegisterRelcacheInvalidation(PrepareInvalidationState(),
+                                InvalidOid, InvalidOid);
 }
 
 /*
@@ -1338,14 +1474,13 @@ CacheInvalidateRelcacheByTuple(HeapTuple classTuple)
    Oid         databaseId;
    Oid         relationId;
 
-   PrepareInvalidationState();
-
    relationId = classtup->oid;
    if (classtup->relisshared)
        databaseId = InvalidOid;
    else
        databaseId = MyDatabaseId;
-   RegisterRelcacheInvalidation(databaseId, relationId);
+   RegisterRelcacheInvalidation(PrepareInvalidationState(),
+                                databaseId, relationId);
 }
 
 /*
@@ -1359,8 +1494,6 @@ CacheInvalidateRelcacheByRelid(Oid relid)
 {
    HeapTuple   tup;
 
-   PrepareInvalidationState();
-
    tup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
    if (!HeapTupleIsValid(tup))
        elog(ERROR, "cache lookup failed for relation %u", relid);
@@ -1549,7 +1682,7 @@ LogLogicalInvalidations()
    if (transInvalInfo == NULL)
        return;
 
-   ProcessInvalidationMessagesMulti(&transInvalInfo->CurrentCmdInvalidMsgs,
+   ProcessInvalidationMessagesMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
                                     MakeSharedInvalidMessagesArray);
 
    Assert(!(numSharedInvalidMessagesArray > 0 &&
index b4f45cf4f2dfac19c0d29cd8443bd7a7a9dacd3b..945935476c159c91e821cb265f95abeff7689965 100644 (file)
@@ -1265,8 +1265,7 @@ SearchSysCacheLocked1(int cacheId,
 
        /*
         * If an inplace update just finished, ensure we process the syscache
-        * inval.  XXX this is insufficient: the inplace updater may not yet
-        * have reached AtEOXact_Inval().  See test at inplace-inval.spec.
+        * inval.
         *
         * If a heap_update() call just released its LOCKTAG_TUPLE, we'll
         * probably find the old tuple and reach "tuple concurrently updated".
index a468bdc449667a61b5ccba96e7d4c024cceb421b..7c03e8e4c17b8042bb31c6b0c4d096052738445c 100644 (file)
@@ -224,7 +224,8 @@ extern void CatCacheInvalidate(CatCache *cache, uint32 hashValue);
 extern void PrepareToInvalidateCacheTuple(Relation relation,
                                          HeapTuple tuple,
                                          HeapTuple newtuple,
-                                         void (*function) (int, uint32, Oid));
+                                         void (*function) (int, uint32, Oid, void *),
+                                         void *context);
 
 extern void PrintCatCacheLeakWarning(HeapTuple tuple);
 extern void PrintCatCacheListLeakWarning(CatCList *list);
index 877e66c63c8deea574dcb5916fce73a804dcc19d..8299badefc16a3f022687cffbb7e395e8edec5d5 100644 (file)
@@ -28,6 +28,10 @@ extern void AcceptInvalidationMessages(void);
 
 extern void AtEOXact_Inval(bool isCommit);
 
+extern void PreInplace_Inval(void);
+extern void AtInplace_Inval(void);
+extern void ForgetInplace_Inval(void);
+
 extern void AtEOSubXact_Inval(bool isCommit);
 
 extern void PostPrepare_Inval(void);
@@ -37,6 +41,8 @@ extern void CommandEndInvalidationMessages(void);
 extern void CacheInvalidateHeapTuple(Relation relation,
                                     HeapTuple tuple,
                                     HeapTuple newtuple);
+extern void CacheInvalidateHeapTupleInplace(Relation relation,
+                                           HeapTuple key_equivalent_tuple);
 
 extern void CacheInvalidateCatalog(Oid catalogId);
 
index e68eca5de98ddd92d631fc551d14f0549542ac9a..c35895a8aa7b0ad102188daa86516547cf0d8659 100644 (file)
@@ -1,6 +1,6 @@
 Parsed test spec with 3 sessions
 
-starting permutation: cachefill3 cir1 cic2 ddl3
+starting permutation: cachefill3 cir1 cic2 ddl3 read1
 step cachefill3: TABLE newly_indexed;
 c
 -
@@ -9,6 +9,14 @@ c
 step cir1: BEGIN; CREATE INDEX i1 ON newly_indexed (c); ROLLBACK;
 step cic2: CREATE INDEX i2 ON newly_indexed (c);
 step ddl3: ALTER TABLE newly_indexed ADD extra int;
+step read1: 
+   SELECT relhasindex FROM pg_class WHERE oid = 'newly_indexed'::regclass;
+
+relhasindex
+-----------
+t          
+(1 row)
+
 
 starting permutation: cir1 cic2 ddl3 read1
 step cir1: BEGIN; CREATE INDEX i1 ON newly_indexed (c); ROLLBACK;
index 96954fd86c439fa086e07261479b2c9979e12afd..b99112ddb8818e077f28a27027a247a327322ad7 100644 (file)
@@ -1,7 +1,7 @@
-# If a heap_update() caller retrieves its oldtup from a cache, it's possible
-# for that cache entry to predate an inplace update, causing loss of that
-# inplace update.  This arises because the transaction may abort before
-# sending the inplace invalidation message to the shared queue.
+# An inplace update had been able to abort before sending the inplace
+# invalidation message to the shared queue.  If a heap_update() caller then
+# retrieved its oldtup from a cache, the heap_update() could revert the
+# inplace update.
 
 setup
 {
@@ -27,14 +27,12 @@ step cachefill3 { TABLE newly_indexed; }
 step ddl3      { ALTER TABLE newly_indexed ADD extra int; }
 
 
-# XXX shows an extant bug.  Adding step read1 at the end would usually print
-# relhasindex=f (not wanted).  This does not reach the unwanted behavior under
-# -DCATCACHE_FORCE_RELEASE and friends.
 permutation
    cachefill3  # populates the pg_class row in the catcache
    cir1    # sets relhasindex=true; rollback discards cache inval
    cic2    # sees relhasindex=true, skips changing it (so no inval)
    ddl3    # cached row as the oldtup of an update, losing relhasindex
+   read1   # observe damage
 
 # without cachefill3, no bug
 permutation cir1 cic2 ddl3 read1
index 1d615493deba396f0350e52de25db6bbe162bf61..7eb23a0c5ea7c5652fb7c3fa4cad7f3b169059cb 100644 (file)
@@ -1156,8 +1156,8 @@ InternalDefaultACL
 InternalGrant
 Interval
 IntoClause
-InvalidationChunk
-InvalidationListHeader
+InvalMessageArray
+InvalidationMsgsGroup
 IpcMemoryId
 IpcMemoryKey
 IpcMemoryState