From: Noah Misch <noah@leadboat.com>
To: Nitin Motiani <nitinmotiani@google.com>
To: Andres Freund <andres@anarazel.de>
To: pgsql-hackers@postgresql.org
Subject: Re: Inval reliability, especially for inplace updates
Date: Wed, 23 Oct 2024 19:53:57 -0700
Message-ID: <20241024025357.a8.nmisch@google.com> (raw)
In-Reply-To: <CAH5HC95rn4gON8zNrFXFY-Cn4z0NZuDU_1TDwKnafX_25_C0UA@mail.gmail.com>
References: <20240615223718.42.nmisch@google.com>
<20240617235854.f8.nmisch@google.com>
<20240618015730.jijhox2a3w3t26en@awork3.anarazel.de>
<20240618152349.7f.nmisch@google.com>
<20240831010711.67.nmisch@google.com>
<20240930193350.04.nmisch@google.com>
<CAH5HC9508Xvz4BVca8OCoDOv7pgHqAf85EqZkWMZ_Q2hAkebzg@mail.gmail.com>
<20241013004511.9c.nmisch@google.com>
<CAH5HC94r2ZKy_MhNaPUjn7NHkOFgbyBAgV2jaYSRt35BNWuOAA@mail.gmail.com>
<CAH5HC95rn4gON8zNrFXFY-Cn4z0NZuDU_1TDwKnafX_25_C0UA@mail.gmail.com>
With the releases wrapping in 2.5 weeks, I'm ambivalent about pushing this
before the release or after. Pushing before means fewer occurrences of
corruption, but pushing after gives more bake time to discover these changes
were defective. It's hard to predict which helps users more, on a
risk-adjusted basis. I'm leaning toward pushing this week. Opinions?
On Sun, Oct 20, 2024 at 06:41:37PM +0530, Nitin Motiani wrote:
> I tested the patch locally and it works. And I have no other question
> regarding the structure. So this patch looks good to me to commit.
Thanks. While resolving a back-branch merge conflict, I found
AtEOXact_Inval() and AtEOSubXact_Inval() were skipping inplaceInvalInfo tasks
if transInvalInfo==NULL. If one PreInplace_Inval() failed, the session's next
inplace update would get an assertion failure. Non-assert builds left
inplaceInvalInfo pointing to freed memory, but I didn't find a reachable
malfunction. Separately, the xact.c comment edit wasn't reflecting that v4
brought back the transactional inval. v6 fixes those. Regarding the
back-branch alternatives to the WAL format change:
On Tue, Jun 18, 2024 at 08:23:49AM -0700, Noah Misch wrote:
> On Mon, Jun 17, 2024 at 06:57:30PM -0700, Andres Freund wrote:
> > On 2024-06-17 16:58:54 -0700, Noah Misch wrote:
> > > On Sat, Jun 15, 2024 at 03:37:18PM -0700, Noah Misch wrote:
> > > > - heap_xlog_inplace() could set the shared-inval-queue overflow signal on
> > > > every backend. This is more wasteful, but inplace updates might be rare
> > > > enough (~once per VACUUM) to make it tolerable.
> >
> > We already set that surprisingly frequently, as
> > a) The size of the sinval queue is small
> > b) If a backend is busy, it does not process catchup interrupts
> > (i.e. executing queries, waiting for a lock prevents processing)
> > c) There's no deduplication of invals, we often end up sending the same inval
> > over and over.
> >
> > So I suspect this might not be too bad, compared to the current badness.
>
> That is good.
I benchmarked that by hacking 027_stream_regress.pl to run "pgbench
--no-vacuum --client=4 -T 30 -b select-only" on the standby, concurrent with
the primary running the regression tests. Standby clients acted on sinval
resetState ~16k times, and pgbench tps decreased 4.5%. That doesn't
necessarily mean the real-life cost would be unacceptable, but it was enough
of a decrease that I switched to the next choice:
> We might be able to do the overflow signal once at end of
> recovery, like RelationCacheInitFileRemove() does for the init file. That's
> mildly harder to reason about, but it would be cheaper. Hmmm.
I'm attaching the branch-specific patches for that and for the main fix.
Other notes from back-patching:
- All branches change the ABI of PrepareToInvalidateCacheTuple(), a function
catcache.c exports for the benefit of inval.c. No PGXN extension calls
that, and I can't think of a use case in extensions.
- Due to v15 commit 3aafc03, the patch for v14 differs to an unusual degree
from the patch for v15+v16. The difference is mostly mechanical, though.
Thanks,
nm
commit 5704b32 (HEAD, zzy_test-commit-REL_14_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Wed Oct 23 15:27:13 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 15:33:18 2024 -0700
At end of recovery, reset all sinval-managed caches.
An inplace update's invalidation messages are part of its transaction's
commit record. However, the update survives even if its transaction
aborts or we stop recovery before replaying its transaction commit.
After recovery, a backend that started in recovery could update the row
without incorporating the inplace update. That could result in a table
with an index, yet relhasindex=f. That is a source of index corruption.
This bulk invalidation avoids the functional consequences. A future
change can fix the !RecoveryInProgress() scenario without changing the
WAL format. Back-patch to v17 - v12 (all supported versions). v18 will
instead add invalidations to WAL.
Reviewed by FIXME.
Discussion: https://postgr.es/m/20240618152349.7f.nmisch@google.com
Conflicts:
src/backend/access/transam/xlog.c
Conflicts:
src/backend/access/transam/xlog.c
---
src/backend/access/transam/xlog.c | 25 ++++++++++++++++++++++
src/backend/storage/ipc/sinvaladt.c | 41 +++++++++++++++++++++++++++++++++++++
src/include/storage/sinvaladt.h | 1 +
3 files changed, 67 insertions(+)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.cindex 7654ad7..f1a795b 100644--- a/src/backend/access/transam/xlog.c+++ b/src/backend/access/transam/xlog.c@@ -70,6 +70,7 @@
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/reinit.h"
+#include "storage/sinvaladt.h"
#include "storage/smgr.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -8020,6 +8021,30 @@ StartupXLOG(void)
}
/*
+ * Invalidate all sinval-managed caches before READ WRITE transactions+ * begin. The xl_heap_inplace WAL record doesn't store sufficient data+ * for invalidations. The commit record, if any, has the invalidations.+ * However, the inplace update is permanent, whether or not we reach a+ * commit record. Fortunately, read-only transactions tolerate caches not+ * reflecting the latest inplace updates. Read-only transactions+ * experience the notable inplace updates as follows:+ *+ * - relhasindex=true affects readers only after the CREATE INDEX+ * transaction commit makes an index fully available to them.+ *+ * - datconnlimit=DATCONNLIMIT_INVALID_DB affects readers only at+ * InitPostgres() time, and that read does not use a cache.+ *+ * - relfrozenxid, datfrozenxid, relminmxid, and datminmxid have no effect+ * on readers.+ *+ * Hence, hot standby queries (all READ ONLY) function correctly without+ * the missing invalidations. This avoided changing the WAL format in+ * back branches.+ */+ SIResetAll();++ /*
* Preallocate additional log files, if wanted.
*/
PreallocXlogFiles(EndOfLog);
diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.cindex 946bd8e..f0e2fb9 100644--- a/src/backend/storage/ipc/sinvaladt.c+++ b/src/backend/storage/ipc/sinvaladt.c@@ -748,6 +748,47 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
}
}
+/*+ * SIResetAll+ * Mark all active backends as "reset"+ *+ * Use this when we don't know what needs to be invalidated. It's a+ * cluster-wide InvalidateSystemCaches(). This was a back-branch-only remedy+ * to avoid a WAL format change.+ *+ * The implementation is like SICleanupQueue(false, MAXNUMMESSAGES + 1), with+ * one addition. SICleanupQueue() assumes minFree << MAXNUMMESSAGES, so it+ * assumes hasMessages==true for any backend it resets. We're resetting even+ * fully-caught-up backends, so we set hasMessages.+ */+void+SIResetAll(void)+{+ SISeg *segP = shmInvalBuffer;+ int i;++ LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);+ LWLockAcquire(SInvalReadLock, LW_EXCLUSIVE);++ for (i = 0; i < segP->lastBackend; i++)+ {+ ProcState *stateP = &segP->procState[i];++ if (stateP->procPid == 0 || stateP->sendOnly)+ continue;++ /* Consuming the reset will update "nextMsgNum" and "signaled". */+ stateP->resetState = true;+ stateP->hasMessages = true;+ }++ segP->minMsgNum = segP->maxMsgNum;+ segP->nextThreshold = CLEANUP_MIN;++ LWLockRelease(SInvalReadLock);+ LWLockRelease(SInvalWriteLock);+}+
/*
* GetNextLocalTransactionId --- allocate a new LocalTransactionId
diff --git a/src/include/storage/sinvaladt.h b/src/include/storage/sinvaladt.hindex 14148bf..07d229a 100644--- a/src/include/storage/sinvaladt.h+++ b/src/include/storage/sinvaladt.h@@ -37,6 +37,7 @@ extern void BackendIdGetTransactionIds(int backendID, TransactionId *xid, Transa
extern void SIInsertDataEntries(const SharedInvalidationMessage *data, int n);
extern int SIGetDataEntries(SharedInvalidationMessage *data, int datasize);
extern void SICleanupQueue(bool callerHasWriteLock, int minFree);
+extern void SIResetAll(void);
extern LocalTransactionId GetNextLocalTransactionId(void);
commit 1bfa06e (HEAD, zzy_test-commit-REL_16_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Wed Oct 23 15:27:13 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 15:31:54 2024 -0700
At end of recovery, reset all sinval-managed caches.
An inplace update's invalidation messages are part of its transaction's
commit record. However, the update survives even if its transaction
aborts or we stop recovery before replaying its transaction commit.
After recovery, a backend that started in recovery could update the row
without incorporating the inplace update. That could result in a table
with an index, yet relhasindex=f. That is a source of index corruption.
This bulk invalidation avoids the functional consequences. A future
change can fix the !RecoveryInProgress() scenario without changing the
WAL format. Back-patch to v17 - v12 (all supported versions). v18 will
instead add invalidations to WAL.
Reviewed by FIXME.
Discussion: https://postgr.es/m/20240618152349.7f.nmisch@google.com
Conflicts:
src/backend/access/transam/xlog.c
---
src/backend/access/transam/xlog.c | 25 ++++++++++++++++++++++
src/backend/storage/ipc/sinvaladt.c | 41 +++++++++++++++++++++++++++++++++++++
src/include/storage/sinvaladt.h | 1 +
3 files changed, 67 insertions(+)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.cindex a19ba71..46faf40 100644--- a/src/backend/access/transam/xlog.c+++ b/src/backend/access/transam/xlog.c@@ -94,6 +94,7 @@
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/reinit.h"
+#include "storage/sinvaladt.h"
#include "storage/smgr.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -5660,6 +5661,30 @@ StartupXLOG(void)
XLogCtl->LogwrtRqst.Flush = EndOfLog;
/*
+ * Invalidate all sinval-managed caches before READ WRITE transactions+ * begin. The xl_heap_inplace WAL record doesn't store sufficient data+ * for invalidations. The commit record, if any, has the invalidations.+ * However, the inplace update is permanent, whether or not we reach a+ * commit record. Fortunately, read-only transactions tolerate caches not+ * reflecting the latest inplace updates. Read-only transactions+ * experience the notable inplace updates as follows:+ *+ * - relhasindex=true affects readers only after the CREATE INDEX+ * transaction commit makes an index fully available to them.+ *+ * - datconnlimit=DATCONNLIMIT_INVALID_DB affects readers only at+ * InitPostgres() time, and that read does not use a cache.+ *+ * - relfrozenxid, datfrozenxid, relminmxid, and datminmxid have no effect+ * on readers.+ *+ * Hence, hot standby queries (all READ ONLY) function correctly without+ * the missing invalidations. This avoided changing the WAL format in+ * back branches.+ */+ SIResetAll();++ /*
* Preallocate additional log files, if wanted.
*/
PreallocXlogFiles(EndOfLog, newTLI);
diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.cindex 3d97c75..099c292 100644--- a/src/backend/storage/ipc/sinvaladt.c+++ b/src/backend/storage/ipc/sinvaladt.c@@ -762,6 +762,47 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
}
}
+/*+ * SIResetAll+ * Mark all active backends as "reset"+ *+ * Use this when we don't know what needs to be invalidated. It's a+ * cluster-wide InvalidateSystemCaches(). This was a back-branch-only remedy+ * to avoid a WAL format change.+ *+ * The implementation is like SICleanupQueue(false, MAXNUMMESSAGES + 1), with+ * one addition. SICleanupQueue() assumes minFree << MAXNUMMESSAGES, so it+ * assumes hasMessages==true for any backend it resets. We're resetting even+ * fully-caught-up backends, so we set hasMessages.+ */+void+SIResetAll(void)+{+ SISeg *segP = shmInvalBuffer;+ int i;++ LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);+ LWLockAcquire(SInvalReadLock, LW_EXCLUSIVE);++ for (i = 0; i < segP->lastBackend; i++)+ {+ ProcState *stateP = &segP->procState[i];++ if (stateP->procPid == 0 || stateP->sendOnly)+ continue;++ /* Consuming the reset will update "nextMsgNum" and "signaled". */+ stateP->resetState = true;+ stateP->hasMessages = true;+ }++ segP->minMsgNum = segP->maxMsgNum;+ segP->nextThreshold = CLEANUP_MIN;++ LWLockRelease(SInvalReadLock);+ LWLockRelease(SInvalWriteLock);+}+
/*
* GetNextLocalTransactionId --- allocate a new LocalTransactionId
diff --git a/src/include/storage/sinvaladt.h b/src/include/storage/sinvaladt.hindex db38819..1f9eaf2 100644--- a/src/include/storage/sinvaladt.h+++ b/src/include/storage/sinvaladt.h@@ -39,6 +39,7 @@ extern void BackendIdGetTransactionIds(int backendID, TransactionId *xid,
extern void SIInsertDataEntries(const SharedInvalidationMessage *data, int n);
extern int SIGetDataEntries(SharedInvalidationMessage *data, int datasize);
extern void SICleanupQueue(bool callerHasWriteLock, int minFree);
+extern void SIResetAll(void);
extern LocalTransactionId GetNextLocalTransactionId(void);
Author: Noah Misch <noah@leadboat.com>
Commit: Noah Misch <noah@leadboat.com>
At end of recovery, reset all sinval-managed caches.
An inplace update's invalidation messages are part of its transaction's
commit record. However, the update survives even if its transaction
aborts or we stop recovery before replaying its transaction commit.
After recovery, a backend that started in recovery could update the row
without incorporating the inplace update. That could result in a table
with an index, yet relhasindex=f. That is a source of index corruption.
This bulk invalidation avoids the functional consequences. A future
change can fix the !RecoveryInProgress() scenario without changing the
WAL format. Back-patch to v17 - v12 (all supported versions). v18 will
instead add invalidations to WAL.
Reviewed by FIXME.
Discussion: https://postgr.es/m/20240618152349.7f.nmisch@google.comdiff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.cindex 7f13602..c1dd8b5 100644--- a/src/backend/access/transam/xlog.c+++ b/src/backend/access/transam/xlog.c@@ -92,6 +92,7 @@
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/reinit.h"
+#include "storage/sinvaladt.h"
#include "storage/spin.h"
#include "storage/sync.h"
#include "utils/guc_hooks.h"
@@ -6038,6 +6039,30 @@ StartupXLOG(void)
XLogCtl->LogwrtRqst.Flush = EndOfLog;
/*
+ * Invalidate all sinval-managed caches before READ WRITE transactions+ * begin. The xl_heap_inplace WAL record doesn't store sufficient data+ * for invalidations. The commit record, if any, has the invalidations.+ * However, the inplace update is permanent, whether or not we reach a+ * commit record. Fortunately, read-only transactions tolerate caches not+ * reflecting the latest inplace updates. Read-only transactions+ * experience the notable inplace updates as follows:+ *+ * - relhasindex=true affects readers only after the CREATE INDEX+ * transaction commit makes an index fully available to them.+ *+ * - datconnlimit=DATCONNLIMIT_INVALID_DB affects readers only at+ * InitPostgres() time, and that read does not use a cache.+ *+ * - relfrozenxid, datfrozenxid, relminmxid, and datminmxid have no effect+ * on readers.+ *+ * Hence, hot standby queries (all READ ONLY) function correctly without+ * the missing invalidations. This avoided changing the WAL format in+ * back branches.+ */+ SIResetAll();++ /*
* Preallocate additional log files, if wanted.
*/
PreallocXlogFiles(EndOfLog, newTLI);
diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.cindex b486d8d..2712129 100644--- a/src/backend/storage/ipc/sinvaladt.c+++ b/src/backend/storage/ipc/sinvaladt.c@@ -683,6 +683,48 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
}
}
+/*+ * SIResetAll+ * Mark all active backends as "reset"+ *+ * Use this when we don't know what needs to be invalidated. It's a+ * cluster-wide InvalidateSystemCaches(). This was a back-branch-only remedy+ * to avoid a WAL format change.+ *+ * The implementation is like SICleanupQueue(false, MAXNUMMESSAGES + 1), with+ * one addition. SICleanupQueue() assumes minFree << MAXNUMMESSAGES, so it+ * assumes hasMessages==true for any backend it resets. We're resetting even+ * fully-caught-up backends, so we set hasMessages.+ */+void+SIResetAll(void)+{+ SISeg *segP = shmInvalBuffer;+ int i;++ LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);+ LWLockAcquire(SInvalReadLock, LW_EXCLUSIVE);++ for (i = 0; i < segP->numProcs; i++)+ {+ ProcState *stateP = &segP->procState[segP->pgprocnos[i]];++ Assert(stateP->procPid != 0);+ if (stateP->sendOnly)+ continue;++ /* Consuming the reset will update "nextMsgNum" and "signaled". */+ stateP->resetState = true;+ stateP->hasMessages = true;+ }++ segP->minMsgNum = segP->maxMsgNum;+ segP->nextThreshold = CLEANUP_MIN;++ LWLockRelease(SInvalReadLock);+ LWLockRelease(SInvalWriteLock);+}+
/*
* GetNextLocalTransactionId --- allocate a new LocalTransactionId
diff --git a/src/include/storage/sinvaladt.h b/src/include/storage/sinvaladt.hindex c3c97b3..7c8f450 100644--- a/src/include/storage/sinvaladt.h+++ b/src/include/storage/sinvaladt.h@@ -35,6 +35,7 @@ extern void SharedInvalBackendInit(bool sendOnly);
extern void SIInsertDataEntries(const SharedInvalidationMessage *data, int n);
extern int SIGetDataEntries(SharedInvalidationMessage *data, int datasize);
extern void SICleanupQueue(bool callerHasWriteLock, int minFree);
+extern void SIResetAll(void);
extern LocalTransactionId GetNextLocalTransactionId(void);
commit efc6d94 (HEAD, zzy_test-commit-REL_13_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Wed Oct 23 15:35:02 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 17:54:08 2024 -0700
For inplace update, send nontransactional invalidations.
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
Conflicts:
src/backend/replication/logical/decode.c
src/backend/utils/cache/inval.c
src/tools/pgindent/typedefs.list
Conflicts:
src/backend/utils/cache/inval.c
---
src/backend/access/heap/heapam.c | 43 ++++-
src/backend/access/transam/xact.c | 26 ++-
src/backend/catalog/index.c | 11 +-
src/backend/replication/logical/decode.c | 26 ++-
src/backend/utils/cache/catcache.c | 7 +-
src/backend/utils/cache/inval.c | 259 +++++++++++++++++++-------
src/backend/utils/cache/syscache.c | 3 +-
src/include/utils/catcache.h | 3 +-
src/include/utils/inval.h | 6 +
src/test/isolation/expected/inplace-inval.out | 10 +-
src/test/isolation/specs/inplace-inval.spec | 12 +-
src/tools/pgindent/typedefs.list | 4 +-
12 files changed, 291 insertions(+), 119 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.cindex 846650c..ce10537 100644--- a/src/backend/access/heap/heapam.c+++ b/src/backend/access/heap/heapam.c@@ -6112,6 +6112,24 @@ 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);++ /*+ * 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();
@@ -6155,17 +6173,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);
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.cindex ffe26e2..cda6e33 100644--- a/src/backend/access/transam/xact.c+++ b/src/backend/access/transam/xact.c@@ -1249,14 +1249,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)
{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.cindex 4ca86d9..9ab38b9 100644--- a/src/backend/catalog/index.c+++ b/src/backend/catalog/index.c@@ -2882,12 +2882,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);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.cindex 297eb11..b3b51a9 100644--- a/src/backend/replication/logical/decode.c+++ b/src/backend/replication/logical/decode.c@@ -460,23 +460,19 @@ 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- * 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;-- SnapBuildProcessChange(builder, xid, buf->origptr);- ReorderBufferXidSetCatalogChanges(ctx->reorder, xid, buf->origptr);
break;
case XLOG_HEAP_CONFIRM:
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.cindex b53232f..9a61890 100644--- a/src/backend/utils/cache/catcache.c+++ b/src/backend/utils/cache/catcache.c@@ -2129,7 +2129,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;
@@ -2170,7 +2171,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)
{
@@ -2179,7 +2180,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);
}
}
}
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.cindex 8fe1f0d..a0dc745 100644--- a/src/backend/utils/cache/inval.c+++ b/src/backend/utils/cache/inval.c@@ -99,6 +99,10 @@
* 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.+ *
*
* Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
@@ -150,7 +154,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
@@ -166,26 +170,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;
@@ -499,9 +513,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);
}
@@ -511,10 +528,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);
}
/*
@@ -523,10 +539,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
@@ -543,7 +558,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;
}
/*
@@ -553,10 +568,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);
}
/*
@@ -748,14 +762,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,
@@ -771,6 +789,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;
}
/*
@@ -866,7 +907,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
@@ -878,7 +919,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
*/
oldcontext = MemoryContextSwitchTo(CurTransactionContext);
- ProcessInvalidationMessagesMulti(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessInvalidationMessagesMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
MakeSharedInvalidMessagesArray);
ProcessInvalidationMessagesMulti(&transInvalInfo->PriorCmdInvalidMsgs,
MakeSharedInvalidMessagesArray);
@@ -968,7 +1009,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;
@@ -982,16 +1025,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
@@ -1007,6 +1050,45 @@ AtEOXact_Inval(bool isCommit)
}
/*
+ * 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 */+}++/*
* AtEOSubXact_Inval
* Process queued-up invalidation messages at end of subtransaction.
*
@@ -1028,9 +1110,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;
@@ -1064,8 +1157,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;
@@ -1112,29 +1205,24 @@ CommandEndInvalidationMessages(void)
if (transInvalInfo == NULL)
return;
- ProcessInvalidationMessages(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessInvalidationMessages(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
LocalExecuteInvalidationMessage);
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;
@@ -1158,11 +1246,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
@@ -1171,11 +1256,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
@@ -1248,7 +1334,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);
}
/*
@@ -1267,14 +1390,13 @@ CacheInvalidateCatalog(Oid catalogId)
{
Oid databaseId;
- PrepareInvalidationState();-
if (IsSharedRelation(catalogId))
databaseId = InvalidOid;
else
databaseId = MyDatabaseId;
- RegisterCatalogInvalidation(databaseId, catalogId);+ RegisterCatalogInvalidation(PrepareInvalidationState(),+ databaseId, catalogId);
}
/*
@@ -1292,15 +1414,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);
}
/*
@@ -1313,9 +1434,8 @@ CacheInvalidateRelcache(Relation relation)
void
CacheInvalidateRelcacheAll(void)
{
- PrepareInvalidationState();-- RegisterRelcacheInvalidation(InvalidOid, InvalidOid);+ RegisterRelcacheInvalidation(PrepareInvalidationState(),+ InvalidOid, InvalidOid);
}
/*
@@ -1329,14 +1449,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);
}
/*
@@ -1350,8 +1469,6 @@ CacheInvalidateRelcacheByRelid(Oid relid)
{
HeapTuple tup;
- PrepareInvalidationState();-
tup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tup))
elog(ERROR, "cache lookup failed for relation %u", relid);
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.cindex 8ad8791..e4a8fb2 100644--- a/src/backend/utils/cache/syscache.c+++ b/src/backend/utils/cache/syscache.c@@ -1249,8 +1249,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".
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.hindex f4aa316..2722173 100644--- a/src/include/utils/catcache.h+++ b/src/include/utils/catcache.h@@ -223,7 +223,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);
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.hindex 4c6b86c..3ff52e8 100644--- a/src/include/utils/inval.h+++ b/src/include/utils/inval.h@@ -27,6 +27,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);
@@ -36,6 +39,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);
diff --git a/src/test/isolation/expected/inplace-inval.out b/src/test/isolation/expected/inplace-inval.outindex e68eca5..c35895a 100644--- a/src/test/isolation/expected/inplace-inval.out+++ b/src/test/isolation/expected/inplace-inval.out@@ -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;
diff --git a/src/test/isolation/specs/inplace-inval.spec b/src/test/isolation/specs/inplace-inval.specindex 96954fd..b99112d 100644--- a/src/test/isolation/specs/inplace-inval.spec+++ b/src/test/isolation/specs/inplace-inval.spec@@ -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
commit 8133c66 (HEAD, zzy_test-commit-REL_14_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Wed Oct 23 15:35:02 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 17:50:57 2024 -0700
For inplace update, send nontransactional invalidations.
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
Conflicts:
src/backend/replication/logical/decode.c
src/backend/utils/cache/inval.c
src/tools/pgindent/typedefs.list
---
src/backend/access/heap/heapam.c | 43 ++++-
src/backend/access/transam/xact.c | 26 ++-
src/backend/catalog/index.c | 11 +-
src/backend/replication/logical/decode.c | 26 ++-
src/backend/utils/cache/catcache.c | 7 +-
src/backend/utils/cache/inval.c | 261 +++++++++++++++++++-------
src/backend/utils/cache/syscache.c | 3 +-
src/include/utils/catcache.h | 3 +-
src/include/utils/inval.h | 6 +
src/test/isolation/expected/inplace-inval.out | 10 +-
src/test/isolation/specs/inplace-inval.spec | 12 +-
src/tools/pgindent/typedefs.list | 4 +-
12 files changed, 292 insertions(+), 120 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.cindex a7267fb..6970bdc 100644--- a/src/backend/access/heap/heapam.c+++ b/src/backend/access/heap/heapam.c@@ -6384,6 +6384,24 @@ 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);++ /*+ * 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();
@@ -6427,17 +6445,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);
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.cindex 3f0f711..395650d 100644--- a/src/backend/access/transam/xact.c+++ b/src/backend/access/transam/xact.c@@ -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)
{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.cindex e4fd1fd..f0dd962 100644--- a/src/backend/catalog/index.c+++ b/src/backend/catalog/index.c@@ -2895,12 +2895,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);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.cindex 1c0058f..e1753dd 100644--- a/src/backend/replication/logical/decode.c+++ b/src/backend/replication/logical/decode.c@@ -547,23 +547,19 @@ 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- * 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;-- SnapBuildProcessChange(builder, xid, buf->origptr);- ReorderBufferXidSetCatalogChanges(ctx->reorder, xid, buf->origptr);
break;
case XLOG_HEAP_CONFIRM:
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.cindex 5979030..091b061 100644--- a/src/backend/utils/cache/catcache.c+++ b/src/backend/utils/cache/catcache.c@@ -2131,7 +2131,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;
@@ -2172,7 +2173,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)
{
@@ -2181,7 +2182,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);
}
}
}
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.cindex e281a45..c793be5 100644--- a/src/backend/utils/cache/inval.c+++ b/src/backend/utils/cache/inval.c@@ -99,6 +99,10 @@
* 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
@@ -1011,6 +1054,45 @@ AtEOXact_Inval(bool isCommit)
}
/*
+ * 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 */+}++/*
* AtEOSubXact_Inval
* Process queued-up invalidation messages at end of subtransaction.
*
@@ -1032,9 +1114,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 +1161,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 +1209,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 +1217,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 +1255,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 +1265,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 +1343,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);
}
/*
@@ -1276,14 +1399,13 @@ CacheInvalidateCatalog(Oid catalogId)
{
Oid databaseId;
- PrepareInvalidationState();-
if (IsSharedRelation(catalogId))
databaseId = InvalidOid;
else
databaseId = MyDatabaseId;
- RegisterCatalogInvalidation(databaseId, catalogId);+ RegisterCatalogInvalidation(PrepareInvalidationState(),+ databaseId, catalogId);
}
/*
@@ -1301,15 +1423,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 +1443,8 @@ CacheInvalidateRelcache(Relation relation)
void
CacheInvalidateRelcacheAll(void)
{
- PrepareInvalidationState();-- RegisterRelcacheInvalidation(InvalidOid, InvalidOid);+ RegisterRelcacheInvalidation(PrepareInvalidationState(),+ InvalidOid, InvalidOid);
}
/*
@@ -1338,14 +1458,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 +1478,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 +1666,7 @@ LogLogicalInvalidations()
if (transInvalInfo == NULL)
return;
- ProcessInvalidationMessagesMulti(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessInvalidationMessagesMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
MakeSharedInvalidMessagesArray);
Assert(!(numSharedInvalidMessagesArray > 0 &&
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.cindex 7d573b6..ce5d2e8 100644--- a/src/backend/utils/cache/syscache.c+++ b/src/backend/utils/cache/syscache.c@@ -1260,8 +1260,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".
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.hindex ddc2762..c4fd4d1 100644--- a/src/include/utils/catcache.h+++ b/src/include/utils/catcache.h@@ -223,7 +223,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);
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.hindex 877e66c..c89ba72 100644--- a/src/include/utils/inval.h+++ b/src/include/utils/inval.h@@ -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);
diff --git a/src/test/isolation/expected/inplace-inval.out b/src/test/isolation/expected/inplace-inval.outindex e68eca5..c35895a 100644--- a/src/test/isolation/expected/inplace-inval.out+++ b/src/test/isolation/expected/inplace-inval.out@@ -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;
diff --git a/src/test/isolation/specs/inplace-inval.spec b/src/test/isolation/specs/inplace-inval.specindex 96954fd..b99112d 100644--- a/src/test/isolation/specs/inplace-inval.spec+++ b/src/test/isolation/specs/inplace-inval.spec@@ -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
commit 89c3673 (HEAD, zzy_test-commit-REL_16_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Tue Oct 22 14:42:05 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 17:46:53 2024 -0700
For inplace update, send nontransactional invalidations.
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
Conflicts:
src/backend/commands/event_trigger.c
src/backend/utils/cache/inval.c
---
src/backend/access/heap/heapam.c | 43 +++-
src/backend/access/transam/xact.c | 26 ++-
src/backend/catalog/index.c | 11 +-
src/backend/replication/logical/decode.c | 26 +--
src/backend/utils/cache/catcache.c | 7 +-
src/backend/utils/cache/inval.c | 304 ++++++++++++++++++--------
src/backend/utils/cache/syscache.c | 3 +-
src/include/utils/catcache.h | 3 +-
src/include/utils/inval.h | 6 +
src/test/isolation/expected/inplace-inval.out | 10 +-
src/test/isolation/specs/inplace-inval.spec | 12 +-
src/tools/pgindent/typedefs.list | 1 +
12 files changed, 320 insertions(+), 132 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.cindex 75e8216..fd17d1c 100644--- a/src/backend/access/heap/heapam.c+++ b/src/backend/access/heap/heapam.c@@ -6192,6 +6192,24 @@ 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);++ /*+ * 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();
@@ -6235,17 +6253,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);
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.cindex 4a2ea4a..91dbfcc 100644--- a/src/backend/access/transam/xact.c+++ b/src/backend/access/transam/xact.c@@ -1337,14 +1337,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)
{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.cindex ec56bb6..b0ffe94 100644--- a/src/backend/catalog/index.c+++ b/src/backend/catalog/index.c@@ -2915,12 +2915,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);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.cindex d91055a..70af02c 100644--- a/src/backend/replication/logical/decode.c+++ b/src/backend/replication/logical/decode.c@@ -511,23 +511,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:
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.cindex 000e81a..9f249fd 100644--- a/src/backend/utils/cache/catcache.c+++ b/src/backend/utils/cache/catcache.c@@ -2219,7 +2219,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;
@@ -2260,7 +2261,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)
{
@@ -2269,7 +2270,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);
}
}
}
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.cindex 0008826..7ad69ee7 100644--- a/src/backend/utils/cache/inval.c+++ b/src/backend/utils/cache/inval.c@@ -94,6 +94,10 @@
* 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.
@@ -131,13 +135,15 @@
/*
* 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
@@ -148,7 +154,9 @@
* 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
@@ -201,7 +209,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
@@ -217,26 +225,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;
@@ -544,9 +562,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);
}
@@ -556,10 +577,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);
}
/*
@@ -568,10 +588,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
@@ -588,7 +607,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;
}
/*
@@ -598,10 +617,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);
}
/*
@@ -791,14 +809,18 @@ AcceptInvalidationMessages(void)
* 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,
@@ -821,7 +843,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");
/*
@@ -830,8 +852,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
@@ -847,6 +869,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;
}
/*
@@ -904,7 +961,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
@@ -915,7 +972,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
* not new ones.
*/
nummsgs = NumMessagesInGroup(&transInvalInfo->PriorCmdInvalidMsgs) +
- NumMessagesInGroup(&transInvalInfo->CurrentCmdInvalidMsgs);+ NumMessagesInGroup(&transInvalInfo->ii.CurrentCmdInvalidMsgs);
*msgs = msgarray = (SharedInvalidationMessage *)
MemoryContextAlloc(CurTransactionContext,
@@ -928,7 +985,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
msgs,
n * sizeof(SharedInvalidationMessage)),
nmsgs += n));
- ProcessMessageSubGroupMulti(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessMessageSubGroupMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
CatCacheMsgs,
(memcpy(msgarray + nmsgs,
msgs,
@@ -940,7 +997,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
msgs,
n * sizeof(SharedInvalidationMessage)),
nmsgs += n));
- ProcessMessageSubGroupMulti(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessMessageSubGroupMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
RelCacheMsgs,
(memcpy(msgarray + nmsgs,
msgs,
@@ -1027,7 +1084,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;
@@ -1041,16 +1100,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
@@ -1064,6 +1123,44 @@ AtEOXact_Inval(bool isCommit)
}
/*
+ * 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.
*
@@ -1085,9 +1182,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;
@@ -1128,12 +1236,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;
@@ -1180,7 +1288,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 */
@@ -1188,26 +1296,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;
@@ -1231,11 +1334,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
@@ -1244,11 +1344,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
@@ -1321,7 +1422,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);
}
/*
@@ -1340,14 +1478,13 @@ CacheInvalidateCatalog(Oid catalogId)
{
Oid databaseId;
- PrepareInvalidationState();-
if (IsSharedRelation(catalogId))
databaseId = InvalidOid;
else
databaseId = MyDatabaseId;
- RegisterCatalogInvalidation(databaseId, catalogId);+ RegisterCatalogInvalidation(PrepareInvalidationState(),+ databaseId, catalogId);
}
/*
@@ -1365,15 +1502,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);
}
/*
@@ -1386,9 +1522,8 @@ CacheInvalidateRelcache(Relation relation)
void
CacheInvalidateRelcacheAll(void)
{
- PrepareInvalidationState();-- RegisterRelcacheInvalidation(InvalidOid, InvalidOid);+ RegisterRelcacheInvalidation(PrepareInvalidationState(),+ InvalidOid, InvalidOid);
}
/*
@@ -1402,14 +1537,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);
}
/*
@@ -1423,8 +1557,6 @@ CacheInvalidateRelcacheByRelid(Oid relid)
{
HeapTuple tup;
- PrepareInvalidationState();-
tup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tup))
elog(ERROR, "cache lookup failed for relation %u", relid);
@@ -1614,7 +1746,7 @@ LogLogicalInvalidations(void)
if (transInvalInfo == NULL)
return;
- group = &transInvalInfo->CurrentCmdInvalidMsgs;+ group = &transInvalInfo->ii.CurrentCmdInvalidMsgs;
nmsgs = NumMessagesInGroup(group);
if (nmsgs > 0)
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.cindex 697b17b..b0cc6a0 100644--- a/src/backend/utils/cache/syscache.c+++ b/src/backend/utils/cache/syscache.c@@ -951,8 +951,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".
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.hindex a32d722..9204663 100644--- a/src/include/utils/catcache.h+++ b/src/include/utils/catcache.h@@ -228,7 +228,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);
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.hindex 14b4eac..e807779 100644--- a/src/include/utils/inval.h+++ b/src/include/utils/inval.h@@ -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);
diff --git a/src/test/isolation/expected/inplace-inval.out b/src/test/isolation/expected/inplace-inval.outindex e68eca5..c35895a 100644--- a/src/test/isolation/expected/inplace-inval.out+++ b/src/test/isolation/expected/inplace-inval.out@@ -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;
diff --git a/src/test/isolation/specs/inplace-inval.spec b/src/test/isolation/specs/inplace-inval.specindex 96954fd..b99112d 100644--- a/src/test/isolation/specs/inplace-inval.spec+++ b/src/test/isolation/specs/inplace-inval.spec@@ -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
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.listindex 4357043..fcaa2e6 100644--- a/src/tools/pgindent/typedefs.list+++ b/src/tools/pgindent/typedefs.list@@ -1205,6 +1205,7 @@ InternalGrant
Interval
IntoClause
InvalMessageArray
+InvalidationInfo
InvalidationMsgsGroup
IpcMemoryId
IpcMemoryKey
commit f52d713 (HEAD, zzy_test-commit-REL_17_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Tue Oct 22 12:59:28 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 17:46:13 2024 -0700
For inplace update, send nontransactional invalidations.
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.
Core code no longer needs XLOG_INVALIDATIONS, but this leaves removing
it for future work. Extensions could be relying on that mechanism, so
that removal would not be back-patch material. Back-patch to v12 (all
supported versions). The back branch versions don't change WAL, because
those branches just added end-of-recovery SIResetAll().
Reviewed by Nitin Motiani and (in earlier versions) Andres Freund.
Discussion: https://postgr.es/m/20240523000548.58.nmisch@google.com
Conflicts:
src/backend/access/heap/heapam_xlog.c
src/include/access/xlog_internal.h
---
src/backend/access/heap/heapam.c | 43 +++-
src/backend/access/transam/xact.c | 26 ++-
src/backend/catalog/index.c | 11 +-
src/backend/commands/event_trigger.c | 5 -
src/backend/replication/logical/decode.c | 26 +--
src/backend/utils/cache/catcache.c | 7 +-
src/backend/utils/cache/inval.c | 304 ++++++++++++++++++--------
src/backend/utils/cache/syscache.c | 3 +-
src/include/utils/catcache.h | 3 +-
src/include/utils/inval.h | 6 +
src/test/isolation/expected/inplace-inval.out | 10 +-
src/test/isolation/specs/inplace-inval.spec | 12 +-
src/tools/pgindent/typedefs.list | 1 +
13 files changed, 320 insertions(+), 137 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.cindex cb65faa..517b374 100644--- a/src/backend/access/heap/heapam.c+++ b/src/backend/access/heap/heapam.c@@ -6352,6 +6352,24 @@ 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);++ /*+ * 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();
@@ -6395,17 +6413,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);
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.cindex 4cecf63..053a200 100644--- a/src/backend/access/transam/xact.c+++ b/src/backend/access/transam/xact.c@@ -1358,14 +1358,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)
{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.cindex 3eaf1f2..2156e21 100644--- a/src/backend/catalog/index.c+++ b/src/backend/catalog/index.c@@ -2889,12 +2889,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);
}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.cindex 05a6de6..a586d24 100644--- a/src/backend/commands/event_trigger.c+++ b/src/backend/commands/event_trigger.c@@ -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);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.cindex 8ec5adf..03f1617 100644--- a/src/backend/replication/logical/decode.c+++ b/src/backend/replication/logical/decode.c@@ -508,23 +508,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:
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.cindex 111d8a2..ea8ca0e 100644--- a/src/backend/utils/cache/catcache.c+++ b/src/backend/utils/cache/catcache.c@@ -2288,7 +2288,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;
@@ -2329,7 +2330,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)
{
@@ -2338,7 +2339,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);
}
}
}
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.cindex 603aa41..93bfa1e 100644--- a/src/backend/utils/cache/inval.c+++ b/src/backend/utils/cache/inval.c@@ -94,6 +94,10 @@
* 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.
@@ -130,13 +134,15 @@
/*
* 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
@@ -147,7 +153,9 @@
* 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,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
msgs,
n * sizeof(SharedInvalidationMessage)),
nmsgs += n));
- ProcessMessageSubGroupMulti(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessMessageSubGroupMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
RelCacheMsgs,
(memcpy(msgarray + nmsgs,
msgs,
@@ -1024,7 +1081,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 +1097,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
@@ -1061,6 +1120,44 @@ AtEOXact_Inval(bool isCommit)
}
/*
+ * 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 +1179,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;
@@ -1125,12 +1233,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 +1285,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 +1293,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 +1331,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 +1341,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 +1419,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 +1475,13 @@ CacheInvalidateCatalog(Oid catalogId)
{
Oid databaseId;
- PrepareInvalidationState();-
if (IsSharedRelation(catalogId))
databaseId = InvalidOid;
else
databaseId = MyDatabaseId;
- RegisterCatalogInvalidation(databaseId, catalogId);+ RegisterCatalogInvalidation(PrepareInvalidationState(),+ databaseId, catalogId);
}
/*
@@ -1362,15 +1499,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 +1519,8 @@ CacheInvalidateRelcache(Relation relation)
void
CacheInvalidateRelcacheAll(void)
{
- PrepareInvalidationState();-- RegisterRelcacheInvalidation(InvalidOid, InvalidOid);+ RegisterRelcacheInvalidation(PrepareInvalidationState(),+ InvalidOid, InvalidOid);
}
/*
@@ -1399,14 +1534,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 +1554,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 +1743,7 @@ LogLogicalInvalidations(void)
if (transInvalInfo == NULL)
return;
- group = &transInvalInfo->CurrentCmdInvalidMsgs;+ group = &transInvalInfo->ii.CurrentCmdInvalidMsgs;
nmsgs = NumMessagesInGroup(group);
if (nmsgs > 0)
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.cindex 50c9440..f41b1c2 100644--- a/src/backend/utils/cache/syscache.c+++ b/src/backend/utils/cache/syscache.c@@ -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".
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.hindex 3fb9647..8f04bb8 100644--- a/src/include/utils/catcache.h+++ b/src/include/utils/catcache.h@@ -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 */
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.hindex 24695fa..3390e7a 100644--- a/src/include/utils/inval.h+++ b/src/include/utils/inval.h@@ -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);
diff --git a/src/test/isolation/expected/inplace-inval.out b/src/test/isolation/expected/inplace-inval.outindex e68eca5..c35895a 100644--- a/src/test/isolation/expected/inplace-inval.out+++ b/src/test/isolation/expected/inplace-inval.out@@ -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;
diff --git a/src/test/isolation/specs/inplace-inval.spec b/src/test/isolation/specs/inplace-inval.specindex 96954fd..b99112d 100644--- a/src/test/isolation/specs/inplace-inval.spec+++ b/src/test/isolation/specs/inplace-inval.spec@@ -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
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.listindex 5628b3f..3b57e78 100644--- a/src/tools/pgindent/typedefs.list+++ b/src/tools/pgindent/typedefs.list@@ -1253,6 +1253,7 @@ Interval
IntervalAggState
IntoClause
InvalMessageArray
+InvalidationInfo
InvalidationMsgsGroup
IpcMemoryId
IpcMemoryKey
Author: Noah Misch <noah@leadboat.com>
Commit: Noah Misch <noah@leadboat.com>
For inplace update, send nontransactional invalidations.
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.comdiff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.cindex da5e656..7c82a95 100644--- a/src/backend/access/heap/heapam.c+++ b/src/backend/access/heap/heapam.c@@ -6327,6 +6327,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;
@@ -6334,6 +6337,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();
@@ -6363,9 +6389,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);
@@ -6377,17 +6410,28 @@ heap_inplace_update_and_unlock(Relation relation,
PageSetLSN(BufferGetPage(buffer), recptr);
}
- END_CRIT_SECTION();-- heap_inplace_unlock(relation, oldtup, buffer);+ LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/*
- * 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.+ * Send invalidations to shared queue. SearchSysCacheLocked1() assumes we+ * do this before UnlockTuple().
*
- * XXX ROLLBACK discards the invalidation. See test inplace-inval.spec.+ * 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);++ AcceptInvalidationMessages(); /* local processing of just-sent inval */++ /*+ * 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);
diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.cindex 6dae723..c5208f3 100644--- a/src/backend/access/heap/heapam_xlog.c+++ b/src/backend/access/heap/heapam_xlog.c@@ -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
diff --git a/src/backend/access/rmgrdesc/heapdesc.c b/src/backend/access/rmgrdesc/heapdesc.cindex 5f5673e..f31cc3a 100644--- a/src/backend/access/rmgrdesc/heapdesc.c+++ b/src/backend/access/rmgrdesc/heapdesc.c@@ -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);
}
}
diff --git a/src/backend/access/rmgrdesc/standbydesc.c b/src/backend/access/rmgrdesc/standbydesc.cindex 25f870b..32e509a 100644--- a/src/backend/access/rmgrdesc/standbydesc.c+++ b/src/backend/access/rmgrdesc/standbydesc.c@@ -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,
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.cindex 87700c7..20b7307 100644--- a/src/backend/access/transam/xact.c+++ b/src/backend/access/transam/xact.c@@ -1368,14 +1368,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)
{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.cindex 12822d0..9162b9f 100644--- a/src/backend/catalog/index.c+++ b/src/backend/catalog/index.c@@ -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);
}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.cindex 05a6de6..a586d24 100644--- a/src/backend/commands/event_trigger.c+++ b/src/backend/commands/event_trigger.c@@ -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);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.cindex d687cee..e73576a 100644--- a/src/backend/replication/logical/decode.c+++ b/src/backend/replication/logical/decode.c@@ -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:
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.cindex 10276aa..ee303dc 100644--- a/src/backend/utils/cache/catcache.c+++ b/src/backend/utils/cache/catcache.c@@ -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);
}
}
}
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.cindex 603aa41..986850c 100644--- a/src/backend/utils/cache/inval.c+++ b/src/backend/utils/cache/inval.c@@ -94,6 +94,10 @@
* 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.
@@ -130,13 +134,15 @@
/*
* 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
@@ -147,7 +153,9 @@
* 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;
- /* Events emitted by previous commands of this (sub)transaction */- InvalidationMsgsGroup PriorCmdInvalidMsgs;-
/* 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;++ /* 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
@@ -1061,6 +1164,44 @@ AtEOXact_Inval(bool isCommit)
}
/*
+ * 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;- /* 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;
@@ -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)
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.cindex 50c9440..f41b1c2 100644--- a/src/backend/utils/cache/syscache.c+++ b/src/backend/utils/cache/syscache.c@@ -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".
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.hindex 42736f3..4591e9a 100644--- a/src/include/access/heapam_xlog.h+++ b/src/include/access/heapam_xlog.h@@ -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
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.hindex 5ef244b..d9cf51a 100644--- a/src/include/access/xlog_internal.h+++ b/src/include/access/xlog_internal.h@@ -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
{
diff --git a/src/include/storage/sinval.h b/src/include/storage/sinval.hindex 8f5744b..c812237 100644--- a/src/include/storage/sinval.h+++ b/src/include/storage/sinval.h@@ -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);
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.hindex 3fb9647..8f04bb8 100644--- a/src/include/utils/catcache.h+++ b/src/include/utils/catcache.h@@ -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 */
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.hindex 24695fa..3390e7a 100644--- a/src/include/utils/inval.h+++ b/src/include/utils/inval.h@@ -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);
diff --git a/src/test/isolation/expected/inplace-inval.out b/src/test/isolation/expected/inplace-inval.outindex e68eca5..c35895a 100644--- a/src/test/isolation/expected/inplace-inval.out+++ b/src/test/isolation/expected/inplace-inval.out@@ -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;
diff --git a/src/test/isolation/specs/inplace-inval.spec b/src/test/isolation/specs/inplace-inval.specindex 96954fd..b99112d 100644--- a/src/test/isolation/specs/inplace-inval.spec+++ b/src/test/isolation/specs/inplace-inval.spec@@ -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
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.listindex 54bf29b..1fb4801 100644--- a/src/tools/pgindent/typedefs.list+++ b/src/tools/pgindent/typedefs.list@@ -1258,6 +1258,7 @@ Interval
IntervalAggState
IntoClause
InvalMessageArray
+InvalidationInfo
InvalidationMsgsGroup
IpcMemoryId
IpcMemoryKey
Attachments:
[text/plain] inplace155-backbranch-inval-v1_14.patch (5.1K, ../20241024025357.a8.nmisch@google.com/2-inplace155-backbranch-inval-v1_14.patch)
download | inline diff:
commit 5704b32 (HEAD, zzy_test-commit-REL_14_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Wed Oct 23 15:27:13 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 15:33:18 2024 -0700
At end of recovery, reset all sinval-managed caches.
An inplace update's invalidation messages are part of its transaction's
commit record. However, the update survives even if its transaction
aborts or we stop recovery before replaying its transaction commit.
After recovery, a backend that started in recovery could update the row
without incorporating the inplace update. That could result in a table
with an index, yet relhasindex=f. That is a source of index corruption.
This bulk invalidation avoids the functional consequences. A future
change can fix the !RecoveryInProgress() scenario without changing the
WAL format. Back-patch to v17 - v12 (all supported versions). v18 will
instead add invalidations to WAL.
Reviewed by FIXME.
Discussion: https://postgr.es/m/20240618152349.7f.nmisch@google.com
Conflicts:
src/backend/access/transam/xlog.c
Conflicts:
src/backend/access/transam/xlog.c
---
src/backend/access/transam/xlog.c | 25 ++++++++++++++++++++++
src/backend/storage/ipc/sinvaladt.c | 41 +++++++++++++++++++++++++++++++++++++
src/include/storage/sinvaladt.h | 1 +
3 files changed, 67 insertions(+)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.cindex 7654ad7..f1a795b 100644--- a/src/backend/access/transam/xlog.c+++ b/src/backend/access/transam/xlog.c@@ -70,6 +70,7 @@
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/reinit.h"
+#include "storage/sinvaladt.h"
#include "storage/smgr.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -8020,6 +8021,30 @@ StartupXLOG(void)
}
/*
+ * Invalidate all sinval-managed caches before READ WRITE transactions+ * begin. The xl_heap_inplace WAL record doesn't store sufficient data+ * for invalidations. The commit record, if any, has the invalidations.+ * However, the inplace update is permanent, whether or not we reach a+ * commit record. Fortunately, read-only transactions tolerate caches not+ * reflecting the latest inplace updates. Read-only transactions+ * experience the notable inplace updates as follows:+ *+ * - relhasindex=true affects readers only after the CREATE INDEX+ * transaction commit makes an index fully available to them.+ *+ * - datconnlimit=DATCONNLIMIT_INVALID_DB affects readers only at+ * InitPostgres() time, and that read does not use a cache.+ *+ * - relfrozenxid, datfrozenxid, relminmxid, and datminmxid have no effect+ * on readers.+ *+ * Hence, hot standby queries (all READ ONLY) function correctly without+ * the missing invalidations. This avoided changing the WAL format in+ * back branches.+ */+ SIResetAll();++ /*
* Preallocate additional log files, if wanted.
*/
PreallocXlogFiles(EndOfLog);
diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.cindex 946bd8e..f0e2fb9 100644--- a/src/backend/storage/ipc/sinvaladt.c+++ b/src/backend/storage/ipc/sinvaladt.c@@ -748,6 +748,47 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
}
}
+/*+ * SIResetAll+ * Mark all active backends as "reset"+ *+ * Use this when we don't know what needs to be invalidated. It's a+ * cluster-wide InvalidateSystemCaches(). This was a back-branch-only remedy+ * to avoid a WAL format change.+ *+ * The implementation is like SICleanupQueue(false, MAXNUMMESSAGES + 1), with+ * one addition. SICleanupQueue() assumes minFree << MAXNUMMESSAGES, so it+ * assumes hasMessages==true for any backend it resets. We're resetting even+ * fully-caught-up backends, so we set hasMessages.+ */+void+SIResetAll(void)+{+ SISeg *segP = shmInvalBuffer;+ int i;++ LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);+ LWLockAcquire(SInvalReadLock, LW_EXCLUSIVE);++ for (i = 0; i < segP->lastBackend; i++)+ {+ ProcState *stateP = &segP->procState[i];++ if (stateP->procPid == 0 || stateP->sendOnly)+ continue;++ /* Consuming the reset will update "nextMsgNum" and "signaled". */+ stateP->resetState = true;+ stateP->hasMessages = true;+ }++ segP->minMsgNum = segP->maxMsgNum;+ segP->nextThreshold = CLEANUP_MIN;++ LWLockRelease(SInvalReadLock);+ LWLockRelease(SInvalWriteLock);+}+
/*
* GetNextLocalTransactionId --- allocate a new LocalTransactionId
diff --git a/src/include/storage/sinvaladt.h b/src/include/storage/sinvaladt.hindex 14148bf..07d229a 100644--- a/src/include/storage/sinvaladt.h+++ b/src/include/storage/sinvaladt.h@@ -37,6 +37,7 @@ extern void BackendIdGetTransactionIds(int backendID, TransactionId *xid, Transa
extern void SIInsertDataEntries(const SharedInvalidationMessage *data, int n);
extern int SIGetDataEntries(SharedInvalidationMessage *data, int datasize);
extern void SICleanupQueue(bool callerHasWriteLock, int minFree);
+extern void SIResetAll(void);
extern LocalTransactionId GetNextLocalTransactionId(void);
[text/plain] inplace155-backbranch-inval-v1_16.patch (5.1K, ../20241024025357.a8.nmisch@google.com/3-inplace155-backbranch-inval-v1_16.patch)
download | inline diff:
commit 1bfa06e (HEAD, zzy_test-commit-REL_16_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Wed Oct 23 15:27:13 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 15:31:54 2024 -0700
At end of recovery, reset all sinval-managed caches.
An inplace update's invalidation messages are part of its transaction's
commit record. However, the update survives even if its transaction
aborts or we stop recovery before replaying its transaction commit.
After recovery, a backend that started in recovery could update the row
without incorporating the inplace update. That could result in a table
with an index, yet relhasindex=f. That is a source of index corruption.
This bulk invalidation avoids the functional consequences. A future
change can fix the !RecoveryInProgress() scenario without changing the
WAL format. Back-patch to v17 - v12 (all supported versions). v18 will
instead add invalidations to WAL.
Reviewed by FIXME.
Discussion: https://postgr.es/m/20240618152349.7f.nmisch@google.com
Conflicts:
src/backend/access/transam/xlog.c
---
src/backend/access/transam/xlog.c | 25 ++++++++++++++++++++++
src/backend/storage/ipc/sinvaladt.c | 41 +++++++++++++++++++++++++++++++++++++
src/include/storage/sinvaladt.h | 1 +
3 files changed, 67 insertions(+)
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.cindex a19ba71..46faf40 100644--- a/src/backend/access/transam/xlog.c+++ b/src/backend/access/transam/xlog.c@@ -94,6 +94,7 @@
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/reinit.h"
+#include "storage/sinvaladt.h"
#include "storage/smgr.h"
#include "storage/spin.h"
#include "storage/sync.h"
@@ -5660,6 +5661,30 @@ StartupXLOG(void)
XLogCtl->LogwrtRqst.Flush = EndOfLog;
/*
+ * Invalidate all sinval-managed caches before READ WRITE transactions+ * begin. The xl_heap_inplace WAL record doesn't store sufficient data+ * for invalidations. The commit record, if any, has the invalidations.+ * However, the inplace update is permanent, whether or not we reach a+ * commit record. Fortunately, read-only transactions tolerate caches not+ * reflecting the latest inplace updates. Read-only transactions+ * experience the notable inplace updates as follows:+ *+ * - relhasindex=true affects readers only after the CREATE INDEX+ * transaction commit makes an index fully available to them.+ *+ * - datconnlimit=DATCONNLIMIT_INVALID_DB affects readers only at+ * InitPostgres() time, and that read does not use a cache.+ *+ * - relfrozenxid, datfrozenxid, relminmxid, and datminmxid have no effect+ * on readers.+ *+ * Hence, hot standby queries (all READ ONLY) function correctly without+ * the missing invalidations. This avoided changing the WAL format in+ * back branches.+ */+ SIResetAll();++ /*
* Preallocate additional log files, if wanted.
*/
PreallocXlogFiles(EndOfLog, newTLI);
diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.cindex 3d97c75..099c292 100644--- a/src/backend/storage/ipc/sinvaladt.c+++ b/src/backend/storage/ipc/sinvaladt.c@@ -762,6 +762,47 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
}
}
+/*+ * SIResetAll+ * Mark all active backends as "reset"+ *+ * Use this when we don't know what needs to be invalidated. It's a+ * cluster-wide InvalidateSystemCaches(). This was a back-branch-only remedy+ * to avoid a WAL format change.+ *+ * The implementation is like SICleanupQueue(false, MAXNUMMESSAGES + 1), with+ * one addition. SICleanupQueue() assumes minFree << MAXNUMMESSAGES, so it+ * assumes hasMessages==true for any backend it resets. We're resetting even+ * fully-caught-up backends, so we set hasMessages.+ */+void+SIResetAll(void)+{+ SISeg *segP = shmInvalBuffer;+ int i;++ LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);+ LWLockAcquire(SInvalReadLock, LW_EXCLUSIVE);++ for (i = 0; i < segP->lastBackend; i++)+ {+ ProcState *stateP = &segP->procState[i];++ if (stateP->procPid == 0 || stateP->sendOnly)+ continue;++ /* Consuming the reset will update "nextMsgNum" and "signaled". */+ stateP->resetState = true;+ stateP->hasMessages = true;+ }++ segP->minMsgNum = segP->maxMsgNum;+ segP->nextThreshold = CLEANUP_MIN;++ LWLockRelease(SInvalReadLock);+ LWLockRelease(SInvalWriteLock);+}+
/*
* GetNextLocalTransactionId --- allocate a new LocalTransactionId
diff --git a/src/include/storage/sinvaladt.h b/src/include/storage/sinvaladt.hindex db38819..1f9eaf2 100644--- a/src/include/storage/sinvaladt.h+++ b/src/include/storage/sinvaladt.h@@ -39,6 +39,7 @@ extern void BackendIdGetTransactionIds(int backendID, TransactionId *xid,
extern void SIInsertDataEntries(const SharedInvalidationMessage *data, int n);
extern int SIGetDataEntries(SharedInvalidationMessage *data, int datasize);
extern void SICleanupQueue(bool callerHasWriteLock, int minFree);
+extern void SIResetAll(void);
extern LocalTransactionId GetNextLocalTransactionId(void);
[text/plain] inplace155-backbranch-inval-v1_17.patch (4.6K, ../20241024025357.a8.nmisch@google.com/4-inplace155-backbranch-inval-v1_17.patch)
download | inline diff:
Author: Noah Misch <noah@leadboat.com>
Commit: Noah Misch <noah@leadboat.com>
At end of recovery, reset all sinval-managed caches.
An inplace update's invalidation messages are part of its transaction's
commit record. However, the update survives even if its transaction
aborts or we stop recovery before replaying its transaction commit.
After recovery, a backend that started in recovery could update the row
without incorporating the inplace update. That could result in a table
with an index, yet relhasindex=f. That is a source of index corruption.
This bulk invalidation avoids the functional consequences. A future
change can fix the !RecoveryInProgress() scenario without changing the
WAL format. Back-patch to v17 - v12 (all supported versions). v18 will
instead add invalidations to WAL.
Reviewed by FIXME.
Discussion: https://postgr.es/m/20240618152349.7f.nmisch@google.com
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.cindex 7f13602..c1dd8b5 100644--- a/src/backend/access/transam/xlog.c+++ b/src/backend/access/transam/xlog.c@@ -92,6 +92,7 @@
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/reinit.h"
+#include "storage/sinvaladt.h"
#include "storage/spin.h"
#include "storage/sync.h"
#include "utils/guc_hooks.h"
@@ -6038,6 +6039,30 @@ StartupXLOG(void)
XLogCtl->LogwrtRqst.Flush = EndOfLog;
/*
+ * Invalidate all sinval-managed caches before READ WRITE transactions+ * begin. The xl_heap_inplace WAL record doesn't store sufficient data+ * for invalidations. The commit record, if any, has the invalidations.+ * However, the inplace update is permanent, whether or not we reach a+ * commit record. Fortunately, read-only transactions tolerate caches not+ * reflecting the latest inplace updates. Read-only transactions+ * experience the notable inplace updates as follows:+ *+ * - relhasindex=true affects readers only after the CREATE INDEX+ * transaction commit makes an index fully available to them.+ *+ * - datconnlimit=DATCONNLIMIT_INVALID_DB affects readers only at+ * InitPostgres() time, and that read does not use a cache.+ *+ * - relfrozenxid, datfrozenxid, relminmxid, and datminmxid have no effect+ * on readers.+ *+ * Hence, hot standby queries (all READ ONLY) function correctly without+ * the missing invalidations. This avoided changing the WAL format in+ * back branches.+ */+ SIResetAll();++ /*
* Preallocate additional log files, if wanted.
*/
PreallocXlogFiles(EndOfLog, newTLI);
diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.cindex b486d8d..2712129 100644--- a/src/backend/storage/ipc/sinvaladt.c+++ b/src/backend/storage/ipc/sinvaladt.c@@ -683,6 +683,48 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
}
}
+/*+ * SIResetAll+ * Mark all active backends as "reset"+ *+ * Use this when we don't know what needs to be invalidated. It's a+ * cluster-wide InvalidateSystemCaches(). This was a back-branch-only remedy+ * to avoid a WAL format change.+ *+ * The implementation is like SICleanupQueue(false, MAXNUMMESSAGES + 1), with+ * one addition. SICleanupQueue() assumes minFree << MAXNUMMESSAGES, so it+ * assumes hasMessages==true for any backend it resets. We're resetting even+ * fully-caught-up backends, so we set hasMessages.+ */+void+SIResetAll(void)+{+ SISeg *segP = shmInvalBuffer;+ int i;++ LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);+ LWLockAcquire(SInvalReadLock, LW_EXCLUSIVE);++ for (i = 0; i < segP->numProcs; i++)+ {+ ProcState *stateP = &segP->procState[segP->pgprocnos[i]];++ Assert(stateP->procPid != 0);+ if (stateP->sendOnly)+ continue;++ /* Consuming the reset will update "nextMsgNum" and "signaled". */+ stateP->resetState = true;+ stateP->hasMessages = true;+ }++ segP->minMsgNum = segP->maxMsgNum;+ segP->nextThreshold = CLEANUP_MIN;++ LWLockRelease(SInvalReadLock);+ LWLockRelease(SInvalWriteLock);+}+
/*
* GetNextLocalTransactionId --- allocate a new LocalTransactionId
diff --git a/src/include/storage/sinvaladt.h b/src/include/storage/sinvaladt.hindex c3c97b3..7c8f450 100644--- a/src/include/storage/sinvaladt.h+++ b/src/include/storage/sinvaladt.h@@ -35,6 +35,7 @@ extern void SharedInvalBackendInit(bool sendOnly);
extern void SIInsertDataEntries(const SharedInvalidationMessage *data, int n);
extern int SIGetDataEntries(SharedInvalidationMessage *data, int datasize);
extern void SICleanupQueue(bool callerHasWriteLock, int minFree);
+extern void SIResetAll(void);
extern LocalTransactionId GetNextLocalTransactionId(void);
[text/plain] inplace160-inval-durability-inplace-v6_13.patch (30.4K, ../20241024025357.a8.nmisch@google.com/5-inplace160-inval-durability-inplace-v6_13.patch)
download | inline diff:
commit efc6d94 (HEAD, zzy_test-commit-REL_13_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Wed Oct 23 15:35:02 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 17:54:08 2024 -0700
For inplace update, send nontransactional invalidations.
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
Conflicts:
src/backend/replication/logical/decode.c
src/backend/utils/cache/inval.c
src/tools/pgindent/typedefs.list
Conflicts:
src/backend/utils/cache/inval.c
---
src/backend/access/heap/heapam.c | 43 ++++-
src/backend/access/transam/xact.c | 26 ++-
src/backend/catalog/index.c | 11 +-
src/backend/replication/logical/decode.c | 26 ++-
src/backend/utils/cache/catcache.c | 7 +-
src/backend/utils/cache/inval.c | 259 +++++++++++++++++++-------
src/backend/utils/cache/syscache.c | 3 +-
src/include/utils/catcache.h | 3 +-
src/include/utils/inval.h | 6 +
src/test/isolation/expected/inplace-inval.out | 10 +-
src/test/isolation/specs/inplace-inval.spec | 12 +-
src/tools/pgindent/typedefs.list | 4 +-
12 files changed, 291 insertions(+), 119 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.cindex 846650c..ce10537 100644--- a/src/backend/access/heap/heapam.c+++ b/src/backend/access/heap/heapam.c@@ -6112,6 +6112,24 @@ 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);++ /*+ * 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();
@@ -6155,17 +6173,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);
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.cindex ffe26e2..cda6e33 100644--- a/src/backend/access/transam/xact.c+++ b/src/backend/access/transam/xact.c@@ -1249,14 +1249,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)
{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.cindex 4ca86d9..9ab38b9 100644--- a/src/backend/catalog/index.c+++ b/src/backend/catalog/index.c@@ -2882,12 +2882,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);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.cindex 297eb11..b3b51a9 100644--- a/src/backend/replication/logical/decode.c+++ b/src/backend/replication/logical/decode.c@@ -460,23 +460,19 @@ 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- * 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;-- SnapBuildProcessChange(builder, xid, buf->origptr);- ReorderBufferXidSetCatalogChanges(ctx->reorder, xid, buf->origptr);
break;
case XLOG_HEAP_CONFIRM:
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.cindex b53232f..9a61890 100644--- a/src/backend/utils/cache/catcache.c+++ b/src/backend/utils/cache/catcache.c@@ -2129,7 +2129,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;
@@ -2170,7 +2171,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)
{
@@ -2179,7 +2180,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);
}
}
}
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.cindex 8fe1f0d..a0dc745 100644--- a/src/backend/utils/cache/inval.c+++ b/src/backend/utils/cache/inval.c@@ -99,6 +99,10 @@
* 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.+ *
*
* Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
@@ -150,7 +154,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
@@ -166,26 +170,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;
@@ -499,9 +513,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);
}
@@ -511,10 +528,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);
}
/*
@@ -523,10 +539,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
@@ -543,7 +558,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;
}
/*
@@ -553,10 +568,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);
}
/*
@@ -748,14 +762,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,
@@ -771,6 +789,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;
}
/*
@@ -866,7 +907,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
@@ -878,7 +919,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
*/
oldcontext = MemoryContextSwitchTo(CurTransactionContext);
- ProcessInvalidationMessagesMulti(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessInvalidationMessagesMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
MakeSharedInvalidMessagesArray);
ProcessInvalidationMessagesMulti(&transInvalInfo->PriorCmdInvalidMsgs,
MakeSharedInvalidMessagesArray);
@@ -968,7 +1009,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;
@@ -982,16 +1025,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
@@ -1007,6 +1050,45 @@ AtEOXact_Inval(bool isCommit)
}
/*
+ * 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 */+}++/*
* AtEOSubXact_Inval
* Process queued-up invalidation messages at end of subtransaction.
*
@@ -1028,9 +1110,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;
@@ -1064,8 +1157,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;
@@ -1112,29 +1205,24 @@ CommandEndInvalidationMessages(void)
if (transInvalInfo == NULL)
return;
- ProcessInvalidationMessages(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessInvalidationMessages(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
LocalExecuteInvalidationMessage);
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;
@@ -1158,11 +1246,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
@@ -1171,11 +1256,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
@@ -1248,7 +1334,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);
}
/*
@@ -1267,14 +1390,13 @@ CacheInvalidateCatalog(Oid catalogId)
{
Oid databaseId;
- PrepareInvalidationState();-
if (IsSharedRelation(catalogId))
databaseId = InvalidOid;
else
databaseId = MyDatabaseId;
- RegisterCatalogInvalidation(databaseId, catalogId);+ RegisterCatalogInvalidation(PrepareInvalidationState(),+ databaseId, catalogId);
}
/*
@@ -1292,15 +1414,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);
}
/*
@@ -1313,9 +1434,8 @@ CacheInvalidateRelcache(Relation relation)
void
CacheInvalidateRelcacheAll(void)
{
- PrepareInvalidationState();-- RegisterRelcacheInvalidation(InvalidOid, InvalidOid);+ RegisterRelcacheInvalidation(PrepareInvalidationState(),+ InvalidOid, InvalidOid);
}
/*
@@ -1329,14 +1449,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);
}
/*
@@ -1350,8 +1469,6 @@ CacheInvalidateRelcacheByRelid(Oid relid)
{
HeapTuple tup;
- PrepareInvalidationState();-
tup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tup))
elog(ERROR, "cache lookup failed for relation %u", relid);
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.cindex 8ad8791..e4a8fb2 100644--- a/src/backend/utils/cache/syscache.c+++ b/src/backend/utils/cache/syscache.c@@ -1249,8 +1249,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".
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.hindex f4aa316..2722173 100644--- a/src/include/utils/catcache.h+++ b/src/include/utils/catcache.h@@ -223,7 +223,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);
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.hindex 4c6b86c..3ff52e8 100644--- a/src/include/utils/inval.h+++ b/src/include/utils/inval.h@@ -27,6 +27,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);
@@ -36,6 +39,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);
diff --git a/src/test/isolation/expected/inplace-inval.out b/src/test/isolation/expected/inplace-inval.outindex e68eca5..c35895a 100644--- a/src/test/isolation/expected/inplace-inval.out+++ b/src/test/isolation/expected/inplace-inval.out@@ -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;
diff --git a/src/test/isolation/specs/inplace-inval.spec b/src/test/isolation/specs/inplace-inval.specindex 96954fd..b99112d 100644--- a/src/test/isolation/specs/inplace-inval.spec+++ b/src/test/isolation/specs/inplace-inval.spec@@ -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
[text/plain] inplace160-inval-durability-inplace-v6_14.patch (30.9K, ../20241024025357.a8.nmisch@google.com/6-inplace160-inval-durability-inplace-v6_14.patch)
download | inline diff:
commit 8133c66 (HEAD, zzy_test-commit-REL_14_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Wed Oct 23 15:35:02 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 17:50:57 2024 -0700
For inplace update, send nontransactional invalidations.
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
Conflicts:
src/backend/replication/logical/decode.c
src/backend/utils/cache/inval.c
src/tools/pgindent/typedefs.list
---
src/backend/access/heap/heapam.c | 43 ++++-
src/backend/access/transam/xact.c | 26 ++-
src/backend/catalog/index.c | 11 +-
src/backend/replication/logical/decode.c | 26 ++-
src/backend/utils/cache/catcache.c | 7 +-
src/backend/utils/cache/inval.c | 261 +++++++++++++++++++-------
src/backend/utils/cache/syscache.c | 3 +-
src/include/utils/catcache.h | 3 +-
src/include/utils/inval.h | 6 +
src/test/isolation/expected/inplace-inval.out | 10 +-
src/test/isolation/specs/inplace-inval.spec | 12 +-
src/tools/pgindent/typedefs.list | 4 +-
12 files changed, 292 insertions(+), 120 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.cindex a7267fb..6970bdc 100644--- a/src/backend/access/heap/heapam.c+++ b/src/backend/access/heap/heapam.c@@ -6384,6 +6384,24 @@ 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);++ /*+ * 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();
@@ -6427,17 +6445,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);
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.cindex 3f0f711..395650d 100644--- a/src/backend/access/transam/xact.c+++ b/src/backend/access/transam/xact.c@@ -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)
{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.cindex e4fd1fd..f0dd962 100644--- a/src/backend/catalog/index.c+++ b/src/backend/catalog/index.c@@ -2895,12 +2895,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);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.cindex 1c0058f..e1753dd 100644--- a/src/backend/replication/logical/decode.c+++ b/src/backend/replication/logical/decode.c@@ -547,23 +547,19 @@ 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- * 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;-- SnapBuildProcessChange(builder, xid, buf->origptr);- ReorderBufferXidSetCatalogChanges(ctx->reorder, xid, buf->origptr);
break;
case XLOG_HEAP_CONFIRM:
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.cindex 5979030..091b061 100644--- a/src/backend/utils/cache/catcache.c+++ b/src/backend/utils/cache/catcache.c@@ -2131,7 +2131,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;
@@ -2172,7 +2173,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)
{
@@ -2181,7 +2182,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);
}
}
}
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.cindex e281a45..c793be5 100644--- a/src/backend/utils/cache/inval.c+++ b/src/backend/utils/cache/inval.c@@ -99,6 +99,10 @@
* 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
@@ -1011,6 +1054,45 @@ AtEOXact_Inval(bool isCommit)
}
/*
+ * 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 */+}++/*
* AtEOSubXact_Inval
* Process queued-up invalidation messages at end of subtransaction.
*
@@ -1032,9 +1114,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 +1161,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 +1209,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 +1217,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 +1255,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 +1265,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 +1343,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);
}
/*
@@ -1276,14 +1399,13 @@ CacheInvalidateCatalog(Oid catalogId)
{
Oid databaseId;
- PrepareInvalidationState();-
if (IsSharedRelation(catalogId))
databaseId = InvalidOid;
else
databaseId = MyDatabaseId;
- RegisterCatalogInvalidation(databaseId, catalogId);+ RegisterCatalogInvalidation(PrepareInvalidationState(),+ databaseId, catalogId);
}
/*
@@ -1301,15 +1423,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 +1443,8 @@ CacheInvalidateRelcache(Relation relation)
void
CacheInvalidateRelcacheAll(void)
{
- PrepareInvalidationState();-- RegisterRelcacheInvalidation(InvalidOid, InvalidOid);+ RegisterRelcacheInvalidation(PrepareInvalidationState(),+ InvalidOid, InvalidOid);
}
/*
@@ -1338,14 +1458,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 +1478,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 +1666,7 @@ LogLogicalInvalidations()
if (transInvalInfo == NULL)
return;
- ProcessInvalidationMessagesMulti(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessInvalidationMessagesMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
MakeSharedInvalidMessagesArray);
Assert(!(numSharedInvalidMessagesArray > 0 &&
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.cindex 7d573b6..ce5d2e8 100644--- a/src/backend/utils/cache/syscache.c+++ b/src/backend/utils/cache/syscache.c@@ -1260,8 +1260,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".
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.hindex ddc2762..c4fd4d1 100644--- a/src/include/utils/catcache.h+++ b/src/include/utils/catcache.h@@ -223,7 +223,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);
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.hindex 877e66c..c89ba72 100644--- a/src/include/utils/inval.h+++ b/src/include/utils/inval.h@@ -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);
diff --git a/src/test/isolation/expected/inplace-inval.out b/src/test/isolation/expected/inplace-inval.outindex e68eca5..c35895a 100644--- a/src/test/isolation/expected/inplace-inval.out+++ b/src/test/isolation/expected/inplace-inval.out@@ -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;
diff --git a/src/test/isolation/specs/inplace-inval.spec b/src/test/isolation/specs/inplace-inval.specindex 96954fd..b99112d 100644--- a/src/test/isolation/specs/inplace-inval.spec+++ b/src/test/isolation/specs/inplace-inval.spec@@ -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
[text/plain] inplace160-inval-durability-inplace-v6_16.patch (35.2K, ../20241024025357.a8.nmisch@google.com/7-inplace160-inval-durability-inplace-v6_16.patch)
download | inline diff:
commit 89c3673 (HEAD, zzy_test-commit-REL_16_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Tue Oct 22 14:42:05 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 17:46:53 2024 -0700
For inplace update, send nontransactional invalidations.
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
Conflicts:
src/backend/commands/event_trigger.c
src/backend/utils/cache/inval.c
---
src/backend/access/heap/heapam.c | 43 +++-
src/backend/access/transam/xact.c | 26 ++-
src/backend/catalog/index.c | 11 +-
src/backend/replication/logical/decode.c | 26 +--
src/backend/utils/cache/catcache.c | 7 +-
src/backend/utils/cache/inval.c | 304 ++++++++++++++++++--------
src/backend/utils/cache/syscache.c | 3 +-
src/include/utils/catcache.h | 3 +-
src/include/utils/inval.h | 6 +
src/test/isolation/expected/inplace-inval.out | 10 +-
src/test/isolation/specs/inplace-inval.spec | 12 +-
src/tools/pgindent/typedefs.list | 1 +
12 files changed, 320 insertions(+), 132 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.cindex 75e8216..fd17d1c 100644--- a/src/backend/access/heap/heapam.c+++ b/src/backend/access/heap/heapam.c@@ -6192,6 +6192,24 @@ 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);++ /*+ * 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();
@@ -6235,17 +6253,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);
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.cindex 4a2ea4a..91dbfcc 100644--- a/src/backend/access/transam/xact.c+++ b/src/backend/access/transam/xact.c@@ -1337,14 +1337,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)
{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.cindex ec56bb6..b0ffe94 100644--- a/src/backend/catalog/index.c+++ b/src/backend/catalog/index.c@@ -2915,12 +2915,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);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.cindex d91055a..70af02c 100644--- a/src/backend/replication/logical/decode.c+++ b/src/backend/replication/logical/decode.c@@ -511,23 +511,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:
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.cindex 000e81a..9f249fd 100644--- a/src/backend/utils/cache/catcache.c+++ b/src/backend/utils/cache/catcache.c@@ -2219,7 +2219,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;
@@ -2260,7 +2261,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)
{
@@ -2269,7 +2270,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);
}
}
}
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.cindex 0008826..7ad69ee7 100644--- a/src/backend/utils/cache/inval.c+++ b/src/backend/utils/cache/inval.c@@ -94,6 +94,10 @@
* 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.
@@ -131,13 +135,15 @@
/*
* 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
@@ -148,7 +154,9 @@
* 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
@@ -201,7 +209,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
@@ -217,26 +225,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;
@@ -544,9 +562,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);
}
@@ -556,10 +577,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);
}
/*
@@ -568,10 +588,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
@@ -588,7 +607,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;
}
/*
@@ -598,10 +617,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);
}
/*
@@ -791,14 +809,18 @@ AcceptInvalidationMessages(void)
* 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,
@@ -821,7 +843,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");
/*
@@ -830,8 +852,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
@@ -847,6 +869,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;
}
/*
@@ -904,7 +961,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
@@ -915,7 +972,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
* not new ones.
*/
nummsgs = NumMessagesInGroup(&transInvalInfo->PriorCmdInvalidMsgs) +
- NumMessagesInGroup(&transInvalInfo->CurrentCmdInvalidMsgs);+ NumMessagesInGroup(&transInvalInfo->ii.CurrentCmdInvalidMsgs);
*msgs = msgarray = (SharedInvalidationMessage *)
MemoryContextAlloc(CurTransactionContext,
@@ -928,7 +985,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
msgs,
n * sizeof(SharedInvalidationMessage)),
nmsgs += n));
- ProcessMessageSubGroupMulti(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessMessageSubGroupMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
CatCacheMsgs,
(memcpy(msgarray + nmsgs,
msgs,
@@ -940,7 +997,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
msgs,
n * sizeof(SharedInvalidationMessage)),
nmsgs += n));
- ProcessMessageSubGroupMulti(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessMessageSubGroupMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
RelCacheMsgs,
(memcpy(msgarray + nmsgs,
msgs,
@@ -1027,7 +1084,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;
@@ -1041,16 +1100,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
@@ -1064,6 +1123,44 @@ AtEOXact_Inval(bool isCommit)
}
/*
+ * 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.
*
@@ -1085,9 +1182,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;
@@ -1128,12 +1236,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;
@@ -1180,7 +1288,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 */
@@ -1188,26 +1296,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;
@@ -1231,11 +1334,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
@@ -1244,11 +1344,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
@@ -1321,7 +1422,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);
}
/*
@@ -1340,14 +1478,13 @@ CacheInvalidateCatalog(Oid catalogId)
{
Oid databaseId;
- PrepareInvalidationState();-
if (IsSharedRelation(catalogId))
databaseId = InvalidOid;
else
databaseId = MyDatabaseId;
- RegisterCatalogInvalidation(databaseId, catalogId);+ RegisterCatalogInvalidation(PrepareInvalidationState(),+ databaseId, catalogId);
}
/*
@@ -1365,15 +1502,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);
}
/*
@@ -1386,9 +1522,8 @@ CacheInvalidateRelcache(Relation relation)
void
CacheInvalidateRelcacheAll(void)
{
- PrepareInvalidationState();-- RegisterRelcacheInvalidation(InvalidOid, InvalidOid);+ RegisterRelcacheInvalidation(PrepareInvalidationState(),+ InvalidOid, InvalidOid);
}
/*
@@ -1402,14 +1537,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);
}
/*
@@ -1423,8 +1557,6 @@ CacheInvalidateRelcacheByRelid(Oid relid)
{
HeapTuple tup;
- PrepareInvalidationState();-
tup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tup))
elog(ERROR, "cache lookup failed for relation %u", relid);
@@ -1614,7 +1746,7 @@ LogLogicalInvalidations(void)
if (transInvalInfo == NULL)
return;
- group = &transInvalInfo->CurrentCmdInvalidMsgs;+ group = &transInvalInfo->ii.CurrentCmdInvalidMsgs;
nmsgs = NumMessagesInGroup(group);
if (nmsgs > 0)
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.cindex 697b17b..b0cc6a0 100644--- a/src/backend/utils/cache/syscache.c+++ b/src/backend/utils/cache/syscache.c@@ -951,8 +951,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".
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.hindex a32d722..9204663 100644--- a/src/include/utils/catcache.h+++ b/src/include/utils/catcache.h@@ -228,7 +228,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);
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.hindex 14b4eac..e807779 100644--- a/src/include/utils/inval.h+++ b/src/include/utils/inval.h@@ -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);
diff --git a/src/test/isolation/expected/inplace-inval.out b/src/test/isolation/expected/inplace-inval.outindex e68eca5..c35895a 100644--- a/src/test/isolation/expected/inplace-inval.out+++ b/src/test/isolation/expected/inplace-inval.out@@ -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;
diff --git a/src/test/isolation/specs/inplace-inval.spec b/src/test/isolation/specs/inplace-inval.specindex 96954fd..b99112d 100644--- a/src/test/isolation/specs/inplace-inval.spec+++ b/src/test/isolation/specs/inplace-inval.spec@@ -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
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.listindex 4357043..fcaa2e6 100644--- a/src/tools/pgindent/typedefs.list+++ b/src/tools/pgindent/typedefs.list@@ -1205,6 +1205,7 @@ InternalGrant
Interval
IntoClause
InvalMessageArray
+InvalidationInfo
InvalidationMsgsGroup
IpcMemoryId
IpcMemoryKey
[text/plain] inplace160-inval-durability-inplace-v6_17.patch (35.9K, ../20241024025357.a8.nmisch@google.com/8-inplace160-inval-durability-inplace-v6_17.patch)
download | inline diff:
commit f52d713 (HEAD, zzy_test-commit-REL_17_STABLE)
Author: Noah Misch <noah@leadboat.com>
AuthorDate: Tue Oct 22 12:59:28 2024 -0700
Commit: Noah Misch <noah@leadboat.com>
CommitDate: Wed Oct 23 17:46:13 2024 -0700
For inplace update, send nontransactional invalidations.
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.
Core code no longer needs XLOG_INVALIDATIONS, but this leaves removing
it for future work. Extensions could be relying on that mechanism, so
that removal would not be back-patch material. Back-patch to v12 (all
supported versions). The back branch versions don't change WAL, because
those branches just added end-of-recovery SIResetAll().
Reviewed by Nitin Motiani and (in earlier versions) Andres Freund.
Discussion: https://postgr.es/m/20240523000548.58.nmisch@google.com
Conflicts:
src/backend/access/heap/heapam_xlog.c
src/include/access/xlog_internal.h
---
src/backend/access/heap/heapam.c | 43 +++-
src/backend/access/transam/xact.c | 26 ++-
src/backend/catalog/index.c | 11 +-
src/backend/commands/event_trigger.c | 5 -
src/backend/replication/logical/decode.c | 26 +--
src/backend/utils/cache/catcache.c | 7 +-
src/backend/utils/cache/inval.c | 304 ++++++++++++++++++--------
src/backend/utils/cache/syscache.c | 3 +-
src/include/utils/catcache.h | 3 +-
src/include/utils/inval.h | 6 +
src/test/isolation/expected/inplace-inval.out | 10 +-
src/test/isolation/specs/inplace-inval.spec | 12 +-
src/tools/pgindent/typedefs.list | 1 +
13 files changed, 320 insertions(+), 137 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.cindex cb65faa..517b374 100644--- a/src/backend/access/heap/heapam.c+++ b/src/backend/access/heap/heapam.c@@ -6352,6 +6352,24 @@ 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);++ /*+ * 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();
@@ -6395,17 +6413,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);
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.cindex 4cecf63..053a200 100644--- a/src/backend/access/transam/xact.c+++ b/src/backend/access/transam/xact.c@@ -1358,14 +1358,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)
{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.cindex 3eaf1f2..2156e21 100644--- a/src/backend/catalog/index.c+++ b/src/backend/catalog/index.c@@ -2889,12 +2889,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);
}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.cindex 05a6de6..a586d24 100644--- a/src/backend/commands/event_trigger.c+++ b/src/backend/commands/event_trigger.c@@ -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);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.cindex 8ec5adf..03f1617 100644--- a/src/backend/replication/logical/decode.c+++ b/src/backend/replication/logical/decode.c@@ -508,23 +508,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:
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.cindex 111d8a2..ea8ca0e 100644--- a/src/backend/utils/cache/catcache.c+++ b/src/backend/utils/cache/catcache.c@@ -2288,7 +2288,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;
@@ -2329,7 +2330,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)
{
@@ -2338,7 +2339,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);
}
}
}
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.cindex 603aa41..93bfa1e 100644--- a/src/backend/utils/cache/inval.c+++ b/src/backend/utils/cache/inval.c@@ -94,6 +94,10 @@
* 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.
@@ -130,13 +134,15 @@
/*
* 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
@@ -147,7 +153,9 @@
* 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,7 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
msgs,
n * sizeof(SharedInvalidationMessage)),
nmsgs += n));
- ProcessMessageSubGroupMulti(&transInvalInfo->CurrentCmdInvalidMsgs,+ ProcessMessageSubGroupMulti(&transInvalInfo->ii.CurrentCmdInvalidMsgs,
RelCacheMsgs,
(memcpy(msgarray + nmsgs,
msgs,
@@ -1024,7 +1081,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 +1097,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
@@ -1061,6 +1120,44 @@ AtEOXact_Inval(bool isCommit)
}
/*
+ * 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 +1179,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;
@@ -1125,12 +1233,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 +1285,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 +1293,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 +1331,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 +1341,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 +1419,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 +1475,13 @@ CacheInvalidateCatalog(Oid catalogId)
{
Oid databaseId;
- PrepareInvalidationState();-
if (IsSharedRelation(catalogId))
databaseId = InvalidOid;
else
databaseId = MyDatabaseId;
- RegisterCatalogInvalidation(databaseId, catalogId);+ RegisterCatalogInvalidation(PrepareInvalidationState(),+ databaseId, catalogId);
}
/*
@@ -1362,15 +1499,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 +1519,8 @@ CacheInvalidateRelcache(Relation relation)
void
CacheInvalidateRelcacheAll(void)
{
- PrepareInvalidationState();-- RegisterRelcacheInvalidation(InvalidOid, InvalidOid);+ RegisterRelcacheInvalidation(PrepareInvalidationState(),+ InvalidOid, InvalidOid);
}
/*
@@ -1399,14 +1534,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 +1554,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 +1743,7 @@ LogLogicalInvalidations(void)
if (transInvalInfo == NULL)
return;
- group = &transInvalInfo->CurrentCmdInvalidMsgs;+ group = &transInvalInfo->ii.CurrentCmdInvalidMsgs;
nmsgs = NumMessagesInGroup(group);
if (nmsgs > 0)
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.cindex 50c9440..f41b1c2 100644--- a/src/backend/utils/cache/syscache.c+++ b/src/backend/utils/cache/syscache.c@@ -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".
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.hindex 3fb9647..8f04bb8 100644--- a/src/include/utils/catcache.h+++ b/src/include/utils/catcache.h@@ -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 */
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.hindex 24695fa..3390e7a 100644--- a/src/include/utils/inval.h+++ b/src/include/utils/inval.h@@ -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);
diff --git a/src/test/isolation/expected/inplace-inval.out b/src/test/isolation/expected/inplace-inval.outindex e68eca5..c35895a 100644--- a/src/test/isolation/expected/inplace-inval.out+++ b/src/test/isolation/expected/inplace-inval.out@@ -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;
diff --git a/src/test/isolation/specs/inplace-inval.spec b/src/test/isolation/specs/inplace-inval.specindex 96954fd..b99112d 100644--- a/src/test/isolation/specs/inplace-inval.spec+++ b/src/test/isolation/specs/inplace-inval.spec@@ -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
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.listindex 5628b3f..3b57e78 100644--- a/src/tools/pgindent/typedefs.list+++ b/src/tools/pgindent/typedefs.list@@ -1253,6 +1253,7 @@ Interval
IntervalAggState
IntoClause
InvalMessageArray
+InvalidationInfo
InvalidationMsgsGroup
IpcMemoryId
IpcMemoryKey
[text/plain] inplace160-inval-durability-inplace-v6.patch (41.4K, ../20241024025357.a8.nmisch@google.com/9-inplace160-inval-durability-inplace-v6.patch)
download | inline diff:
Author: Noah Misch <noah@leadboat.com>
Commit: Noah Misch <noah@leadboat.com>
For inplace update, send nontransactional invalidations.
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
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.cindex da5e656..7c82a95 100644--- a/src/backend/access/heap/heapam.c+++ b/src/backend/access/heap/heapam.c@@ -6327,6 +6327,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;
@@ -6334,6 +6337,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();
@@ -6363,9 +6389,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);
@@ -6377,17 +6410,28 @@ heap_inplace_update_and_unlock(Relation relation,
PageSetLSN(BufferGetPage(buffer), recptr);
}
- END_CRIT_SECTION();-- heap_inplace_unlock(relation, oldtup, buffer);+ LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
/*
- * 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.+ * Send invalidations to shared queue. SearchSysCacheLocked1() assumes we+ * do this before UnlockTuple().
*
- * XXX ROLLBACK discards the invalidation. See test inplace-inval.spec.+ * 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);++ AcceptInvalidationMessages(); /* local processing of just-sent inval */++ /*+ * 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);
diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.cindex 6dae723..c5208f3 100644--- a/src/backend/access/heap/heapam_xlog.c+++ b/src/backend/access/heap/heapam_xlog.c@@ -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
diff --git a/src/backend/access/rmgrdesc/heapdesc.c b/src/backend/access/rmgrdesc/heapdesc.cindex 5f5673e..f31cc3a 100644--- a/src/backend/access/rmgrdesc/heapdesc.c+++ b/src/backend/access/rmgrdesc/heapdesc.c@@ -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);
}
}
diff --git a/src/backend/access/rmgrdesc/standbydesc.c b/src/backend/access/rmgrdesc/standbydesc.cindex 25f870b..32e509a 100644--- a/src/backend/access/rmgrdesc/standbydesc.c+++ b/src/backend/access/rmgrdesc/standbydesc.c@@ -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,
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.cindex 87700c7..20b7307 100644--- a/src/backend/access/transam/xact.c+++ b/src/backend/access/transam/xact.c@@ -1368,14 +1368,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)
{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.cindex 12822d0..9162b9f 100644--- a/src/backend/catalog/index.c+++ b/src/backend/catalog/index.c@@ -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);
}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.cindex 05a6de6..a586d24 100644--- a/src/backend/commands/event_trigger.c+++ b/src/backend/commands/event_trigger.c@@ -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);
}
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.cindex d687cee..e73576a 100644--- a/src/backend/replication/logical/decode.c+++ b/src/backend/replication/logical/decode.c@@ -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:
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.cindex 10276aa..ee303dc 100644--- a/src/backend/utils/cache/catcache.c+++ b/src/backend/utils/cache/catcache.c@@ -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);
}
}
}
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.cindex 603aa41..986850c 100644--- a/src/backend/utils/cache/inval.c+++ b/src/backend/utils/cache/inval.c@@ -94,6 +94,10 @@
* 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.
@@ -130,13 +134,15 @@
/*
* 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
@@ -147,7 +153,9 @@
* 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;
- /* Events emitted by previous commands of this (sub)transaction */- InvalidationMsgsGroup PriorCmdInvalidMsgs;-
/* 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;++ /* 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
@@ -1061,6 +1164,44 @@ AtEOXact_Inval(bool isCommit)
}
/*
+ * 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;- /* 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;
@@ -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)
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.cindex 50c9440..f41b1c2 100644--- a/src/backend/utils/cache/syscache.c+++ b/src/backend/utils/cache/syscache.c@@ -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".
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.hindex 42736f3..4591e9a 100644--- a/src/include/access/heapam_xlog.h+++ b/src/include/access/heapam_xlog.h@@ -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
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.hindex 5ef244b..d9cf51a 100644--- a/src/include/access/xlog_internal.h+++ b/src/include/access/xlog_internal.h@@ -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
{
diff --git a/src/include/storage/sinval.h b/src/include/storage/sinval.hindex 8f5744b..c812237 100644--- a/src/include/storage/sinval.h+++ b/src/include/storage/sinval.h@@ -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);
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.hindex 3fb9647..8f04bb8 100644--- a/src/include/utils/catcache.h+++ b/src/include/utils/catcache.h@@ -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 */
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.hindex 24695fa..3390e7a 100644--- a/src/include/utils/inval.h+++ b/src/include/utils/inval.h@@ -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);
diff --git a/src/test/isolation/expected/inplace-inval.out b/src/test/isolation/expected/inplace-inval.outindex e68eca5..c35895a 100644--- a/src/test/isolation/expected/inplace-inval.out+++ b/src/test/isolation/expected/inplace-inval.out@@ -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;
diff --git a/src/test/isolation/specs/inplace-inval.spec b/src/test/isolation/specs/inplace-inval.specindex 96954fd..b99112d 100644--- a/src/test/isolation/specs/inplace-inval.spec+++ b/src/test/isolation/specs/inplace-inval.spec@@ -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
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.listindex 54bf29b..1fb4801 100644--- a/src/tools/pgindent/typedefs.list+++ b/src/tools/pgindent/typedefs.list@@ -1258,6 +1258,7 @@ Interval
IntervalAggState
IntoClause
InvalMessageArray
+InvalidationInfo
InvalidationMsgsGroup
IpcMemoryId
IpcMemoryKey
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-hackers@postgresql.org
Cc: noah@leadboat.com, nitinmotiani@google.com, andres@anarazel.de
Subject: Re: Inval reliability, especially for inplace updates
In-Reply-To: <20241024025357.a8.nmisch@google.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox