For inplace update, send nontransactional invalidations.
authorNoah Misch <noah@leadboat.com>
Fri, 25 Oct 2024 13:51:02 +0000 (06:51 -0700)
committerNoah Misch <noah@leadboat.com>
Fri, 25 Oct 2024 13:51:02 +0000 (06:51 -0700)
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 v12 (all supported versions).
The back branch versions don't change WAL, because those branches just
added 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.

Reviewed by Nitin Motiani and (in earlier versions) Andres Freund.

Discussion: https://postgr.es/m/20240523000548.58.nmisch@google.com

19 files changed:
src/backend/access/heap/heapam.c
src/backend/access/heap/heapam_xlog.c
src/backend/access/rmgrdesc/heapdesc.c
src/backend/access/rmgrdesc/standbydesc.c
src/backend/access/transam/xact.c
src/backend/catalog/index.c
src/backend/commands/event_trigger.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/access/heapam_xlog.h
src/include/access/xlog_internal.h
src/include/storage/sinval.h
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 82a0492aac5fde54c9deee37b3e075af84f633b9..3a13671a1ef68fc35dc008fb4d2dddfd31d3f57a 100644 (file)
@@ -6326,6 +6326,9 @@ heap_inplace_update_and_unlock(Relation relation,
    HeapTupleHeader htup = oldtup->t_data;
    uint32      oldlen;
    uint32      newlen;
+   int         nmsgs = 0;
+   SharedInvalidationMessage *invalMessages = NULL;
+   bool        RelcacheInitFileInval = false;
 
    Assert(ItemPointerEquals(&oldtup->t_self, &tuple->t_self));
    oldlen = oldtup->t_len - htup->t_hoff;
@@ -6333,6 +6336,29 @@ heap_inplace_update_and_unlock(Relation relation,
    if (oldlen != newlen || htup->t_hoff != tuple->t_data->t_hoff)
        elog(ERROR, "wrong tuple length");
 
+   /*
+    * Construct 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.
+    */
+   CacheInvalidateHeapTupleInplace(relation, tuple, NULL);
+
+   /* Like RecordTransactionCommit(), log only if needed */
+   if (XLogStandbyInfoActive())
+       nmsgs = inplaceGetInvalidationMessages(&invalMessages,
+                                              &RelcacheInitFileInval);
+
+   /*
+    * 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();
 
@@ -6362,9 +6388,16 @@ heap_inplace_update_and_unlock(Relation relation,
        XLogRecPtr  recptr;
 
        xlrec.offnum = ItemPointerGetOffsetNumber(&tuple->t_self);
+       xlrec.dbId = MyDatabaseId;
+       xlrec.tsId = MyDatabaseTableSpace;
+       xlrec.relcacheInitFileInval = RelcacheInitFileInval;
+       xlrec.nmsgs = nmsgs;
 
        XLogBeginInsert();
-       XLogRegisterData((char *) &xlrec, SizeOfHeapInplace);
+       XLogRegisterData((char *) &xlrec, MinSizeOfHeapInplace);
+       if (nmsgs != 0)
+           XLogRegisterData((char *) invalMessages,
+                            nmsgs * sizeof(SharedInvalidationMessage));
 
        XLogRegisterBuffer(0, buffer, REGBUF_STANDARD);
        XLogRegisterBufData(0, (char *) htup + htup->t_hoff, newlen);
@@ -6376,17 +6409,28 @@ 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().
+    *
+    * If we're mutating a tuple visible only to this transaction, there's an
+    * equivalent transactional inval from the action that created the tuple,
+    * and this inval is superfluous.
+    */
+   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.  The immediate invalidation we just sent
+    * is the only one known to be necessary.  To reduce risk from the
+    * transition to immediate invalidation, continue sending a transactional
+    * invalidation like we've long done.  Third-party code might rely on it.
     */
    if (!IsBootstrapProcessingMode())
        CacheInvalidateHeapTuple(relation, tuple, NULL);
index 6dae7233ecb119585b1aea47cc78c7574733e249..c5208f3df617288f421768efe78cdf3bc2d269f0 100644 (file)
@@ -1170,6 +1170,12 @@ heap_xlog_inplace(XLogReaderState *record)
    }
    if (BufferIsValid(buffer))
        UnlockReleaseBuffer(buffer);
+
+   ProcessCommittedInvalidationMessages(xlrec->msgs,
+                                        xlrec->nmsgs,
+                                        xlrec->relcacheInitFileInval,
+                                        xlrec->dbId,
+                                        xlrec->tsId);
 }
 
 void
index 5f5673e088b01f69dbb8f57a197c4a24bed0c5fa..f31cc3a4df4bbe61de013007ddcd33b6933dbe9a 100644 (file)
@@ -16,6 +16,7 @@
 
 #include "access/heapam_xlog.h"
 #include "access/rmgrdesc_utils.h"
+#include "storage/standbydefs.h"
 
 /*
  * NOTE: "keyname" argument cannot have trailing spaces or punctuation
@@ -253,6 +254,9 @@ heap_desc(StringInfo buf, XLogReaderState *record)
        xl_heap_inplace *xlrec = (xl_heap_inplace *) rec;
 
        appendStringInfo(buf, "off: %u", xlrec->offnum);
+       standby_desc_invalidations(buf, xlrec->nmsgs, xlrec->msgs,
+                                  xlrec->dbId, xlrec->tsId,
+                                  xlrec->relcacheInitFileInval);
    }
 }
 
index 25f870b187e8e5b72fc4214ec27c2a574c3798d2..32e509a40063e300dd594d400248886e844cb482 100644 (file)
@@ -96,11 +96,7 @@ standby_identify(uint8 info)
    return id;
 }
 
-/*
- * This routine is used by both standby_desc and xact_desc, because
- * transaction commits and XLOG_INVALIDATIONS messages contain invalidations;
- * it seems pointless to duplicate the code.
- */
+/* also used by non-standby records having analogous invalidation fields */
 void
 standby_desc_invalidations(StringInfo buf,
                           int nmsgs, SharedInvalidationMessage *msgs,
index d8f6c6584208c0e7a61151589f6c1eef3e83ad08..004f7e10e559784e664fa8310fddf5512c733b0f 100644 (file)
@@ -1369,14 +1369,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 12822d0b1400957e304955eff83168c55a20d9d8..9162b9f81a2e230dedf112dbab7d25c64d88a540 100644 (file)
@@ -2918,12 +2918,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 05a6de68ba3b036c7d8559c4516a042a8209a35b..a586d246eceec1b3d69423fec7cc1cfc931c2d5d 100644 (file)
@@ -975,11 +975,6 @@ EventTriggerOnLogin(void)
                 * this instead of regular updates serves two purposes. First,
                 * that avoids possible waiting on the row-level lock. Second,
                 * that avoids dealing with TOAST.
-                *
-                * Changes made by inplace update may be lost due to
-                * concurrent normal updates; see inplace-inval.spec. However,
-                * we are OK with that.  The subsequent connections will still
-                * have a chance to set "dathasloginevt" to false.
                 */
                systable_inplace_update_finish(state, tuple);
            }
index d687ceee339f8fe8c13d2bb869c523586d3db2d1..e73576ad12f24bf1d12dcbe17cba9455acbec23a 100644 (file)
@@ -509,23 +509,19 @@ heap_decode(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
-            * record's contents.
+            * can, per definition, not change tuple visibility.  Inplace
+            * updates don't affect storage or interpretation of table rows,
+            * so they don't affect logicalrep_write_tuple() outcomes.  Hence,
+            * we don't process invalidations from the original operation.  If
+            * inplace updates did affect those things, invalidations wouldn't
+            * make it work, since there are no snapshot-specific versions of
+            * inplace-updated values.  Since we 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.
+            * WAL contains likely-unnecessary commit-time invals from the
+            * CacheInvalidateHeapTuple() call in heap_inplace_update().
+            * Excess invalidation is safe.
             */
-           if (!TransactionIdIsValid(xid))
-               break;
-
-           (void) SnapBuildProcessChange(builder, xid, buf->origptr);
-           ReorderBufferXidSetCatalogChanges(ctx->reorder, xid, buf->origptr);
            break;
 
        case XLOG_HEAP_CONFIRM:
index 10276aa1db10ff5091e51c6269dca82b57b40424..ee303dc501dd69b406b953dcafb6873e5a206df7 100644 (file)
@@ -2286,7 +2286,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;
@@ -2327,7 +2328,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)
        {
@@ -2336,7 +2337,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 603aa4157bef170a56f567466ff3b8f7eacdaf3a..986850ccda95c56c58f664161699e99bf0cb7954 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.
 
 /*
  * Pending requests are stored as ready-to-send SharedInvalidationMessages.
- * We keep the messages themselves in arrays in TopTransactionContext
- * (there are separate arrays for catcache and relcache messages).  Control
- * information is kept in a chain of TransInvalidationInfo structs, also
- * allocated in TopTransactionContext.  (We could keep a subtransaction's
- * TransInvalidationInfo in its CurTransactionContext; but that's more
- * wasteful not less so, since in very many scenarios it'd be the only
- * allocation in the subtransaction's CurTransactionContext.)
+ * We keep the messages themselves in arrays in TopTransactionContext (there
+ * are separate arrays for catcache and relcache messages).  For transactional
+ * messages, control information is kept in a chain of TransInvalidationInfo
+ * structs, also allocated in TopTransactionContext.  (We could keep a
+ * subtransaction's TransInvalidationInfo in its CurTransactionContext; but
+ * that's more wasteful not less so, since in very many scenarios it'd be the
+ * only allocation in the subtransaction's CurTransactionContext.)  For
+ * inplace update messages, control information appears in an
+ * InvalidationInfo, allocated in CurrentMemoryContext.
  *
  * We can store the message arrays densely, and yet avoid moving data around
  * within an array, because within any one subtransaction we need only
  * struct.  Similarly, we need distinguish messages of prior subtransactions
  * from those of the current subtransaction only until the subtransaction
  * completes, after which we adjust the array indexes in the parent's
- * TransInvalidationInfo to include the subtransaction's messages.
+ * TransInvalidationInfo to include the subtransaction's messages.  Inplace
+ * invalidations don't need a concept of command or subtransaction boundaries,
+ * since we send them during the WAL insertion critical section.
  *
  * The ordering of the individual messages within a command's or
  * subtransaction's output is not considered significant, although this
@@ -200,7 +208,7 @@ typedef struct InvalidationMsgsGroup
 
 
 /*----------------
- * Invalidation messages are divided into two groups:
+ * Transactional invalidation messages are divided into two groups:
  * 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
@@ -216,26 +224,36 @@ typedef struct InvalidationMsgsGroup
  *----------------
  */
 
-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;
-
    /* Events emitted by current command */
    InvalidationMsgsGroup CurrentCmdInvalidMsgs;
 
+   /* init file must be invalidated? */
+   bool        RelcacheInitFileInval;
+} InvalidationInfo;
+
+/* subclass adding fields specific to transactional invalidation */
+typedef struct TransInvalidationInfo
+{
+   /* Base class */
+   struct InvalidationInfo ii;
+
    /* Events emitted by previous commands of this (sub)transaction */
    InvalidationMsgsGroup 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;
+
 /* GUC storage */
 int            debug_discard_caches = 0;
 
@@ -543,9 +561,12 @@ ProcessInvalidationMessagesMulti(InvalidationMsgsGroup *group,
 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);
 }
 
@@ -555,10 +576,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);
 }
 
 /*
@@ -567,10 +587,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
@@ -587,7 +606,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;
 }
 
 /*
@@ -597,24 +616,27 @@ 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);
 }
 
 /*
  * PrepareInvalidationState
  *     Initialize inval data 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,
@@ -637,7 +659,7 @@ PrepareInvalidationState(void)
         * counter.  This is a convenient place to check for that, as well as
         * being important to keep management of the message arrays simple.
         */
