From 0d4dbffb8900a5ac065cc3bd60d62578c5881c00 Mon Sep 17 00:00:00 2001 From: Vaijayanti Bharadwaj Date: Wed, 2 Sep 2026 20:09:38 +0530 Subject: [PATCH] Get correct lastCommitBeforeSnapshot when importing an SSI snapshot GetSerializableTransactionSnapshotInt assigns sxact->SeqNo.lastCommitBeforeSnapshot = PredXact->LastSxactCommitSeqNo; A serializable transaction can have lastCommitBeforeSnapshot between the exporter's value and PredXact->LastSxactCommitSeqNo. Using the exporter's lastCommitBeforeSnapshot for a read-write transaction is safer. For read-only transactions all comparisons check: X >= roXact->SeqNo.lastCommitBeforeSnapshot for aborting or releasing some resource. so a higher value will only result in false positives or resources released later, which is not incorrect. --- src/backend/storage/lmgr/predicate.c | 79 ++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/backend/storage/lmgr/predicate.c b/src/backend/storage/lmgr/predicate.c index 0ae85b7d5b4..bebbae26a92 100644 --- a/src/backend/storage/lmgr/predicate.c +++ b/src/backend/storage/lmgr/predicate.c @@ -464,6 +464,8 @@ static uint32 predicatelock_hash(const void *key, Size keysize); static void SummarizeOldestCommittedSxact(void); static Snapshot GetSafeSnapshot(Snapshot origSnapshot); +static SerCommitSeqNo GetSourceLastCommitBeforeSnapshot(const SERIALIZABLEXACT *mysxact, + const VirtualTransactionId *sourcevxid); static Snapshot GetSerializableTransactionSnapshotInt(Snapshot snapshot, VirtualTransactionId *sourcevxid, int sourcepid); @@ -1680,6 +1682,60 @@ SetSerializableTransactionSnapshot(Snapshot snapshot, sourcepid); } +/* + * Find the commit sequence number horizon of the transaction we are importing + * a snapshot from. + * + * Since the source transaction exported the very snapshot we are adopting, + * its lastCommitBeforeSnapshot is by definition the correct horizon for that + * snapshot, and using it makes the two transactions agree. A read-write + * serializable transaction can only import a snapshot from a serializable + * transaction which is not read-only (ImportSnapshot() rejects the other + * combinations) and which is still running (ProcArrayInstallImportedXmin() + * checks that), and such a transaction always has a SERIALIZABLEXACT of its + * own -- only read-only transactions ever opt out of predicate locking -- so + * the search below should always succeed. Should it somehow not, return 0, + * which makes PredicateLockIsForOverlappingTransaction() treat every + * summarized lock as overlapping ours. + * + * There is no index from a VirtualTransactionId to a SERIALIZABLEXACT, but + * activeList is short and importing a snapshot is rare, so just scan it. + * + * Caller must hold SerializableXactHashLock. mysxact is the caller's own + * entry, which is already on activeList but not yet initialized. + */ +static SerCommitSeqNo +GetSourceLastCommitBeforeSnapshot(const SERIALIZABLEXACT *mysxact, + const VirtualTransactionId *sourcevxid) +{ + dlist_iter iter; + + Assert(LWLockHeldByMeInMode(SerializableXactHashLock, LW_EXCLUSIVE)); + + dlist_foreach(iter, &PredXact->activeList) + { + const SERIALIZABLEXACT *othersxact = + dlist_container(SERIALIZABLEXACT, xactLink, iter.cur); + + /* + * Skip the dummy transaction used for summarized locks, our own + * uninitialized entry, and any transaction which has finished: in the + * last case the SeqNo union no longer holds a + * lastCommitBeforeSnapshot. + */ + if (othersxact == PredXact->OldCommittedSxact + || othersxact == mysxact + || SxactIsCommitted(othersxact) + || SxactIsRolledBack(othersxact)) + continue; + + if (VirtualTransactionIdEquals(othersxact->vxid, *sourcevxid)) + return othersxact->SeqNo.lastCommitBeforeSnapshot; + } + + return 0; +} + /* * Guts of GetSerializableTransactionSnapshot * @@ -1780,6 +1836,29 @@ GetSerializableTransactionSnapshotInt(Snapshot snapshot, /* Initialize the structure. */ sxact->vxid = vxid; sxact->SeqNo.lastCommitBeforeSnapshot = PredXact->LastSxactCommitSeqNo; + + /* + * If we are importing a snapshot, the value just recorded is wrong: the + * snapshot was taken by the source transaction at some earlier point, so + * LastSxactCommitSeqNo has since moved past the commits which the + * snapshot does not include. Anything which committed in between would + * then be wrongly judged to precede our snapshot by + * PredicateLockIsForOverlappingTransaction(), and we would miss a + * rw-conflict in from a summarized reader. Record the horizon of the + * transaction which exported the snapshot instead. + * + * Only do this for read-write transactions. A read-only transaction + * never writes, so it never reaches + * PredicateLockIsForOverlappingTransaction(); every other consumer of + * lastCommitBeforeSnapshot is one of the read-only optimizations, and + * those err on the safe side when the value is too large, not when it is + * too small. Leaving read-only transactions with the existing value + * therefore keeps this change confined to the test above. + */ + if (sourcevxid && !XactReadOnly) + sxact->SeqNo.lastCommitBeforeSnapshot = + GetSourceLastCommitBeforeSnapshot(sxact, sourcevxid); + sxact->prepareSeqNo = InvalidSerCommitSeqNo; sxact->commitSeqNo = InvalidSerCommitSeqNo; dlist_init(&(sxact->outConflicts)); -- 2.55.0