-       if (NumMessagesInGroup(&transInvalInfo->CurrentCmdInvalidMsgs) != 0)
+       if (NumMessagesInGroup(&transInvalInfo->ii.CurrentCmdInvalidMsgs) != 0)
            elog(ERROR, "cannot start a subtransaction when there are unprocessed inval messages");
 
        /*
@@ -646,8 +668,8 @@ PrepareInvalidationState(void)
         * to update them to follow whatever is already in the arrays.
         */
        SetGroupToFollow(&myInfo->PriorCmdInvalidMsgs,
-                        &transInvalInfo->CurrentCmdInvalidMsgs);
-       SetGroupToFollow(&myInfo->CurrentCmdInvalidMsgs,
+                        &transInvalInfo->ii.CurrentCmdInvalidMsgs);
+       SetGroupToFollow(&myInfo->ii.CurrentCmdInvalidMsgs,
                         &myInfo->PriorCmdInvalidMsgs);
    }
    else
@@ -663,6 +685,41 @@ PrepareInvalidationState(void)
    }
 
    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));
+
+   /* Stash our messages past end of the transactional messages, if any. */
+   if (transInvalInfo != NULL)
+       SetGroupToFollow(&myInfo->CurrentCmdInvalidMsgs,
+                        &transInvalInfo->ii.CurrentCmdInvalidMsgs);
+   else
+   {
+       InvalMessageArrays[CatCacheMsgs].msgs = NULL;
+       InvalMessageArrays[CatCacheMsgs].maxmsgs = 0;
+       InvalMessageArrays[RelCacheMsgs].msgs = NULL;
+       InvalMessageArrays[RelCacheMsgs].maxmsgs = 0;
+   }
+
+   inplaceInvalInfo = myInfo;
+   return myInfo;
 }
 
 /* ----------------------------------------------------------------
@@ -902,7 +959,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;
 
    /*
     * Collect all the pending messages into a single contiguous array of
@@ -913,7 +970,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
     * not new ones.
     */
    nummsgs = NumMessagesInGroup(&transInvalInfo->PriorCmdInvalidMsgs) +
-       NumMessagesInGroup(&transInvalInfo->CurrentCmdInvalidMsgs);
+       NumMessagesInGroup(&transInvalInfo->ii.CurrentCmdInvalidMsgs);
 
    *msgs = msgarray = (SharedInvalidationMessage *)
        MemoryContextAlloc(CurTransactionContext,
@@ -926,7 +983,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
                                        msgs,
                                        n * sizeof(SharedInvalidationMessage)),
                                 nmsgs += n));
-   ProcessMessageSubGroupMulti(&transInvalInfo->CurrentCmdInvalidMsgs,
+   ProcessMessageSubGroupMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
                                CatCacheMsgs,
                                (memcpy(msgarray + nmsgs,
                                        msgs,
@@ -938,7 +995,51 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
                                        msgs,
                                        n * sizeof(SharedInvalidationMessage)),
                                 nmsgs += n));
-   ProcessMessageSubGroupMulti(&transInvalInfo->CurrentCmdInvalidMsgs,
+   ProcessMessageSubGroupMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
+                               RelCacheMsgs,
+                               (memcpy(msgarray + nmsgs,
+                                       msgs,
+                                       n * sizeof(SharedInvalidationMessage)),
+                                nmsgs += n));
+   Assert(nmsgs == nummsgs);
+
+   return nmsgs;
+}
+
+/*
+ * inplaceGetInvalidationMessages() is called by the inplace update to collect
+ * invalidation messages to add to its WAL record.  Like the previous
+ * function, we might still fail.
+ */
+int
+inplaceGetInvalidationMessages(SharedInvalidationMessage **msgs,
+                              bool *RelcacheInitFileInval)
+{
+   SharedInvalidationMessage *msgarray;
+   int         nummsgs;
+   int         nmsgs;
+
+   /* Quick exit if we haven't done anything with invalidation messages. */
+   if (inplaceInvalInfo == NULL)
+   {
+       *RelcacheInitFileInval = false;
+       *msgs = NULL;
+       return 0;
+   }
+
+   *RelcacheInitFileInval = inplaceInvalInfo->RelcacheInitFileInval;
+   nummsgs = NumMessagesInGroup(&inplaceInvalInfo->CurrentCmdInvalidMsgs);
+   *msgs = msgarray = (SharedInvalidationMessage *)
+       palloc(nummsgs * sizeof(SharedInvalidationMessage));
+
+   nmsgs = 0;
+   ProcessMessageSubGroupMulti(&inplaceInvalInfo->CurrentCmdInvalidMsgs,
+                               CatCacheMsgs,
+                               (memcpy(msgarray + nmsgs,
+                                       msgs,
+                                       n * sizeof(SharedInvalidationMessage)),
+                                nmsgs += n));
+   ProcessMessageSubGroupMulti(&inplaceInvalInfo->CurrentCmdInvalidMsgs,
                                RelCacheMsgs,
                                (memcpy(msgarray + nmsgs,
                                        msgs,
@@ -1024,7 +1125,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;
 
@@ -1038,16 +1141,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
@@ -1060,6 +1163,44 @@ AtEOXact_Inval(bool isCommit)
    transInvalInfo = NULL;
 }
 
+/*
+ * 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;
+}
+
 /*
  * AtEOSubXact_Inval
  *     Process queued-up invalidation messages at end of subtransaction.
@@ -1082,9 +1223,20 @@ void
 AtEOSubXact_Inval(bool isCommit)
 {
    int         my_level;
-   TransInvalidationInfo *myInfo = transInvalInfo;
+   TransInvalidationInfo *myInfo;
+
+   /*
+    * 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 messages. */
+   /* Quick exit if no transactional messages. */
+   myInfo = transInvalInfo;
    if (myInfo == NULL)
        return;
 
@@ -1125,12 +1277,12 @@ AtEOSubXact_Inval(bool isCommit)
                                   &myInfo->PriorCmdInvalidMsgs);
 
        /* Must readjust parent's CurrentCmdInvalidMsgs indexes now */
-       SetGroupToFollow(&myInfo->parent->CurrentCmdInvalidMsgs,
+       SetGroupToFollow(&myInfo->parent->ii.CurrentCmdInvalidMsgs,
                         &myInfo->parent->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;
@@ -1177,7 +1329,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 */
@@ -1185,26 +1337,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;
@@ -1228,11 +1375,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
@@ -1241,11 +1385,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
@@ -1318,7 +1463,44 @@ 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.
+ */
+void
+CacheInvalidateHeapTupleInplace(Relation relation,
+                               HeapTuple tuple,
+                               HeapTuple newtuple)
+{
+   CacheInvalidateHeapTupleCommon(relation, tuple, newtuple,
+                                  PrepareInplaceInvalidationState);
 }
 
 /*
@@ -1337,14 +1519,13 @@ CacheInvalidateCatalog(Oid catalogId)
 {
    Oid         databaseId;
 
-   PrepareInvalidationState();
-
    if (IsSharedRelation(catalogId))
        databaseId = InvalidOid;
    else
        databaseId = MyDatabaseId;
 
-   RegisterCatalogInvalidation(databaseId, catalogId);
+   RegisterCatalogInvalidation(PrepareInvalidationState(),
+                               databaseId, catalogId);
 }
 
 /*
@@ -1362,15 +1543,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);
 }
 
 /*
@@ -1383,9 +1563,8 @@ CacheInvalidateRelcache(Relation relation)
 void
 CacheInvalidateRelcacheAll(void)
 {
-   PrepareInvalidationState();
-
-   RegisterRelcacheInvalidation(InvalidOid, InvalidOid);
+   RegisterRelcacheInvalidation(PrepareInvalidationState(),
+                                InvalidOid, InvalidOid);
 }
 
 /*
@@ -1399,14 +1578,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);
 }
 
 /*
@@ -1420,8 +1598,6 @@ CacheInvalidateRelcacheByRelid(Oid relid)
 {
    HeapTuple   tup;
 
-   PrepareInvalidationState();
-
    tup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
    if (!HeapTupleIsValid(tup))
        elog(ERROR, "cache lookup failed for relation %u", relid);
@@ -1611,7 +1787,7 @@ LogLogicalInvalidations(void)
    if (transInvalInfo == NULL)
        return;
 
-   group = &transInvalInfo->CurrentCmdInvalidMsgs;
+   group = &transInvalInfo->ii.CurrentCmdInvalidMsgs;
    nmsgs = NumMessagesInGroup(group);
 
    if (nmsgs > 0)
index 50c9440f7923e8a3b473b8fdefe6d2e47f13c385..f41b1c221a1ed90c3ff0f093621a878e8792412d 100644 (file)
@@ -351,8 +351,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 42736f37e79f3248a572e16c007326e179b93c0e..4591e9a918f0bca7439f65cc945539ea0d7dcd1d 100644 (file)
@@ -20,6 +20,7 @@
 #include "storage/buf.h"
 #include "storage/bufpage.h"
 #include "storage/relfilelocator.h"
+#include "storage/sinval.h"
 #include "utils/relcache.h"
 
 
@@ -425,9 +426,14 @@ typedef struct xl_heap_confirm
 typedef struct xl_heap_inplace
 {
    OffsetNumber offnum;        /* updated tuple's offset on page */
+   Oid         dbId;           /* MyDatabaseId */
+   Oid         tsId;           /* MyDatabaseTableSpace */
+   bool        relcacheInitFileInval;  /* invalidate relcache init files */
+   int         nmsgs;          /* number of shared inval msgs */
+   SharedInvalidationMessage msgs[FLEXIBLE_ARRAY_MEMBER];
 } xl_heap_inplace;
 
-#define SizeOfHeapInplace  (offsetof(xl_heap_inplace, offnum) + sizeof(OffsetNumber))
+#define MinSizeOfHeapInplace   (offsetof(xl_heap_inplace, nmsgs) + sizeof(int))
 
 /*
  * This is what we need to know about setting a visibility map bit
index 5ef244bcdb44156967a9a04e268230c512a20f2c..d9cf51a0f9fa4a55ffd2d35b62e1618b2a596418 100644 (file)
@@ -31,7 +31,7 @@
 /*
  * Each page of XLOG file has a header like this:
  */
-#define XLOG_PAGE_MAGIC 0xD117 /* can be used as WAL version indicator */
+#define XLOG_PAGE_MAGIC 0xD118 /* can be used as WAL version indicator */
 
 typedef struct XLogPageHeaderData
 {
index 8f5744b21bc9a8aab2ad5b3b82d2da5b0641d060..c8122372551c39ab9ab85254ac84fbb9b5a988bf 100644 (file)
@@ -144,6 +144,8 @@ extern void ProcessCatchupInterrupt(void);
 
 extern int xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
                                                 bool *RelcacheInitFileInval);
+extern int inplaceGetInvalidationMessages(SharedInvalidationMessage **msgs,
+                                          bool *RelcacheInitFileInval);
 extern void ProcessCommittedInvalidationMessages(SharedInvalidationMessage *msgs,
                                                 int nmsgs, bool RelcacheInitFileInval,
                                                 Oid dbid, Oid tsid);
index 3fb9647b87c871740ee8cb737a78d1d327f45009..8f04bb8845bab6f4f42a7ceb43ff288b20657097 100644 (file)
@@ -225,6 +225,7 @@ 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);
 
 #endif                         /* CATCACHE_H */
index 24695facf22183a01b25c1f18808761cac07f2ad..3390e7ab8af9cf724339a2d756f531d312ae022f 100644 (file)
@@ -28,6 +28,9 @@ extern void AcceptInvalidationMessages(void);
 
 extern void AtEOXact_Inval(bool isCommit);
 
+extern void PreInplace_Inval(void);
+extern void AtInplace_Inval(void);
+
 extern void AtEOSubXact_Inval(bool isCommit);
 
 extern void PostPrepare_Inval(void);
@@ -37,6 +40,9 @@ extern void CommandEndInvalidationMessages(void);
 extern void CacheInvalidateHeapTuple(Relation relation,
                                     HeapTuple tuple,
                                     HeapTuple newtuple);
+extern void CacheInvalidateHeapTupleInplace(Relation relation,
+                                           HeapTuple tuple,
+                                           HeapTuple newtuple);
 
 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 110089695e19ddfc5338819305537d54480f6e89..171a7dd5d2b94600e217626f09afde8655f59142 100644 (file)
@@ -1258,6 +1258,7 @@ Interval
 IntervalAggState
 IntoClause
 InvalMessageArray
+InvalidationInfo
 InvalidationMsgsGroup
 IpcMemoryId
 IpcMemoryKey