pg.ddx.io pgsql-hackers@postgresql.org mailing list archive
help / color / mirror / Atom feedFix bug in multixact Oldest*MXactId initialization and access
31+ messages / 5 participants
[nested] [flat]
Commits from this thread:
2026-03-04 fe08113aef fa3b328e6d 969576dab4 Add test for row-locking and multixids with prepared transactions
2026-03-04 19615a44b3 201436c19f 80d1d4ee06 65f5c451a3 a6b11ac4c4 ffe53037df Skip prepared_xacts test if max_prepared_transactions < 2
2026-03-02 ccae90abdb 0a50ef0943 dcd9c06a42 Fix OldestMemberMXactId and OldestVisibleMXactId array usage
* Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-24 17:35 Yura Sokolov <y.sokolov@postgrespro.ru>
0 siblings, 2 replies; 31+ messages in thread
From: Yura Sokolov @ 2026-02-24 17:35 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; +Cc: Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>
Good day.
multixact.c has bug in initialization and access of OldestMemberMXactId
(and partially OldestVisibleMXactId).
Size of this arrays is defined as:
#define MaxOldestSlot (MaxBackends + max_prepared_xacts)
assuming there are only backends and prepared transactions could hold
multixacts.
This assumption is correct. And in fact there were no bug when this formula
were introduced in 2009y [1], since these arrays were indexed but synthetic
dummy `dummyBackendId` field of `GlobalTransactionData` struct.
But in 2024y [2] field `dummyBackendId` were removed and pgprocno were used
instead.
But proc structs reserved for two phase commit are placed after auxiliary
procs, therefore writes to OldestMemberMXactId[dummy] starts to overwrites
slots of OldestVisibleMXactId.
Then PostgreSQL 18 increased NUM_AUXILIARY_PROCS due to reserve of workers
for AIO. And it is possible to make such test postgresql.conf with so
extremely low MaxBackend so writes to OldestMemberMXactId[dummy] overwrites
first entry of BufferDescriptors, which are allocated next in shared memory.
Patch in attach replaces direct accesses to this arrays with inline
functions which include asserts. And changes calculation of MaxOldestSlot
to include NUM_AUXILIARY_PROCS.
Certainly, it is not clearest patch possible:
- may be you will decide to not introduce inline functions,
- or will introduce separate inline function for each array,
- or will fix slot calculation to remove aux procs from account,
- or will revert deletion of dummyBackendId.
[1]
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=cd87b6f8a5084c070c3e56b07794be8fe...
[2]
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=ab355e3a88de745607f6dd4c21f0119b5...
--
regards
Yura Sokolov aka funny-falcon
Attachments:
v00-0001-Fix-multixacts-OldestMemberMXactId-and-OldestVis.patch (text/x-patch, 7.2K) download
From f518e1454d5ee09eee1a84ab43bec1150361e76b Mon Sep 17 00:00:00 2001
From: Yura Sokolov <y.sokolov@postgrespro.ru>
Date: Tue, 24 Feb 2026 20:29:01 +0300
Subject: [PATCH v00] Fix multixacts OldestMemberMXactId and
OldestVisibleMXactId initialization.
Due to [1], OldestMemberMXactId is no longer accessed by synthetic
dummyBackendId, but rather with pgprocno. Procs for prepared xacts are
placed after auxiliary procs, therefore calculation for MaxOldestSlot
became invalid.
Fix MaxOldestSlot to include NUM_AUXILIARY_PROCS and replace direct access
to these arrays with inline functions.
[1] ab355e3a88de745 "Redefine backend ID to be an index into the proc array"
---
src/backend/access/transam/multixact.c | 58 ++++++++++++++++++--------
1 file changed, 41 insertions(+), 17 deletions(-)
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 90ec87d9dd6..15dd928eb95 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -210,7 +210,7 @@ typedef struct MultiXactStateData
/*
* Size of OldestMemberMXactId and OldestVisibleMXactId arrays.
*/
-#define MaxOldestSlot (MaxBackends + max_prepared_xacts)
+#define MaxOldestSlot (MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts)
/* Pointers to the state data in shared memory */
static MultiXactStateData *MultiXactState;
@@ -285,6 +285,9 @@ static void WriteMTruncateXlogRec(Oid oldestMultiDB,
MultiXactId endTruncOff,
MultiXactOffset endTruncMemb);
+static inline MultiXactId getOldest(MultiXactId *oldest, ProcNumber procno);
+static inline bool validOldest(MultiXactId *oldest, ProcNumber procno);
+static inline void setOldest(MultiXactId *oldest, ProcNumber procno, MultiXactId mxact);
/*
* MultiXactIdCreate
@@ -308,7 +311,7 @@ MultiXactIdCreate(TransactionId xid1, MultiXactStatus status1,
Assert(!TransactionIdEquals(xid1, xid2) || (status1 != status2));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(validOldest(OldestMemberMXactId, MyProcNumber));
/*
* Note: unlike MultiXactIdExpand, we don't bother to check that both XIDs
@@ -362,7 +365,7 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status)
Assert(TransactionIdIsValid(xid));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(validOldest(OldestMemberMXactId, MyProcNumber));
debug_elog5(DEBUG2, "Expand: received multi %u, xid %u status %s",
multi, xid, mxstatus_to_string(status));
@@ -536,7 +539,7 @@ MultiXactIdIsRunning(MultiXactId multi, bool isLockOnly)
void
MultiXactIdSetOldestMember(void)
{
- if (!MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]))
+ if (!validOldest(OldestMemberMXactId, MyProcNumber))
{
MultiXactId nextMXact;
@@ -558,7 +561,7 @@ MultiXactIdSetOldestMember(void)
nextMXact = MultiXactState->nextMXact;
- OldestMemberMXactId[MyProcNumber] = nextMXact;
+ setOldest(OldestMemberMXactId, MyProcNumber, nextMXact);
LWLockRelease(MultiXactGenLock);
@@ -586,7 +589,7 @@ MultiXactIdSetOldestMember(void)
static void
MultiXactIdSetOldestVisible(void)
{
- if (!MultiXactIdIsValid(OldestVisibleMXactId[MyProcNumber]))
+ if (!validOldest(OldestVisibleMXactId, MyProcNumber))
{
MultiXactId oldestMXact;
int i;
@@ -603,7 +606,7 @@ MultiXactIdSetOldestVisible(void)
oldestMXact = thisoldest;
}
- OldestVisibleMXactId[MyProcNumber] = oldestMXact;
+ setOldest(OldestVisibleMXactId, MyProcNumber, oldestMXact);
LWLockRelease(MultiXactGenLock);
@@ -1152,7 +1155,7 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members,
* multi. It cannot possibly still be running.
*/
if (isLockOnly &&
- MultiXactIdPrecedes(multi, OldestVisibleMXactId[MyProcNumber]))
+ MultiXactIdPrecedes(multi, getOldest(OldestVisibleMXactId, MyProcNumber)))
{
debug_elog2(DEBUG2, "GetMembers: a locker-only multi is too old");
*members = NULL;
@@ -1574,8 +1577,8 @@ AtEOXact_MultiXact(void)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ setOldest(OldestMemberMXactId, MyProcNumber, InvalidMultiXactId);
+ setOldest(OldestVisibleMXactId, MyProcNumber, InvalidMultiXactId);
/*
* Discard the local MultiXactId cache. Since MXactContext was created as
@@ -1595,7 +1598,7 @@ AtEOXact_MultiXact(void)
void
AtPrepare_MultiXact(void)
{
- MultiXactId myOldestMember = OldestMemberMXactId[MyProcNumber];
+ MultiXactId myOldestMember = getOldest(OldestMemberMXactId, MyProcNumber);
if (MultiXactIdIsValid(myOldestMember))
RegisterTwoPhaseRecord(TWOPHASE_RM_MULTIXACT_ID, 0,
@@ -1615,7 +1618,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* Transfer our OldestMemberMXactId value to the slot reserved for the
* prepared transaction.
*/
- myOldestMember = OldestMemberMXactId[MyProcNumber];
+ myOldestMember = getOldest(OldestMemberMXactId, MyProcNumber);
if (MultiXactIdIsValid(myOldestMember))
{
ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(fxid, false);
@@ -1628,8 +1631,8 @@ PostPrepare_MultiXact(FullTransactionId fxid)
*/
LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
- OldestMemberMXactId[dummyProcNumber] = myOldestMember;
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
+ setOldest(OldestMemberMXactId, dummyProcNumber, myOldestMember);
+ setOldest(OldestMemberMXactId, MyProcNumber, InvalidMultiXactId);
LWLockRelease(MultiXactGenLock);
}
@@ -1642,7 +1645,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ setOldest(OldestVisibleMXactId, MyProcNumber, InvalidMultiXactId);
/*
* Discard the local MultiXactId cache like in AtEOXact_MultiXact.
@@ -1669,7 +1672,7 @@ multixact_twophase_recover(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
oldestMember = *((MultiXactId *) recdata);
- OldestMemberMXactId[dummyProcNumber] = oldestMember;
+ setOldest(OldestMemberMXactId, dummyProcNumber, oldestMember);
}
/*
@@ -1684,7 +1687,7 @@ multixact_twophase_postcommit(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
- OldestMemberMXactId[dummyProcNumber] = InvalidMultiXactId;
+ setOldest(OldestMemberMXactId, dummyProcNumber, InvalidMultiXactId);
}
/*
@@ -2922,3 +2925,24 @@ multixactmemberssyncfiletag(const FileTag *ftag, char *path)
{
return SlruSyncFileTag(MultiXactMemberCtl, ftag, path);
}
+
+static inline MultiXactId
+getOldest(MultiXactId *oldest, ProcNumber procno)
+{
+ Assert(procno < MaxOldestSlot);
+ return oldest[procno];
+}
+
+static inline bool
+validOldest(MultiXactId *oldest, ProcNumber procno)
+{
+ Assert(procno < MaxOldestSlot);
+ return MultiXactIdIsValid(oldest[procno]);
+}
+
+static inline void
+setOldest(MultiXactId *oldest, ProcNumber procno, MultiXactId mxact)
+{
+ Assert(procno < MaxOldestSlot);
+ oldest[procno] = mxact;
+}
--
2.51.0
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-25 01:02 Sami Imseih <samimseih@gmail.com>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
1 sibling, 0 replies; 31+ messages in thread
From: Sami Imseih @ 2026-02-25 01:02 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>
Hi,
> But proc structs reserved for two phase commit are placed after auxiliary
> procs, therefore writes to OldestMemberMXactId[dummy] starts to overwrites
> slots of OldestVisibleMXactId.
>
> Then PostgreSQL 18 increased NUM_AUXILIARY_PROCS due to reserve of workers
> for AIO. And it is possible to make such test postgresql.conf with so
> extremely low MaxBackend so writes to OldestMemberMXactId[dummy] overwrites
> first entry of BufferDescriptors, which are allocated next in shared memory.
From what I can tell, PreparedXactProcs proc numbers start at
MaxBackends + NUM_AUXILIARY_PROCS, but MaxOldestSlot is calculated as
MaxBackends + max_prepared_xacts, missing the NUM_AUXILIARY_PROCS offset.
MyProcNumber (for regular backends) is always < MaxBackends, so it's safe.
But dummyProcNumber (for prepared transactions) starts at
MaxBackends + NUM_AUXILIARY_PROCS, which exceeds MaxOldestSlot.
Using this workload:
```
psql -c "DROP TABLE IF EXISTS test;"
psql -c "CREATE TABLE IF NOT EXISTS test (id int PRIMARY KEY, data text);"
psql -c "INSERT INTO test VALUES (1, 'test data');"
for i in {1..25}; do
psql -q <<EOF
BEGIN;
SELECT * FROM test WHERE id = 1 FOR SHARE;
PREPARE TRANSACTION 'prep_$i';
EOF
echo " Created prep_$i"
done
```
I can see the assert in your patch fail here:
```
@@ -1628,8 +1631,8 @@ PostPrepare_MultiXact(FullTransactionId fxid)
*/
LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
- OldestMemberMXactId[dummyProcNumber] = myOldestMember;
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
+ setOldest(OldestMemberMXactId, dummyProcNumber, myOldestMember);
```
> Patch in attach replaces direct accesses to this arrays with inline
> functions which include asserts. And changes calculation of MaxOldestSlot
> to include NUM_AUXILIARY_PROCS.
This is the important part of the patch to correctly size MaxOldestSlot
```
+#define MaxOldestSlot (MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts)
```
but, I am not sure if we need all the extra asserts, as this can only
be an issue for the
dummyProcNumber, right?
Maybe we can also define a new macro:
```
#define TOTAL_PROCS MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts
```
and use it in InitProcGlobal instead of TotalProcs, as well as MaxOldestSlot?
--
Sami Imseih
Amazon Web Services
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-25 01:44 Chao Li <li.evan.chao@gmail.com>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
1 sibling, 1 reply; 31+ messages in thread
From: Chao Li @ 2026-02-25 01:44 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>
> On Feb 25, 2026, at 01:35, Yura Sokolov <y.sokolov@postgrespro.ru> wrote:
>
> Good day.
>
> multixact.c has bug in initialization and access of OldestMemberMXactId
> (and partially OldestVisibleMXactId).
>
> Size of this arrays is defined as:
>
> #define MaxOldestSlot (MaxBackends + max_prepared_xacts)
>
> assuming there are only backends and prepared transactions could hold
> multixacts.
>
> This assumption is correct. And in fact there were no bug when this formula
> were introduced in 2009y [1], since these arrays were indexed but synthetic
> dummy `dummyBackendId` field of `GlobalTransactionData` struct.
>
> But in 2024y [2] field `dummyBackendId` were removed and pgprocno were used
> instead.
>
> But proc structs reserved for two phase commit are placed after auxiliary
> procs, therefore writes to OldestMemberMXactId[dummy] starts to overwrites
> slots of OldestVisibleMXactId.
>
> Then PostgreSQL 18 increased NUM_AUXILIARY_PROCS due to reserve of workers
> for AIO. And it is possible to make such test postgresql.conf with so
> extremely low MaxBackend so writes to OldestMemberMXactId[dummy] overwrites
> first entry of BufferDescriptors, which are allocated next in shared memory.
>
> Patch in attach replaces direct accesses to this arrays with inline
> functions which include asserts. And changes calculation of MaxOldestSlot
> to include NUM_AUXILIARY_PROCS.
>
> Certainly, it is not clearest patch possible:
> - may be you will decide to not introduce inline functions,
> - or will introduce separate inline function for each array,
> - or will fix slot calculation to remove aux procs from account,
> - or will revert deletion of dummyBackendId.
>
> [1]
> https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=cd87b6f8a5084c070c3e56b07794be8fe...
> [2]
> https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=ab355e3a88de745607f6dd4c21f0119b5...
>
> --
> regards
> Yura Sokolov aka funny-falcon
> <v00-0001-Fix-multixacts-OldestMemberMXactId-and-OldestVis.patch>
I think this is a good catch. I read through the related code paths:
In InitProcGlobal()
```
PreparedXactProcs = &procs[MaxBackends + NUM_AUXILIARY_PROCS];
```
Then in TwoPhaseShmemInit()
```
gxacts[i].pgprocno = GetNumberFromPGProc(&PreparedXactProcs[i]);
```
This shows prepared xacts are placed after MaxBackends + NUM_AUXILIARY_PROCS.
Also, in InitializeMaxBackends():
```
/* Note that this does not include "auxiliary" processes */
MaxBackends = MaxConnections + autovacuum_worker_slots +
max_worker_processes + max_wal_senders + NUM_SPECIAL_WORKER_PROCS;
```
So MaxBackends does not include NUM_AUXILIARY_PROCS.
Overall, the change looks correct to me.
One minor thought on the new helper asserts:
```
+ Assert(procno < MaxOldestSlot);
```
It may be worth also checking procno >= 0, since INVALID_PROC_NUMBER is -1.
While reading, I also noticed two stale comments related to this area. I added those fixes in 0002 (attached v2); 0001 is unchanged from the original v00.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
Attachments: download all (2 files, .tar.gz)
v2-0001-Fix-multixacts-OldestMemberMXactId-and-OldestVisi.patch (application/octet-stream, 7.2K) download
From 69248c04ee651f05604b1cb8bf9b16db8467f330 Mon Sep 17 00:00:00 2001
From: Yura Sokolov <y.sokolov@postgrespro.ru>
Date: Tue, 24 Feb 2026 20:29:01 +0300
Subject: [PATCH v2 1/2] Fix multixacts OldestMemberMXactId and
OldestVisibleMXactId initialization.
Due to [1], OldestMemberMXactId is no longer accessed by synthetic
dummyBackendId, but rather with pgprocno. Procs for prepared xacts are
placed after auxiliary procs, therefore calculation for MaxOldestSlot
became invalid.
Fix MaxOldestSlot to include NUM_AUXILIARY_PROCS and replace direct access
to these arrays with inline functions.
[1] ab355e3a88de745 "Redefine backend ID to be an index into the proc array"
---
src/backend/access/transam/multixact.c | 58 ++++++++++++++++++--------
1 file changed, 41 insertions(+), 17 deletions(-)
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 90ec87d9dd6..15dd928eb95 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -210,7 +210,7 @@ typedef struct MultiXactStateData
/*
* Size of OldestMemberMXactId and OldestVisibleMXactId arrays.
*/
-#define MaxOldestSlot (MaxBackends + max_prepared_xacts)
+#define MaxOldestSlot (MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts)
/* Pointers to the state data in shared memory */
static MultiXactStateData *MultiXactState;
@@ -285,6 +285,9 @@ static void WriteMTruncateXlogRec(Oid oldestMultiDB,
MultiXactId endTruncOff,
MultiXactOffset endTruncMemb);
+static inline MultiXactId getOldest(MultiXactId *oldest, ProcNumber procno);
+static inline bool validOldest(MultiXactId *oldest, ProcNumber procno);
+static inline void setOldest(MultiXactId *oldest, ProcNumber procno, MultiXactId mxact);
/*
* MultiXactIdCreate
@@ -308,7 +311,7 @@ MultiXactIdCreate(TransactionId xid1, MultiXactStatus status1,
Assert(!TransactionIdEquals(xid1, xid2) || (status1 != status2));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(validOldest(OldestMemberMXactId, MyProcNumber));
/*
* Note: unlike MultiXactIdExpand, we don't bother to check that both XIDs
@@ -362,7 +365,7 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status)
Assert(TransactionIdIsValid(xid));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(validOldest(OldestMemberMXactId, MyProcNumber));
debug_elog5(DEBUG2, "Expand: received multi %u, xid %u status %s",
multi, xid, mxstatus_to_string(status));
@@ -536,7 +539,7 @@ MultiXactIdIsRunning(MultiXactId multi, bool isLockOnly)
void
MultiXactIdSetOldestMember(void)
{
- if (!MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]))
+ if (!validOldest(OldestMemberMXactId, MyProcNumber))
{
MultiXactId nextMXact;
@@ -558,7 +561,7 @@ MultiXactIdSetOldestMember(void)
nextMXact = MultiXactState->nextMXact;
- OldestMemberMXactId[MyProcNumber] = nextMXact;
+ setOldest(OldestMemberMXactId, MyProcNumber, nextMXact);
LWLockRelease(MultiXactGenLock);
@@ -586,7 +589,7 @@ MultiXactIdSetOldestMember(void)
static void
MultiXactIdSetOldestVisible(void)
{
- if (!MultiXactIdIsValid(OldestVisibleMXactId[MyProcNumber]))
+ if (!validOldest(OldestVisibleMXactId, MyProcNumber))
{
MultiXactId oldestMXact;
int i;
@@ -603,7 +606,7 @@ MultiXactIdSetOldestVisible(void)
oldestMXact = thisoldest;
}
- OldestVisibleMXactId[MyProcNumber] = oldestMXact;
+ setOldest(OldestVisibleMXactId, MyProcNumber, oldestMXact);
LWLockRelease(MultiXactGenLock);
@@ -1152,7 +1155,7 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members,
* multi. It cannot possibly still be running.
*/
if (isLockOnly &&
- MultiXactIdPrecedes(multi, OldestVisibleMXactId[MyProcNumber]))
+ MultiXactIdPrecedes(multi, getOldest(OldestVisibleMXactId, MyProcNumber)))
{
debug_elog2(DEBUG2, "GetMembers: a locker-only multi is too old");
*members = NULL;
@@ -1574,8 +1577,8 @@ AtEOXact_MultiXact(void)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ setOldest(OldestMemberMXactId, MyProcNumber, InvalidMultiXactId);
+ setOldest(OldestVisibleMXactId, MyProcNumber, InvalidMultiXactId);
/*
* Discard the local MultiXactId cache. Since MXactContext was created as
@@ -1595,7 +1598,7 @@ AtEOXact_MultiXact(void)
void
AtPrepare_MultiXact(void)
{
- MultiXactId myOldestMember = OldestMemberMXactId[MyProcNumber];
+ MultiXactId myOldestMember = getOldest(OldestMemberMXactId, MyProcNumber);
if (MultiXactIdIsValid(myOldestMember))
RegisterTwoPhaseRecord(TWOPHASE_RM_MULTIXACT_ID, 0,
@@ -1615,7 +1618,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* Transfer our OldestMemberMXactId value to the slot reserved for the
* prepared transaction.
*/
- myOldestMember = OldestMemberMXactId[MyProcNumber];
+ myOldestMember = getOldest(OldestMemberMXactId, MyProcNumber);
if (MultiXactIdIsValid(myOldestMember))
{
ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(fxid, false);
@@ -1628,8 +1631,8 @@ PostPrepare_MultiXact(FullTransactionId fxid)
*/
LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
- OldestMemberMXactId[dummyProcNumber] = myOldestMember;
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
+ setOldest(OldestMemberMXactId, dummyProcNumber, myOldestMember);
+ setOldest(OldestMemberMXactId, MyProcNumber, InvalidMultiXactId);
LWLockRelease(MultiXactGenLock);
}
@@ -1642,7 +1645,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ setOldest(OldestVisibleMXactId, MyProcNumber, InvalidMultiXactId);
/*
* Discard the local MultiXactId cache like in AtEOXact_MultiXact.
@@ -1669,7 +1672,7 @@ multixact_twophase_recover(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
oldestMember = *((MultiXactId *) recdata);
- OldestMemberMXactId[dummyProcNumber] = oldestMember;
+ setOldest(OldestMemberMXactId, dummyProcNumber, oldestMember);
}
/*
@@ -1684,7 +1687,7 @@ multixact_twophase_postcommit(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
- OldestMemberMXactId[dummyProcNumber] = InvalidMultiXactId;
+ setOldest(OldestMemberMXactId, dummyProcNumber, InvalidMultiXactId);
}
/*
@@ -2922,3 +2925,24 @@ multixactmemberssyncfiletag(const FileTag *ftag, char *path)
{
return SlruSyncFileTag(MultiXactMemberCtl, ftag, path);
}
+
+static inline MultiXactId
+getOldest(MultiXactId *oldest, ProcNumber procno)
+{
+ Assert(procno < MaxOldestSlot);
+ return oldest[procno];
+}
+
+static inline bool
+validOldest(MultiXactId *oldest, ProcNumber procno)
+{
+ Assert(procno < MaxOldestSlot);
+ return MultiXactIdIsValid(oldest[procno]);
+}
+
+static inline void
+setOldest(MultiXactId *oldest, ProcNumber procno, MultiXactId mxact)
+{
+ Assert(procno < MaxOldestSlot);
+ oldest[procno] = mxact;
+}
--
2.50.1 (Apple Git-155)
=
v2-0002-Fix-stale-comments-about-prepared-xact-proc-numbe.patch (application/octet-stream, 2.8K) download
From 9f6e79663d9988148051930009a1d400206d7671 Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Wed, 25 Feb 2026 09:27:59 +0800
Subject: [PATCH v2 2/2] Fix stale comments about prepared-xact proc number
layout
Update two comments to match current ProcNumber/PGPROC layout after
backend ID and proc-array indexing changes:
- multixact.c: clarify that per-backend multixact horizon arrays include
auxiliary process slots between normal backends and prepared xacts.
- twophase.c: clarify that dummy proc numbers for prepared transactions
start at MaxBackends + NUM_AUXILIARY_PROCS (not MaxBackends).
Author: Chao Li <lic@highgo.com>
Reviewed-by:
Discussion: https://postgr.es/m/7acc94b0-ea82-4657-b1b0-77842cb7a60c@postgrespro.ru
---
src/backend/access/transam/multixact.c | 7 ++++---
src/backend/access/transam/twophase.c | 7 ++++---
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 15dd928eb95..ddc79d19fd6 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -162,9 +162,10 @@ typedef struct MultiXactStateData
* immediately following the MultiXactStateData struct. Each is indexed by
* ProcNumber.
*
- * In both arrays, there's a slot for all normal backends
- * (0..MaxBackends-1) followed by a slot for max_prepared_xacts prepared
- * transactions.
+ * In both arrays, there are slots for all normal backends
+ * (0..MaxBackends-1), followed by slots for auxiliary processes
+ * (MaxBackends..MaxBackends + NUM_AUXILIARY_PROCS - 1), followed by slots
+ * for max_prepared_xacts prepared transactions.
*
* OldestMemberMXactId[k] is the oldest MultiXactId each backend's current
* transaction(s) could possibly be a member of, or InvalidMultiXactId
diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index e4340b59640..8d1644f6988 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -900,9 +900,10 @@ TwoPhaseGetXidByVirtualXID(VirtualTransactionId vxid,
* Get the dummy proc number for prepared transaction
*
* Dummy proc numbers are similar to proc numbers of real backends. They
- * start at MaxBackends, and are unique across all currently active real
- * backends and prepared transactions. If lock_held is set to true,
- * TwoPhaseStateLock will not be taken, so the caller had better hold it.
+ * start at MaxBackends + NUM_AUXILIARY_PROCS, and are unique across all
+ * currently active real backends and prepared transactions. If lock_held
+ * is set to true, TwoPhaseStateLock will not be taken, so the caller had
+ * better hold it.
*/
ProcNumber
TwoPhaseGetDummyProcNumber(FullTransactionId fxid, bool lock_held)
--
2.50.1 (Apple Git-155)
=
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-25 15:16 Yura Sokolov <y.sokolov@postgrespro.ru>
parent: Chao Li <li.evan.chao@gmail.com>
0 siblings, 2 replies; 31+ messages in thread
From: Yura Sokolov @ 2026-02-25 15:16 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; +Cc: Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Chao Li <li.evan.chao@gmail.com>; Sami Imseih <samimseih@gmail.com>
Good day.
Chao Li and Sami Imseih, thank you for looking at.
After thinking a bit, I've decided to make sizes of arrays precise:
- OldestMemberMXactId's size remains MaxBackends + max_prepared_xacts.
Instead of changing its size, procno is now adjusted to not include
auxiliary procs.
- OldestVisibleMXactId contains only MaxBackends elemenents now.
It is used only for real backends and not prepared transactions.
All accesses are validated with asserts certainly.
I believe, index transformation in access of OldestMemberMXactId will not
cost much since all this operations are quite rare.
In the loops arrays are accessed directly since limiting loop index is enough.
--
regards
Yura Sokolov aka funny-falcon
Attachments:
v3-0001-Fix-multixacts-OldestMemberMXactId-and-OldestVisi.patch (text/x-patch, 9.8K) download
From c4ae9ee5f8d287cbda8c4a5a780ad6ca25b1ede1 Mon Sep 17 00:00:00 2001
From: Yura Sokolov <y.sokolov@postgrespro.ru>
Date: Wed, 25 Feb 2026 18:03:57 +0300
Subject: [PATCH v3] Fix multixacts OldestMemberMXactId and
OldestVisibleMXactId usage
Due to [1], OldestMemberMXactId is no longer accessed by synthetic
dummyBackendId, but rather with pgprocno. Procs for prepared xacts are
placed after auxiliary procs, therefore calculation for MaxOldestSlot
became invalid.
On the other hand, OldestVisibleMXactId is used only for real backends,
and so never accessed at index greater than MaxBackends.
Lets separate size calculation for arrays and use conversion of index
in access to OldestMemberMXactId.
[1] ab355e3a88de745 "Redefine backend ID to be an index into the proc array"
---
src/backend/access/transam/multixact.c | 102 +++++++++++++++++--------
1 file changed, 70 insertions(+), 32 deletions(-)
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 90ec87d9dd6..ca515092cfd 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -159,12 +159,7 @@ typedef struct MultiXactStateData
/*
* Per-backend data starts here. We have two arrays stored in the area
- * immediately following the MultiXactStateData struct. Each is indexed by
- * ProcNumber.
- *
- * In both arrays, there's a slot for all normal backends
- * (0..MaxBackends-1) followed by a slot for max_prepared_xacts prepared
- * transactions.
+ * immediately following the MultiXactStateData struct.
*
* OldestMemberMXactId[k] is the oldest MultiXactId each backend's current
* transaction(s) could possibly be a member of, or InvalidMultiXactId
@@ -203,6 +198,14 @@ typedef struct MultiXactStateData
* In turn, the minimum of all of those values is stored in pg_control.
* This is used as the truncation point for pg_multixact when unneeded
* segments get removed by vac_truncate_clog() during vacuuming.
+ *
+ * In OldestMemberMXactId[] there's a slot for all normal backends
+ * (0..MaxBackends-1) followed by a slot for max_prepared_xacts prepared
+ * transactions. It is indexed by value derived from ProcNumber.
+ *
+ * OldestVisibleMXactId has only slots for normal backends since it is not
+ * used for prepared transactions. And it is indexed by ProcNumber
+ * directly.
*/
MultiXactId perBackendXactIds[FLEXIBLE_ARRAY_MEMBER];
} MultiXactStateData;
@@ -210,13 +213,52 @@ typedef struct MultiXactStateData
/*
* Size of OldestMemberMXactId and OldestVisibleMXactId arrays.
*/
-#define MaxOldestSlot (MaxBackends + max_prepared_xacts)
+#define MaxOldestMemberSlot (MaxBackends + max_prepared_xacts)
+#define MaxOldestVisibleSlot MaxBackends
+#define MaxOldestSlot (MaxOldestMemberSlot + MaxOldestVisibleSlot)
/* Pointers to the state data in shared memory */
static MultiXactStateData *MultiXactState;
static MultiXactId *OldestMemberMXactId;
static MultiXactId *OldestVisibleMXactId;
+static inline MultiXactId
+GetOldestMemberMXactId(ProcNumber procno)
+{
+ if (procno >= MaxBackends)
+ {
+ Assert(procno >= MaxBackends + NUM_AUXILIARY_PROCS);
+ procno -= NUM_AUXILIARY_PROCS;
+ }
+ Assert(procno >= 0 && procno < MaxOldestMemberSlot);
+ return OldestMemberMXactId[procno];
+}
+
+static inline void
+SetOldestMemberMXactId(ProcNumber procno, MultiXactId value)
+{
+ if (procno >= MaxBackends)
+ {
+ Assert(procno >= MaxBackends + NUM_AUXILIARY_PROCS);
+ procno -= NUM_AUXILIARY_PROCS;
+ }
+ Assert(procno >= 0 && procno < MaxOldestMemberSlot);
+ OldestMemberMXactId[procno] = value;
+}
+
+static inline MultiXactId
+GetOldestVisibleMXactId(ProcNumber procno)
+{
+ Assert(procno >= 0 && procno < MaxOldestVisibleSlot);
+ return OldestVisibleMXactId[procno];
+}
+
+static inline void
+SetOldestVisibleMXactId(ProcNumber procno, MultiXactId value)
+{
+ Assert(procno >= 0 && procno < MaxOldestVisibleSlot);
+ OldestVisibleMXactId[procno] = value;
+}
/*
* Definitions for the backend-local MultiXactId cache.
@@ -308,7 +350,7 @@ MultiXactIdCreate(TransactionId xid1, MultiXactStatus status1,
Assert(!TransactionIdEquals(xid1, xid2) || (status1 != status2));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(MultiXactIdIsValid(GetOldestMemberMXactId(MyProcNumber)));
/*
* Note: unlike MultiXactIdExpand, we don't bother to check that both XIDs
@@ -362,7 +404,7 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status)
Assert(TransactionIdIsValid(xid));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(MultiXactIdIsValid(GetOldestMemberMXactId(MyProcNumber)));
debug_elog5(DEBUG2, "Expand: received multi %u, xid %u status %s",
multi, xid, mxstatus_to_string(status));
@@ -536,7 +578,7 @@ MultiXactIdIsRunning(MultiXactId multi, bool isLockOnly)
void
MultiXactIdSetOldestMember(void)
{
- if (!MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]))
+ if (!MultiXactIdIsValid(GetOldestMemberMXactId(MyProcNumber)))
{
MultiXactId nextMXact;
@@ -558,7 +600,7 @@ MultiXactIdSetOldestMember(void)
nextMXact = MultiXactState->nextMXact;
- OldestMemberMXactId[MyProcNumber] = nextMXact;
+ SetOldestMemberMXactId(MyProcNumber, nextMXact);
LWLockRelease(MultiXactGenLock);
@@ -594,7 +636,7 @@ MultiXactIdSetOldestVisible(void)
LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
oldestMXact = MultiXactState->nextMXact;
- for (i = 0; i < MaxOldestSlot; i++)
+ for (i = 0; i < MaxOldestMemberSlot; i++)
{
MultiXactId thisoldest = OldestMemberMXactId[i];
@@ -603,7 +645,7 @@ MultiXactIdSetOldestVisible(void)
oldestMXact = thisoldest;
}
- OldestVisibleMXactId[MyProcNumber] = oldestMXact;
+ SetOldestVisibleMXactId(MyProcNumber, oldestMXact);
LWLockRelease(MultiXactGenLock);
@@ -1574,8 +1616,8 @@ AtEOXact_MultiXact(void)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ SetOldestMemberMXactId(MyProcNumber, InvalidMultiXactId);
+ SetOldestVisibleMXactId(MyProcNumber, InvalidMultiXactId);
/*
* Discard the local MultiXactId cache. Since MXactContext was created as
@@ -1595,7 +1637,7 @@ AtEOXact_MultiXact(void)
void
AtPrepare_MultiXact(void)
{
- MultiXactId myOldestMember = OldestMemberMXactId[MyProcNumber];
+ MultiXactId myOldestMember = GetOldestMemberMXactId(MyProcNumber);
if (MultiXactIdIsValid(myOldestMember))
RegisterTwoPhaseRecord(TWOPHASE_RM_MULTIXACT_ID, 0,
@@ -1615,7 +1657,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* Transfer our OldestMemberMXactId value to the slot reserved for the
* prepared transaction.
*/
- myOldestMember = OldestMemberMXactId[MyProcNumber];
+ myOldestMember = GetOldestMemberMXactId(MyProcNumber);
if (MultiXactIdIsValid(myOldestMember))
{
ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(fxid, false);
@@ -1628,8 +1670,8 @@ PostPrepare_MultiXact(FullTransactionId fxid)
*/
LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
- OldestMemberMXactId[dummyProcNumber] = myOldestMember;
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
+ SetOldestMemberMXactId(dummyProcNumber, myOldestMember);
+ SetOldestMemberMXactId(MyProcNumber, InvalidMultiXactId);
LWLockRelease(MultiXactGenLock);
}
@@ -1642,7 +1684,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ SetOldestVisibleMXactId(MyProcNumber, InvalidMultiXactId);
/*
* Discard the local MultiXactId cache like in AtEOXact_MultiXact.
@@ -1669,7 +1711,7 @@ multixact_twophase_recover(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
oldestMember = *((MultiXactId *) recdata);
- OldestMemberMXactId[dummyProcNumber] = oldestMember;
+ SetOldestMemberMXactId(dummyProcNumber, oldestMember);
}
/*
@@ -1684,7 +1726,7 @@ multixact_twophase_postcommit(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
- OldestMemberMXactId[dummyProcNumber] = InvalidMultiXactId;
+ SetOldestMemberMXactId(dummyProcNumber, InvalidMultiXactId);
}
/*
@@ -1708,10 +1750,9 @@ MultiXactShmemSize(void)
{
Size size;
- /* We need 2*MaxOldestSlot perBackendXactIds[] entries */
#define SHARED_MULTIXACT_STATE_SIZE \
add_size(offsetof(MultiXactStateData, perBackendXactIds), \
- mul_size(sizeof(MultiXactId) * 2, MaxOldestSlot))
+ mul_size(sizeof(MultiXactId), MaxOldestSlot))
size = SHARED_MULTIXACT_STATE_SIZE;
size = add_size(size, SimpleLruShmemSize(multixact_offset_buffers, 0));
@@ -1763,7 +1804,7 @@ MultiXactShmemInit(void)
* Set up array pointers.
*/
OldestMemberMXactId = MultiXactState->perBackendXactIds;
- OldestVisibleMXactId = OldestMemberMXactId + MaxOldestSlot;
+ OldestVisibleMXactId = OldestMemberMXactId + MaxOldestMemberSlot;
}
/*
@@ -2303,6 +2344,8 @@ MultiXactId
GetOldestMultiXactId(void)
{
MultiXactId oldestMXact;
+ MultiXactId thisoldest;
+ MultiXactId *allOldest;
int i;
/*
@@ -2311,15 +2354,10 @@ GetOldestMultiXactId(void)
*/
LWLockAcquire(MultiXactGenLock, LW_SHARED);
oldestMXact = MultiXactState->nextMXact;
+ allOldest = MultiXactState->perBackendXactIds;
for (i = 0; i < MaxOldestSlot; i++)
{
- MultiXactId thisoldest;
-
- thisoldest = OldestMemberMXactId[i];
- if (MultiXactIdIsValid(thisoldest) &&
- MultiXactIdPrecedes(thisoldest, oldestMXact))
- oldestMXact = thisoldest;
- thisoldest = OldestVisibleMXactId[i];
+ thisoldest = allOldest[i];
if (MultiXactIdIsValid(thisoldest) &&
MultiXactIdPrecedes(thisoldest, oldestMXact))
oldestMXact = thisoldest;
--
2.51.0
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-25 21:53 Sami Imseih <samimseih@gmail.com>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
1 sibling, 0 replies; 31+ messages in thread
From: Sami Imseih @ 2026-02-25 21:53 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Chao Li <li.evan.chao@gmail.com>
Hi.
> All accesses are validated with asserts certainly.
Maybe I am missing something, but the extra asserts and wrapper
functions being proposed seem unnecessary for this purpose.
We know the total number of procs, so we just need to make this
available to all code paths that need to index by pgprocno.
If that calculation ever changes, code like multixact.c will not
need to care.
This can be done, as mentioned earlier, by defining the total
number of procs in proc.h, which can then be used by both
multixact.c and InitProcGlobal.
```
#define TOTAL_PROCS (MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts)
```
and to calculate where the prepared transaction procs start:
```
PreparedXactProcs = &procs[TOTAL_PROCS - max_prepared_xacts];
```
What do you think?
With regards to v3, I got a compilation warning that `GetOldestVisibleMXactId`
is an unused function.
--
Sami Imseih
Amazon Web Services (AWS)
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-26 02:21 Chao Li <li.evan.chao@gmail.com>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
1 sibling, 1 reply; 31+ messages in thread
From: Chao Li @ 2026-02-26 02:21 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Sami Imseih <samimseih@gmail.com>
> On Feb 25, 2026, at 23:16, Yura Sokolov <y.sokolov@postgrespro.ru> wrote:
>
> Good day.
>
> Chao Li and Sami Imseih, thank you for looking at.
>
> After thinking a bit, I've decided to make sizes of arrays precise:
> - OldestMemberMXactId's size remains MaxBackends + max_prepared_xacts.
> Instead of changing its size, procno is now adjusted to not include
> auxiliary procs.
> - OldestVisibleMXactId contains only MaxBackends elemenents now.
> It is used only for real backends and not prepared transactions.
>
> All accesses are validated with asserts certainly.
>
> I believe, index transformation in access of OldestMemberMXactId will not
> cost much since all this operations are quite rare.
> In the loops arrays are accessed directly since limiting loop index is enough.
>
> --
> regards
> Yura Sokolov aka funny-falcon<v3-0001-Fix-multixacts-OldestMemberMXactId-and-OldestVisi.patch>
Actually, while I reviewing v1, I was thinking over the same way as v3, as the NUM_AUXILIARY_PROCS portions in OldestMemberMXactId and OldestVisibleMXactId arrays are not used at all. I didn’t raise the idea because that given these code are in hot paths, I wasn't sure if the complexity was worth the shared memory optimization. It’s a classic trade-off, and I’m curious to see which direction the senior committers prefer.
However, I agree that the new helper function names in v3 are a significant improvement over v1.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-26 16:22 Yura Sokolov <y.sokolov@postgrespro.ru>
parent: Chao Li <li.evan.chao@gmail.com>
0 siblings, 2 replies; 31+ messages in thread
From: Yura Sokolov @ 2026-02-26 16:22 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; +Cc: Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Sami Imseih <samimseih@gmail.com>; Chao Li <li.evan.chao@gmail.com>
Good day.
Sami Imseih, I've introduced new precalculated variables:
MaxChildren = MaxBackends + NUM_AUXILIARY_PROCS;
TotalProcs = MaxChildren + max_prepared_xacts;
TotalXactProcs = MaxBackends + max_prepared_xacts;
and spread their usage through the sources.
I don't know how committers will accept this change. But they will
certainly make their own version of patch, so I don't bother much.
But I still use inline procs for access to the arrays. Asserts cost nothing
in release build. And new version of functions doesn't branch.
And you're right: looks like there were mistake in previous version so
GetOldestVisibleMXactId became unused. New version has no this mistake.
Chao Li, new access functions doen't branch because I've made separate
functions to store at slots for prepared transactions.
--
regards
Yura Sokolov aka funny-falcon
Attachments: download all (2 files, .tar.gz)
v4-0001-Add-precalculated-proc-counts-to-reduce-repetitio.patch (text/x-patch, 15.7K) download
From a9db95d6f97300d49c5c3f8ac1ecb87c9d7cf429 Mon Sep 17 00:00:00 2001
From: Yura Sokolov <y.sokolov@postgrespro.ru>
Date: Thu, 26 Feb 2026 17:41:40 +0300
Subject: [PATCH v4 1/2] Add precalculated proc counts to reduce repetition.
Lets name common sums:
MaxBackends + NUM_AUXILIARY_PROCS => MaxChildren
MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts => TotalProcs
MaxBackends + max_prepared_xacts => TotalXactProcs
It also may simplify api for extension.
---
src/backend/access/transam/multixact.c | 2 +-
src/backend/access/transam/xlogwait.c | 7 +++----
src/backend/postmaster/launch_backend.c | 4 ++++
src/backend/storage/aio/aio_init.c | 4 ++--
src/backend/storage/aio/method_io_uring.c | 2 +-
src/backend/storage/buffer/bufmgr.c | 2 +-
src/backend/storage/ipc/procarray.c | 2 +-
src/backend/storage/ipc/procsignal.c | 2 +-
src/backend/storage/ipc/sinvaladt.c | 2 +-
src/backend/storage/lmgr/lock.c | 9 ++++-----
src/backend/storage/lmgr/predicate.c | 13 +++++--------
src/backend/storage/lmgr/proc.c | 13 ++++---------
src/backend/utils/activity/backend_status.c | 2 +-
src/backend/utils/init/globals.c | 7 +++++--
src/backend/utils/init/postinit.c | 9 +++++++--
src/include/miscadmin.h | 3 +++
16 files changed, 44 insertions(+), 39 deletions(-)
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 90ec87d9dd6..a35671ea346 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -210,7 +210,7 @@ typedef struct MultiXactStateData
/*
* Size of OldestMemberMXactId and OldestVisibleMXactId arrays.
*/
-#define MaxOldestSlot (MaxBackends + max_prepared_xacts)
+#define MaxOldestSlot TotalXactProcs
/* Pointers to the state data in shared memory */
static MultiXactStateData *MultiXactState;
diff --git a/src/backend/access/transam/xlogwait.c b/src/backend/access/transam/xlogwait.c
index d286ff63123..4c2f3085e34 100644
--- a/src/backend/access/transam/xlogwait.c
+++ b/src/backend/access/transam/xlogwait.c
@@ -115,7 +115,7 @@ WaitLSNShmemSize(void)
Size size;
size = offsetof(WaitLSNState, procInfos);
- size = add_size(size, mul_size(MaxBackends + NUM_AUXILIARY_PROCS, sizeof(WaitLSNProcInfo)));
+ size = add_size(size, mul_size(MaxChildren, sizeof(WaitLSNProcInfo)));
return size;
}
@@ -140,8 +140,7 @@ WaitLSNShmemInit(void)
}
/* Initialize process info array */
- memset(&waitLSNState->procInfos, 0,
- (MaxBackends + NUM_AUXILIARY_PROCS) * sizeof(WaitLSNProcInfo));
+ memset(&waitLSNState->procInfos, 0, MaxChildren * sizeof(WaitLSNProcInfo));
}
}
@@ -382,7 +381,7 @@ WaitForLSN(WaitLSNType lsnType, XLogRecPtr targetLSN, int64 timeout)
Assert(waitLSNState);
/* Should have a valid proc number */
- Assert(MyProcNumber >= 0 && MyProcNumber < MaxBackends + NUM_AUXILIARY_PROCS);
+ Assert(MyProcNumber >= 0 && MyProcNumber < MaxChildren);
if (timeout > 0)
{
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index e9134b9751b..1b5c5b7f550 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -62,6 +62,7 @@
#ifdef EXEC_BACKEND
+#include "access/twophase.h"
#include "common/file_utils.h"
#include "storage/fd.h"
#include "storage/lwlock.h"
@@ -1010,6 +1011,9 @@ restore_backend_variables(BackendParameters *param)
max_safe_fds = param->max_safe_fds;
MaxBackends = param->MaxBackends;
+ MaxChildren = MaxBackends + NUM_AUXILIARY_PROCS;
+ TotalProcs = MaxChildren + max_prepared_xacts;
+ TotalXactProcs = MaxBackends + max_prepared_xacts;
num_pmchild_slots = param->num_pmchild_slots;
#ifdef WIN32
diff --git a/src/backend/storage/aio/aio_init.c b/src/backend/storage/aio/aio_init.c
index d3c68d8b04c..df1e750e802 100644
--- a/src/backend/storage/aio/aio_init.c
+++ b/src/backend/storage/aio/aio_init.c
@@ -42,7 +42,7 @@ AioProcs(void)
* guarantee that nothing gets assigned to an IO worker's ProcNumber if we
* just subtracted MAX_IO_WORKERS.
*/
- return MaxBackends + NUM_AUXILIARY_PROCS;
+ return MaxChildren;
}
static Size
@@ -100,7 +100,7 @@ AioChooseMaxConcurrency(void)
int max_proportional_pins;
/* Similar logic to LimitAdditionalPins() */
- max_backends = MaxBackends + NUM_AUXILIARY_PROCS;
+ max_backends = MaxChildren;
max_proportional_pins = NBuffers / max_backends;
max_proportional_pins = Max(max_proportional_pins, 1);
diff --git a/src/backend/storage/aio/method_io_uring.c b/src/backend/storage/aio/method_io_uring.c
index ed6e71bcd46..904deeb724f 100644
--- a/src/backend/storage/aio/method_io_uring.c
+++ b/src/backend/storage/aio/method_io_uring.c
@@ -131,7 +131,7 @@ pgaio_uring_procs(void)
* We can subtract MAX_IO_WORKERS here as io workers are never used at the
* same time as io_method=io_uring.
*/
- return MaxBackends + NUM_AUXILIARY_PROCS - MAX_IO_WORKERS;
+ return MaxChildren - MAX_IO_WORKERS;
}
/*
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index d1babaff023..c1d6306a073 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -4129,7 +4129,7 @@ InitBufferManagerAccess(void)
* allow plenty of pins. LimitAdditionalPins() and
* GetAdditionalPinLimit() can be used to check the remaining balance.
*/
- MaxProportionalPins = NBuffers / (MaxBackends + NUM_AUXILIARY_PROCS);
+ MaxProportionalPins = NBuffers / MaxChildren;
memset(&PrivateRefCountArray, 0, sizeof(PrivateRefCountArray));
memset(&PrivateRefCountArrayKeys, 0, sizeof(PrivateRefCountArrayKeys));
diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index 40312df2cac..807c292f481 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -381,7 +381,7 @@ ProcArrayShmemSize(void)
Size size;
/* Size of the ProcArray structure itself */
-#define PROCARRAY_MAXPROCS (MaxBackends + max_prepared_xacts)
+#define PROCARRAY_MAXPROCS TotalXactProcs
size = offsetof(ProcArrayStruct, pgprocnos);
size = add_size(size, mul_size(sizeof(int), PROCARRAY_MAXPROCS));
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7505c9d3a37..4694f645805 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -93,7 +93,7 @@ struct ProcSignalHeader
* more than one of any auxiliary process type at a time, except for
* IO workers.)
*/
-#define NumProcSignalSlots (MaxBackends + NUM_AUXILIARY_PROCS)
+#define NumProcSignalSlots MaxChildren
/* Check whether the relevant type bit is set in the flags. */
#define BARRIER_SHOULD_CHECK(flags, type) \
diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.c
index a7a7cc4f0a9..ec030b9466b 100644
--- a/src/backend/storage/ipc/sinvaladt.c
+++ b/src/backend/storage/ipc/sinvaladt.c
@@ -201,7 +201,7 @@ typedef struct SISeg
* more than one of any auxiliary process type at a time, except for
* IO workers.)
*/
-#define NumProcStateSlots (MaxBackends + NUM_AUXILIARY_PROCS)
+#define NumProcStateSlots MaxChildren
static SISeg *shmInvalBuffer; /* pointer to the shared inval buffer */
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index d930c66cdbd..d3294200c2c 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -53,8 +53,7 @@
int max_locks_per_xact; /* used to set the lock table size */
bool log_lock_failures = false;
-#define NLOCKENTS() \
- mul_size(max_locks_per_xact, add_size(MaxBackends, max_prepared_xacts))
+#define NLOCKENTS() mul_size(max_locks_per_xact, TotalXactProcs)
/*
@@ -3097,10 +3096,10 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
vxids = (VirtualTransactionId *)
MemoryContextAlloc(TopMemoryContext,
sizeof(VirtualTransactionId) *
- (MaxBackends + max_prepared_xacts + 1));
+ (TotalXactProcs + 1));
}
else
- vxids = palloc0_array(VirtualTransactionId, (MaxBackends + max_prepared_xacts + 1));
+ vxids = palloc0_array(VirtualTransactionId, (TotalXactProcs + 1));
/* Compute hash code and partition lock, and look up conflicting modes. */
hashcode = LockTagHashCode(locktag);
@@ -3259,7 +3258,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
LWLockRelease(partitionLock);
- if (count > MaxBackends + max_prepared_xacts) /* should never happen */
+ if (count > TotalXactProcs) /* should never happen */
elog(PANIC, "too many conflicting locks found");
vxids[count].procNumber = INVALID_PROC_NUMBER;
diff --git a/src/backend/storage/lmgr/predicate.c b/src/backend/storage/lmgr/predicate.c
index fe75ead3501..5a14422ad4c 100644
--- a/src/backend/storage/lmgr/predicate.c
+++ b/src/backend/storage/lmgr/predicate.c
@@ -261,8 +261,7 @@
#define PredicateLockHashPartitionLockByIndex(i) \
(&MainLWLockArray[PREDICATELOCK_MANAGER_LWLOCK_OFFSET + (i)].lock)
-#define NPREDICATELOCKTARGETENTS() \
- mul_size(max_predicate_locks_per_xact, add_size(MaxBackends, max_prepared_xacts))
+#define NPREDICATELOCKTARGETENTS() mul_size(max_predicate_locks_per_xact, TotalXactProcs)
#define SxactIsOnFinishedList(sxact) (!dlist_node_is_detached(&(sxact)->finishedLink))
@@ -1214,7 +1213,7 @@ PredicateLockShmemInit(void)
* Compute size for serializable transaction hashtable. Note these
* calculations must agree with PredicateLockShmemSize!
*/
- max_table_size = (MaxBackends + max_prepared_xacts);
+ max_table_size = TotalXactProcs;
/*
* Allocate a list to hold information on transactions participating in
@@ -1376,7 +1375,7 @@ PredicateLockShmemSize(void)
size = add_size(size, size / 10);
/* transaction list */
- max_table_size = MaxBackends + max_prepared_xacts;
+ max_table_size = TotalXactProcs;
max_table_size *= 10;
size = add_size(size, PredXactListDataSize);
size = add_size(size, mul_size((Size) max_table_size,
@@ -1902,8 +1901,7 @@ GetSerializableTransactionSnapshotInt(Snapshot snapshot,
else
{
++(PredXact->WritableSxactCount);
- Assert(PredXact->WritableSxactCount <=
- (MaxBackends + max_prepared_xacts));
+ Assert(PredXact->WritableSxactCount <= TotalXactProcs);
}
/* Maintain serializable global xmin info. */
@@ -4965,8 +4963,7 @@ predicatelock_twophase_recover(FullTransactionId fxid, uint16 info,
if (!SxactIsReadOnly(sxact))
{
++(PredXact->WritableSxactCount);
- Assert(PredXact->WritableSxactCount <=
- (MaxBackends + max_prepared_xacts));
+ Assert(PredXact->WritableSxactCount <= TotalXactProcs);
}
/*
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 771b006b522..52cc3c7c702 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -87,8 +87,6 @@ static Size
PGProcShmemSize(void)
{
Size size = 0;
- Size TotalProcs =
- add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts));
size = add_size(size, mul_size(TotalProcs, sizeof(PGPROC)));
size = add_size(size, mul_size(TotalProcs, sizeof(*ProcGlobal->xids)));
@@ -105,8 +103,6 @@ static Size
FastPathLockShmemSize(void)
{
Size size = 0;
- Size TotalProcs =
- add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts));
Size fpLockBitsSize,
fpRelIdSize;
@@ -151,7 +147,7 @@ ProcGlobalSemas(void)
* We need a sema per backend (including autovacuum), plus one for each
* auxiliary process.
*/
- return MaxBackends + NUM_AUXILIARY_PROCS;
+ return MaxChildren;
}
/*
@@ -186,7 +182,6 @@ InitProcGlobal(void)
int i,
j;
bool found;
- uint32 TotalProcs = MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts;
/* Used for setup of per-backend fast-path slots. */
char *fpPtr,
@@ -238,7 +233,7 @@ InitProcGlobal(void)
ProcGlobal->allProcs = procs;
/* XXX allProcCount isn't really all of them; it excludes prepared xacts */
- ProcGlobal->allProcCount = MaxBackends + NUM_AUXILIARY_PROCS;
+ ProcGlobal->allProcCount = MaxChildren;
/*
* Allocate arrays mirroring PGPROC fields in a dense manner. See
@@ -304,7 +299,7 @@ InitProcGlobal(void)
* dummy PGPROCs don't need these though - they're never associated
* with a real process
*/
- if (i < MaxBackends + NUM_AUXILIARY_PROCS)
+ if (i < MaxChildren)
{
proc->sem = PGSemaphoreCreate();
InitSharedLatch(&(proc->procLatch));
@@ -369,7 +364,7 @@ InitProcGlobal(void)
* processes and prepared transactions.
*/
AuxiliaryProcs = &procs[MaxBackends];
- PreparedXactProcs = &procs[MaxBackends + NUM_AUXILIARY_PROCS];
+ PreparedXactProcs = &procs[MaxChildren];
}
/*
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index cd087129469..ed797da6bee 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -32,7 +32,7 @@
* real processes.)
* ----------
*/
-#define NumBackendStatSlots (MaxBackends + NUM_AUXILIARY_PROCS)
+#define NumBackendStatSlots MaxChildren
/* ----------
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 36ad708b360..1fbef826a8b 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -136,14 +136,17 @@ int max_parallel_maintenance_workers = 2;
/*
* Primary determinants of sizes of shared-memory structures.
*
- * MaxBackends is computed by PostmasterMain after modules have had a chance to
- * register background workers.
+ * MaxBackends, MaxChildren and TotalProcs are computed by PostmasterMain after
+ * modules have had a chance to register background workers.
*/
int NBuffers = 16384;
int MaxConnections = 100;
int max_worker_processes = 8;
int max_parallel_workers = 8;
int MaxBackends = 0;
+int MaxChildren = 0;
+int TotalProcs = 0;
+int TotalXactProcs = 0;
/* GUC parameters for vacuum */
int VacuumBufferUsageLimit = 2048;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index b59e08605cc..cc295878868 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -24,6 +24,7 @@
#include "access/htup_details.h"
#include "access/session.h"
#include "access/tableam.h"
+#include "access/twophase.h"
#include "access/xact.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
@@ -563,14 +564,18 @@ InitializeMaxBackends(void)
MaxBackends = MaxConnections + autovacuum_worker_slots +
max_worker_processes + max_wal_senders + NUM_SPECIAL_WORKER_PROCS;
- if (MaxBackends > MAX_BACKENDS)
+ MaxChildren = MaxBackends + NUM_AUXILIARY_PROCS;
+ if (MaxChildren > MAX_BACKENDS)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("too many server processes configured"),
errdetail("\"max_connections\" (%d) plus \"autovacuum_worker_slots\" (%d) plus \"max_worker_processes\" (%d) plus \"max_wal_senders\" (%d) must be less than %d.",
MaxConnections, autovacuum_worker_slots,
max_worker_processes, max_wal_senders,
- MAX_BACKENDS - (NUM_SPECIAL_WORKER_PROCS - 1))));
+ MAX_BACKENDS - NUM_AUXILIARY_PROCS - (NUM_SPECIAL_WORKER_PROCS - 1))));
+
+ TotalProcs = MaxChildren + max_prepared_xacts;
+ TotalXactProcs = MaxBackends + max_prepared_xacts;
}
/*
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f16f35659b9..b2498455ea4 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -175,6 +175,9 @@ extern PGDLLIMPORT int data_directory_mode;
extern PGDLLIMPORT int NBuffers;
extern PGDLLIMPORT int MaxBackends;
+extern PGDLLIMPORT int MaxChildren;
+extern PGDLLIMPORT int TotalProcs;
+extern PGDLLIMPORT int TotalXactProcs;
extern PGDLLIMPORT int MaxConnections;
extern PGDLLIMPORT int max_worker_processes;
extern PGDLLIMPORT int max_parallel_workers;
--
2.51.0
v4-0002-Fix-multixacts-OldestMemberMXactId-and-OldestVisi.patch (text/x-patch, 10.9K) download
From d3c2bb72de0b4f5e3149cd8f01493c37315067ce Mon Sep 17 00:00:00 2001
From: Yura Sokolov <y.sokolov@postgrespro.ru>
Date: Thu, 26 Feb 2026 19:08:57 +0300
Subject: [PATCH v4 2/2] Fix multixacts OldestMemberMXactId and
OldestVisibleMXactId usage
Due to [1], OldestMemberMXactId is no longer accessed by synthetic
dummyBackendId, but rather with pgprocno. Procs for prepared xacts are
placed after auxiliary procs, therefore calculation for MaxOldestSlot
became invalid.
On the other hand, OldestVisibleMXactId is used only for real backends,
and so never accessed at index greater than MaxBackends.
Lets separate size calculation for arrays and use converted index to
store oldest member mxact for prepared transactions.
[1] ab355e3a88de745 "Redefine backend ID to be an index into the proc array"
---
src/backend/access/transam/multixact.c | 104 +++++++++++++++++--------
1 file changed, 72 insertions(+), 32 deletions(-)
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index a35671ea346..42859828513 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -162,10 +162,6 @@ typedef struct MultiXactStateData
* immediately following the MultiXactStateData struct. Each is indexed by
* ProcNumber.
*
- * In both arrays, there's a slot for all normal backends
- * (0..MaxBackends-1) followed by a slot for max_prepared_xacts prepared
- * transactions.
- *
* OldestMemberMXactId[k] is the oldest MultiXactId each backend's current
* transaction(s) could possibly be a member of, or InvalidMultiXactId
* when the backend has no live transaction that could possibly be a
@@ -176,6 +172,10 @@ typedef struct MultiXactStateData
* member of a MultiXact, and that MultiXact would have to be created
* during or after the lock acquisition.)
*
+ * In OldestMemberMXactId array, there's a slot for all normal backends
+ * (0..MaxBackends-1) followed by a slot for max_prepared_xacts prepared
+ * transactions.
+ *
* OldestVisibleMXactId[k] is the oldest MultiXactId each backend's
* current transaction(s) think is potentially live, or InvalidMultiXactId
* when not in a transaction or not in a transaction that's paid any
@@ -187,6 +187,9 @@ typedef struct MultiXactStateData
* than its own OldestVisibleMXactId[] setting; this is necessary because
* the relevant SLRU data can be concurrently truncated away.
*
+ * In OldestVisibleMXactId array, there's a slot for all normal backends
+ * (0..MaxBackends-1) only. No slots for prepared transactions.
+ *
* The oldest valid value among all of the OldestMemberMXactId[] and
* OldestVisibleMXactId[] entries is considered by vacuum as the earliest
* possible value still having any live member transaction -- OldestMxact.
@@ -210,13 +213,54 @@ typedef struct MultiXactStateData
/*
* Size of OldestMemberMXactId and OldestVisibleMXactId arrays.
*/
-#define MaxOldestSlot TotalXactProcs
+#define MaxMemberSlot TotalXactProcs
+#define MaxVisibleSlot MaxBackends
+/*
+ * Total size of perBackendXactIds.
+ */
+#define MaxOldestSlot (MaxMemberSlot + MaxVisibleSlot)
/* Pointers to the state data in shared memory */
static MultiXactStateData *MultiXactState;
static MultiXactId *OldestMemberMXactId;
static MultiXactId *OldestVisibleMXactId;
+static inline MultiXactId
+GetOldestMemberMXactId(ProcNumber procno)
+{
+ Assert(procno >= 0 && procno < MaxBackends);
+ return OldestMemberMXactId[procno];
+}
+
+static inline void
+SetOldestMemberMXactId(ProcNumber procno, MultiXactId mxact)
+{
+ Assert(procno >= 0 && procno < MaxBackends);
+ OldestMemberMXactId[procno] = mxact;
+}
+
+static inline void
+SetOldestPreparedMXactId(ProcNumber procno, MultiXactId mxact)
+{
+ Assert(procno >= MaxChildren && procno < TotalProcs);
+ /* This check looks redundant, but it is future proof. */
+ Assert(procno - MaxChildren + MaxBackends < MaxMemberSlot);
+ OldestMemberMXactId[procno - MaxChildren + MaxBackends] = mxact;
+}
+
+static inline MultiXactId
+GetOldestVisibleMXactId(ProcNumber procno)
+{
+ Assert(procno >= 0 && procno < MaxVisibleSlot);
+ return OldestVisibleMXactId[procno];
+}
+
+static inline void
+SetOldestVisibleMXactId(ProcNumber procno, MultiXactId mxact)
+{
+ Assert(procno >= 0 && procno < MaxVisibleSlot);
+ OldestVisibleMXactId[procno] = mxact;
+}
/*
* Definitions for the backend-local MultiXactId cache.
@@ -308,7 +352,7 @@ MultiXactIdCreate(TransactionId xid1, MultiXactStatus status1,
Assert(!TransactionIdEquals(xid1, xid2) || (status1 != status2));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(MultiXactIdIsValid(GetOldestMemberMXactId(MyProcNumber)));
/*
* Note: unlike MultiXactIdExpand, we don't bother to check that both XIDs
@@ -362,7 +406,7 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status)
Assert(TransactionIdIsValid(xid));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(MultiXactIdIsValid(GetOldestMemberMXactId(MyProcNumber)));
debug_elog5(DEBUG2, "Expand: received multi %u, xid %u status %s",
multi, xid, mxstatus_to_string(status));
@@ -536,7 +580,7 @@ MultiXactIdIsRunning(MultiXactId multi, bool isLockOnly)
void
MultiXactIdSetOldestMember(void)
{
- if (!MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]))
+ if (!MultiXactIdIsValid(GetOldestMemberMXactId(MyProcNumber)))
{
MultiXactId nextMXact;
@@ -558,7 +602,7 @@ MultiXactIdSetOldestMember(void)
nextMXact = MultiXactState->nextMXact;
- OldestMemberMXactId[MyProcNumber] = nextMXact;
+ SetOldestMemberMXactId(MyProcNumber, nextMXact);
LWLockRelease(MultiXactGenLock);
@@ -586,7 +630,7 @@ MultiXactIdSetOldestMember(void)
static void
MultiXactIdSetOldestVisible(void)
{
- if (!MultiXactIdIsValid(OldestVisibleMXactId[MyProcNumber]))
+ if (!MultiXactIdIsValid(GetOldestVisibleMXactId(MyProcNumber)))
{
MultiXactId oldestMXact;
int i;
@@ -594,7 +638,7 @@ MultiXactIdSetOldestVisible(void)
LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
oldestMXact = MultiXactState->nextMXact;
- for (i = 0; i < MaxOldestSlot; i++)
+ for (i = 0; i < MaxMemberSlot; i++)
{
MultiXactId thisoldest = OldestMemberMXactId[i];
@@ -603,7 +647,7 @@ MultiXactIdSetOldestVisible(void)
oldestMXact = thisoldest;
}
- OldestVisibleMXactId[MyProcNumber] = oldestMXact;
+ SetOldestVisibleMXactId(MyProcNumber, oldestMXact);
LWLockRelease(MultiXactGenLock);
@@ -1152,7 +1196,7 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members,
* multi. It cannot possibly still be running.
*/
if (isLockOnly &&
- MultiXactIdPrecedes(multi, OldestVisibleMXactId[MyProcNumber]))
+ MultiXactIdPrecedes(multi, GetOldestVisibleMXactId(MyProcNumber)))
{
debug_elog2(DEBUG2, "GetMembers: a locker-only multi is too old");
*members = NULL;
@@ -1574,8 +1618,8 @@ AtEOXact_MultiXact(void)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ SetOldestMemberMXactId(MyProcNumber, InvalidMultiXactId);
+ SetOldestVisibleMXactId(MyProcNumber, InvalidMultiXactId);
/*
* Discard the local MultiXactId cache. Since MXactContext was created as
@@ -1595,7 +1639,7 @@ AtEOXact_MultiXact(void)
void
AtPrepare_MultiXact(void)
{
- MultiXactId myOldestMember = OldestMemberMXactId[MyProcNumber];
+ MultiXactId myOldestMember = GetOldestMemberMXactId(MyProcNumber);
if (MultiXactIdIsValid(myOldestMember))
RegisterTwoPhaseRecord(TWOPHASE_RM_MULTIXACT_ID, 0,
@@ -1615,7 +1659,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* Transfer our OldestMemberMXactId value to the slot reserved for the
* prepared transaction.
*/
- myOldestMember = OldestMemberMXactId[MyProcNumber];
+ myOldestMember = GetOldestMemberMXactId(MyProcNumber);
if (MultiXactIdIsValid(myOldestMember))
{
ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(fxid, false);
@@ -1628,8 +1672,8 @@ PostPrepare_MultiXact(FullTransactionId fxid)
*/
LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
- OldestMemberMXactId[dummyProcNumber] = myOldestMember;
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
+ SetOldestPreparedMXactId(dummyProcNumber, myOldestMember);
+ SetOldestMemberMXactId(MyProcNumber, InvalidMultiXactId);
LWLockRelease(MultiXactGenLock);
}
@@ -1642,7 +1686,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ SetOldestVisibleMXactId(MyProcNumber, InvalidMultiXactId);
/*
* Discard the local MultiXactId cache like in AtEOXact_MultiXact.
@@ -1669,7 +1713,7 @@ multixact_twophase_recover(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
oldestMember = *((MultiXactId *) recdata);
- OldestMemberMXactId[dummyProcNumber] = oldestMember;
+ SetOldestPreparedMXactId(dummyProcNumber, oldestMember);
}
/*
@@ -1684,7 +1728,7 @@ multixact_twophase_postcommit(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
- OldestMemberMXactId[dummyProcNumber] = InvalidMultiXactId;
+ SetOldestPreparedMXactId(dummyProcNumber, InvalidMultiXactId);
}
/*
@@ -1708,10 +1752,9 @@ MultiXactShmemSize(void)
{
Size size;
- /* We need 2*MaxOldestSlot perBackendXactIds[] entries */
#define SHARED_MULTIXACT_STATE_SIZE \
add_size(offsetof(MultiXactStateData, perBackendXactIds), \
- mul_size(sizeof(MultiXactId) * 2, MaxOldestSlot))
+ mul_size(sizeof(MultiXactId), MaxOldestSlot))
size = SHARED_MULTIXACT_STATE_SIZE;
size = add_size(size, SimpleLruShmemSize(multixact_offset_buffers, 0));
@@ -1763,7 +1806,7 @@ MultiXactShmemInit(void)
* Set up array pointers.
*/
OldestMemberMXactId = MultiXactState->perBackendXactIds;
- OldestVisibleMXactId = OldestMemberMXactId + MaxOldestSlot;
+ OldestVisibleMXactId = OldestMemberMXactId + MaxMemberSlot;
}
/*
@@ -2303,6 +2346,8 @@ MultiXactId
GetOldestMultiXactId(void)
{
MultiXactId oldestMXact;
+ MultiXactId thisoldest;
+ MultiXactId *allOldest;
int i;
/*
@@ -2311,15 +2356,10 @@ GetOldestMultiXactId(void)
*/
LWLockAcquire(MultiXactGenLock, LW_SHARED);
oldestMXact = MultiXactState->nextMXact;
+ allOldest = MultiXactState->perBackendXactIds;
for (i = 0; i < MaxOldestSlot; i++)
{
- MultiXactId thisoldest;
-
- thisoldest = OldestMemberMXactId[i];
- if (MultiXactIdIsValid(thisoldest) &&
- MultiXactIdPrecedes(thisoldest, oldestMXact))
- oldestMXact = thisoldest;
- thisoldest = OldestVisibleMXactId[i];
+ thisoldest = allOldest[i];
if (MultiXactIdIsValid(thisoldest) &&
MultiXactIdPrecedes(thisoldest, oldestMXact))
oldestMXact = thisoldest;
--
2.51.0
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-26 16:34 Sami Imseih <samimseih@gmail.com>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
1 sibling, 1 reply; 31+ messages in thread
From: Sami Imseih @ 2026-02-26 16:34 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Chao Li <li.evan.chao@gmail.com>
> But I still use inline procs for access to the arrays. Asserts cost nothing
> in release build. And new version of functions doesn't branch.
Asserts may have minimal impact for most builds, but my concern is
this is all unnecessary complexity. I mean if we guarantee that the
arrays are initialized correctly, maybe we can have an assert at
init time, why would we need asserts at run time?
--
Sami Imseih
Amazon Web Services (AWS)
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-26 17:02 Yura Sokolov <y.sokolov@postgrespro.ru>
parent: Sami Imseih <samimseih@gmail.com>
0 siblings, 1 reply; 31+ messages in thread
From: Yura Sokolov @ 2026-02-26 17:02 UTC (permalink / raw)
To: Sami Imseih <samimseih@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Chao Li <li.evan.chao@gmail.com>
26.02.2026 19:34, Sami Imseih пишет:
>> But I still use inline procs for access to the arrays. Asserts cost nothing
>> in release build. And new version of functions doesn't branch.
>
> Asserts may have minimal impact for most builds, but my concern is
> this is all unnecessary complexity. I mean if we guarantee that the
> arrays are initialized correctly, maybe we can have an assert at
> init time, why would we need asserts at run time?
They are future proof.
If there had been assertions from the beginning, there would not have been
a breaking change. The tests would have failed.
(Sorry, I've used Google Translate to write this sentence).
When you write assert, you protect yourself from shooting your leg far in
the future. Believe me.
--
regards
Yura Sokolov aka funny-falcon
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-26 20:44 Sami Imseih <samimseih@gmail.com>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
0 siblings, 1 reply; 31+ messages in thread
From: Sami Imseih @ 2026-02-26 20:44 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Chao Li <li.evan.chao@gmail.com>
> (Sorry, I've used Google Translate to write this sentence).
>
> When you write assert, you protect yourself from shooting your leg far in
> the future. Believe me.
The issue to me seems that we a few code paths that rely on the
total proc calculation in several places, and changing one and
forgetting to change another is what broke things. I am all for
future proofing this, and I think simply centralizing the calculation
should be enough without adding complexity to the code.
Maybe others have a different opinion.
--
Sami Imseih
Amazon Web Services (AWS)
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-27 05:00 Yura Sokolov <y.sokolov@postgrespro.ru>
parent: Sami Imseih <samimseih@gmail.com>
0 siblings, 1 reply; 31+ messages in thread
From: Yura Sokolov @ 2026-02-27 05:00 UTC (permalink / raw)
To: Sami Imseih <samimseih@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Chao Li <li.evan.chao@gmail.com>
26.02.2026 23:44, Sami Imseih пишет:
>> (Sorry, I've used Google Translate to write this sentence).
>>
>> When you write assert, you protect yourself from shooting your leg far in
>> the future. Believe me.
>
> The issue to me seems that we a few code paths that rely on the
> total proc calculation in several places, and changing one and
> forgetting to change another is what broke things.
If there were asserts, then tests would have failed.
Period.
There would no the bug. There would no this discussion.
If only array bounds were checked with asserts.
--
regards
Yura Sokolov aka funny-falcon
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-27 07:48 Chao Li <li.evan.chao@gmail.com>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
1 sibling, 1 reply; 31+ messages in thread
From: Chao Li @ 2026-02-27 07:48 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Sami Imseih <samimseih@gmail.com>
> On Feb 27, 2026, at 00:22, Yura Sokolov <y.sokolov@postgrespro.ru> wrote:
>
>
> Chao Li, new access functions doen't branch because I've made separate
> functions to store at slots for prepared transactions.
>
```
+ OldestMemberMXactId[procno - MaxChildren + MaxBackends] = mxact;
```
Wow, this is clever. It removes NUM_AUXILIARY_PROCS from procno without introducing an extra if check.
However, I am not a big fan of the new global variable names:
```
MaxChildren = MaxBackends + NUM_AUXILIARY_PROCS;
TotalProcs = MaxChildren + max_prepared_xacts;
TotalXactProcs = MaxBackends + max_prepared_xacts;
```
MaxChildren is actually the total number of backends plus auxiliary processes. Maybe something like MaxProcs would better reflect that?
TotalProcs is really the length of the PGPROC array in shared memory. Prepared transactions are not real processes; they just occupy some slots in the proc array. So perhaps a name like MaxPGProcSlots or MaxBackendAndPreparedSlots would be more accurate.
As for TotalXactProcs, maybe something like MaxBackendAndPreparedSlots would make the intent clearer.
Anyway, I am not very good at naming things. You or others may have better suggestions.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-27 08:00 Yura Sokolov <y.sokolov@postgrespro.ru>
parent: Chao Li <li.evan.chao@gmail.com>
0 siblings, 0 replies; 31+ messages in thread
From: Yura Sokolov @ 2026-02-27 08:00 UTC (permalink / raw)
To: Chao Li <li.evan.chao@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Sami Imseih <samimseih@gmail.com>
27.02.2026 10:48, Chao Li пишет:
> Anyway, I am not very good at naming things. You or others may have better suggestions.
I am bad at naming as well.
So I will not play this game.
Let committers do the way they like.
--
regards
Yura Sokolov aka funny-falcon
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-27 10:58 Heikki Linnakangas <hlinnaka@iki.fi>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
0 siblings, 2 replies; 31+ messages in thread
From: Heikki Linnakangas @ 2026-02-27 10:58 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; Sami Imseih <samimseih@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Andres Freund <andres@anarazel.de>; Chao Li <li.evan.chao@gmail.com>
On 27/02/2026 07:00, Yura Sokolov wrote:
> 26.02.2026 23:44, Sami Imseih пишет:
>>> (Sorry, I've used Google Translate to write this sentence).
>>>
>>> When you write assert, you protect yourself from shooting your leg far in
>>> the future. Believe me.
>>
>> The issue to me seems that we a few code paths that rely on the
>> total proc calculation in several places, and changing one and
>> forgetting to change another is what broke things.
>
> If there were asserts, then tests would have failed.
> Period.
>
> There would no the bug. There would no this discussion.
> If only array bounds were checked with asserts.
Yeah, more asserts == good, usually.
Here's my version of this. It's the same basic idea, some minor changes:
- Instead of the MaxChildren, TotalProcs, TotalXactProcs variables, I
added FIRST_PREPARED_XACT_PROC_NUMBER. Rationale: The variables still
required you to know the layout of the PGPROCs, i.e. you still had to
know that aux processes come after regular backends and dummy pgprocs
after aux processes. An FIRST_PREPARED_XACT_PROC_NUMBER is more explicit
in the places where it's needed.
I have been thinking of adding variables like that anyway, because
the current calculations with MaxBackends et al. are just so confusing.
So I'm vaguely in favor of doing something like that in the future. But
I think FIRST_PREPARED_XACT_PROC_NUMBER is more clear for this patch,
and those variable names as done here (MaxBackends, MaxChildren,
Totalprocs and TotalXactProcs) were still super confusing.
- I added a slightly different the set of Getter/Setter functions:
MyOldestMemberMXactIdSlot(),
PreparedXactOldestMemberMXactIdSlot(ProcNumber), and
MyOldestVisibleMXactIdSlot(). I think this is slightly less error-prone,
making it harder to pass proc number to wrong function (although the
assertions would catch such misuses pretty quick anyway). And these
functions return a pointer to the slot, so we don't need separate getter
and setter functions. That's a matter of taste, having a little less
code looks nicer to me.
What do you think?
I've been pondering what kind of issues this bug could cause. Because
the OldestVisibleMXactId array is allocated just after the
OldestMemberMXactId array, you don't get a buffer overflow, but a
prepared xact can overwrite a backend's value in the
OldestVisibleMXactId array. That can surely cause trouble later, but not
sure what exactly, and it seems pretty hard to write a repro script for it.
- Heikki
Attachments:
0001-Fix-multixacts-OldestMemberMXactId-and-OldestVisible.patch (text/x-patch, 14.1K) download
From 1bc2b558710aab2ee2c94c319b27587907cc3695 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Fri, 27 Feb 2026 12:30:24 +0200
Subject: [PATCH 1/1] Fix multixacts OldestMemberMXactId and
OldestVisibleMXactId usage
Due to [1], OldestMemberMXactId is no longer accessed by synthetic
dummyBackendId, but rather with pgprocno. Procs for prepared xacts are
placed after auxiliary procs, therefore calculation for MaxOldestSlot
became invalid.
On the other hand, OldestVisibleMXactId is used only for real backends,
and so never accessed at index greater than MaxBackends.
Lets separate size calculation for arrays and use converted index to
store oldest member mxact for prepared transactions.
[1] ab355e3a88de745 "Redefine backend ID to be an index into the proc array"
Author: Yura Sokolov <y.sokolov@postgrespro.ru>
Discussion: https://www.postgresql.org/message-id/7acc94b0-ea82-4657-b1b0-77842cb7a60c@postgrespro.ru
---
src/backend/access/transam/multixact.c | 106 +++++++++++++++++--------
src/backend/access/transam/twophase.c | 7 +-
src/backend/storage/lmgr/proc.c | 4 +-
src/include/storage/proc.h | 1 +
4 files changed, 79 insertions(+), 39 deletions(-)
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 90ec87d9dd6..bc45edb4c47 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -162,10 +162,6 @@ typedef struct MultiXactStateData
* immediately following the MultiXactStateData struct. Each is indexed by
* ProcNumber.
*
- * In both arrays, there's a slot for all normal backends
- * (0..MaxBackends-1) followed by a slot for max_prepared_xacts prepared
- * transactions.
- *
* OldestMemberMXactId[k] is the oldest MultiXactId each backend's current
* transaction(s) could possibly be a member of, or InvalidMultiXactId
* when the backend has no live transaction that could possibly be a
@@ -176,6 +172,10 @@ typedef struct MultiXactStateData
* member of a MultiXact, and that MultiXact would have to be created
* during or after the lock acquisition.)
*
+ * In the OldestMemberMXactId array, there's a slot for all normal
+ * backends (0..MaxBackends-1) followed by a slot for max_prepared_xacts
+ * prepared transactions.
+ *
* OldestVisibleMXactId[k] is the oldest MultiXactId each backend's
* current transaction(s) think is potentially live, or InvalidMultiXactId
* when not in a transaction or not in a transaction that's paid any
@@ -187,6 +187,9 @@ typedef struct MultiXactStateData
* than its own OldestVisibleMXactId[] setting; this is necessary because
* the relevant SLRU data can be concurrently truncated away.
*
+ * In the OldestVisibleMXactId array, there's a slot for all normal
+ * backends (0..MaxBackends-1) only. No slots for prepared transactions.
+ *
* The oldest valid value among all of the OldestMemberMXactId[] and
* OldestVisibleMXactId[] entries is considered by vacuum as the earliest
* possible value still having any live member transaction -- OldestMxact.
@@ -208,9 +211,10 @@ typedef struct MultiXactStateData
} MultiXactStateData;
/*
- * Size of OldestMemberMXactId and OldestVisibleMXactId arrays.
+ * Sizes of OldestMemberMXactId and OldestVisibleMXactId arrays.
*/
-#define MaxOldestSlot (MaxBackends + max_prepared_xacts)
+#define NumMemberSlots (MaxBackends + max_prepared_xacts)
+#define NumVisibleSlots MaxBackends
/* Pointers to the state data in shared memory */
static MultiXactStateData *MultiXactState;
@@ -218,6 +222,28 @@ static MultiXactId *OldestMemberMXactId;
static MultiXactId *OldestVisibleMXactId;
+static inline MultiXactId *
+MyOldestMemberMXactIdSlot(void)
+{
+ Assert(MyProcNumber >= 0 && MyProcNumber < MaxBackends);
+ return &OldestMemberMXactId[MyProcNumber];
+}
+
+static inline MultiXactId *
+PreparedXactOldestMemberMXactIdSlot(ProcNumber procno)
+{
+ Assert(procno >= FIRST_PREPARED_XACT_PROC_NUMBER);
+ Assert(procno - FIRST_PREPARED_XACT_PROC_NUMBER < NumMemberSlots);
+ return &OldestMemberMXactId[procno - FIRST_PREPARED_XACT_PROC_NUMBER];
+}
+
+static inline MultiXactId *
+MyOldestVisibleMXactIdSlot(void)
+{
+ Assert(MyProcNumber >= 0 && MyProcNumber < NumVisibleSlots);
+ return &OldestVisibleMXactId[MyProcNumber];
+}
+
/*
* Definitions for the backend-local MultiXactId cache.
*
@@ -308,7 +334,7 @@ MultiXactIdCreate(TransactionId xid1, MultiXactStatus status1,
Assert(!TransactionIdEquals(xid1, xid2) || (status1 != status2));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(MultiXactIdIsValid(*MyOldestMemberMXactIdSlot()));
/*
* Note: unlike MultiXactIdExpand, we don't bother to check that both XIDs
@@ -362,7 +388,7 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status)
Assert(TransactionIdIsValid(xid));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(MultiXactIdIsValid(*MyOldestMemberMXactIdSlot()));
debug_elog5(DEBUG2, "Expand: received multi %u, xid %u status %s",
multi, xid, mxstatus_to_string(status));
@@ -536,7 +562,7 @@ MultiXactIdIsRunning(MultiXactId multi, bool isLockOnly)
void
MultiXactIdSetOldestMember(void)
{
- if (!MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]))
+ if (!MultiXactIdIsValid(*MyOldestMemberMXactIdSlot()))
{
MultiXactId nextMXact;
@@ -558,7 +584,7 @@ MultiXactIdSetOldestMember(void)
nextMXact = MultiXactState->nextMXact;
- OldestMemberMXactId[MyProcNumber] = nextMXact;
+ *MyOldestMemberMXactIdSlot() = nextMXact;
LWLockRelease(MultiXactGenLock);
@@ -586,7 +612,7 @@ MultiXactIdSetOldestMember(void)
static void
MultiXactIdSetOldestVisible(void)
{
- if (!MultiXactIdIsValid(OldestVisibleMXactId[MyProcNumber]))
+ if (!MultiXactIdIsValid(*MyOldestVisibleMXactIdSlot()))
{
MultiXactId oldestMXact;
int i;
@@ -594,7 +620,7 @@ MultiXactIdSetOldestVisible(void)
LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
oldestMXact = MultiXactState->nextMXact;
- for (i = 0; i < MaxOldestSlot; i++)
+ for (i = 0; i < NumMemberSlots; i++)
{
MultiXactId thisoldest = OldestMemberMXactId[i];
@@ -603,7 +629,7 @@ MultiXactIdSetOldestVisible(void)
oldestMXact = thisoldest;
}
- OldestVisibleMXactId[MyProcNumber] = oldestMXact;
+ *MyOldestVisibleMXactIdSlot() = oldestMXact;
LWLockRelease(MultiXactGenLock);
@@ -1152,7 +1178,7 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members,
* multi. It cannot possibly still be running.
*/
if (isLockOnly &&
- MultiXactIdPrecedes(multi, OldestVisibleMXactId[MyProcNumber]))
+ MultiXactIdPrecedes(multi, *MyOldestVisibleMXactIdSlot()))
{
debug_elog2(DEBUG2, "GetMembers: a locker-only multi is too old");
*members = NULL;
@@ -1574,8 +1600,8 @@ AtEOXact_MultiXact(void)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ *MyOldestMemberMXactIdSlot() = InvalidMultiXactId;
+ *MyOldestVisibleMXactIdSlot() = InvalidMultiXactId;
/*
* Discard the local MultiXactId cache. Since MXactContext was created as
@@ -1595,7 +1621,7 @@ AtEOXact_MultiXact(void)
void
AtPrepare_MultiXact(void)
{
- MultiXactId myOldestMember = OldestMemberMXactId[MyProcNumber];
+ MultiXactId myOldestMember = *MyOldestMemberMXactIdSlot();
if (MultiXactIdIsValid(myOldestMember))
RegisterTwoPhaseRecord(TWOPHASE_RM_MULTIXACT_ID, 0,
@@ -1615,7 +1641,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* Transfer our OldestMemberMXactId value to the slot reserved for the
* prepared transaction.
*/
- myOldestMember = OldestMemberMXactId[MyProcNumber];
+ myOldestMember = *MyOldestMemberMXactIdSlot();
if (MultiXactIdIsValid(myOldestMember))
{
ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(fxid, false);
@@ -1628,8 +1654,8 @@ PostPrepare_MultiXact(FullTransactionId fxid)
*/
LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
- OldestMemberMXactId[dummyProcNumber] = myOldestMember;
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
+ *PreparedXactOldestMemberMXactIdSlot(dummyProcNumber) = myOldestMember;
+ *MyOldestMemberMXactIdSlot() = InvalidMultiXactId;
LWLockRelease(MultiXactGenLock);
}
@@ -1642,7 +1668,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ *MyOldestVisibleMXactIdSlot() = InvalidMultiXactId;
/*
* Discard the local MultiXactId cache like in AtEOXact_MultiXact.
@@ -1669,7 +1695,7 @@ multixact_twophase_recover(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
oldestMember = *((MultiXactId *) recdata);
- OldestMemberMXactId[dummyProcNumber] = oldestMember;
+ *PreparedXactOldestMemberMXactIdSlot(dummyProcNumber) = oldestMember;
}
/*
@@ -1684,7 +1710,7 @@ multixact_twophase_postcommit(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
- OldestMemberMXactId[dummyProcNumber] = InvalidMultiXactId;
+ *PreparedXactOldestMemberMXactIdSlot(dummyProcNumber) = InvalidMultiXactId;
}
/*
@@ -1703,17 +1729,25 @@ multixact_twophase_postabort(FullTransactionId fxid, uint16 info,
* thus double memory. Also, reserve space for the shared MultiXactState
* struct and the per-backend MultiXactId arrays (two of those, too).
*/
+static Size
+MultiXactSharedStateShmemSize()
+{
+ Size size;
+
+ size = offsetof(MultiXactStateData, perBackendXactIds);
+ size = add_size(size,
+ mul_size(sizeof(MultiXactId), NumMemberSlots));
+ size = add_size(size,
+ mul_size(sizeof(MultiXactId), NumVisibleSlots));
+ return size;
+}
+
Size
MultiXactShmemSize(void)
{
Size size;
- /* We need 2*MaxOldestSlot perBackendXactIds[] entries */
-#define SHARED_MULTIXACT_STATE_SIZE \
- add_size(offsetof(MultiXactStateData, perBackendXactIds), \
- mul_size(sizeof(MultiXactId) * 2, MaxOldestSlot))
-
- size = SHARED_MULTIXACT_STATE_SIZE;
+ size = MultiXactSharedStateShmemSize();
size = add_size(size, SimpleLruShmemSize(multixact_offset_buffers, 0));
size = add_size(size, SimpleLruShmemSize(multixact_member_buffers, 0));
@@ -1747,14 +1781,14 @@ MultiXactShmemInit(void)
/* Initialize our shared state struct */
MultiXactState = ShmemInitStruct("Shared MultiXact State",
- SHARED_MULTIXACT_STATE_SIZE,
+ MultiXactSharedStateShmemSize(),
&found);
if (!IsUnderPostmaster)
{
Assert(!found);
/* Make sure we zero out the per-backend state */
- MemSet(MultiXactState, 0, SHARED_MULTIXACT_STATE_SIZE);
+ MemSet(MultiXactState, 0, MultiXactSharedStateShmemSize());
}
else
Assert(found);
@@ -1763,7 +1797,7 @@ MultiXactShmemInit(void)
* Set up array pointers.
*/
OldestMemberMXactId = MultiXactState->perBackendXactIds;
- OldestVisibleMXactId = OldestMemberMXactId + MaxOldestSlot;
+ OldestVisibleMXactId = OldestMemberMXactId + NumMemberSlots;
}
/*
@@ -2303,7 +2337,6 @@ MultiXactId
GetOldestMultiXactId(void)
{
MultiXactId oldestMXact;
- int i;
/*
* This is the oldest valid value among all the OldestMemberMXactId[] and
@@ -2311,7 +2344,7 @@ GetOldestMultiXactId(void)
*/
LWLockAcquire(MultiXactGenLock, LW_SHARED);
oldestMXact = MultiXactState->nextMXact;
- for (i = 0; i < MaxOldestSlot; i++)
+ for (int i = 0; i < NumMemberSlots; i++)
{
MultiXactId thisoldest;
@@ -2319,6 +2352,11 @@ GetOldestMultiXactId(void)
if (MultiXactIdIsValid(thisoldest) &&
MultiXactIdPrecedes(thisoldest, oldestMXact))
oldestMXact = thisoldest;
+ }
+ for (int i = 0; i < NumVisibleSlots; i++)
+ {
+ MultiXactId thisoldest;
+
thisoldest = OldestVisibleMXactId[i];
if (MultiXactIdIsValid(thisoldest) &&
MultiXactIdPrecedes(thisoldest, oldestMXact))
diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index e4340b59640..5f65fa7b80f 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -900,9 +900,10 @@ TwoPhaseGetXidByVirtualXID(VirtualTransactionId vxid,
* Get the dummy proc number for prepared transaction
*
* Dummy proc numbers are similar to proc numbers of real backends. They
- * start at MaxBackends, and are unique across all currently active real
- * backends and prepared transactions. If lock_held is set to true,
- * TwoPhaseStateLock will not be taken, so the caller had better hold it.
+ * start at FIRST_PREPARED_XACT_PROC_NUMBER, and are unique across all
+ * currently active real backends and prepared transactions. If lock_held is
+ * set to true, TwoPhaseStateLock will not be taken, so the caller had better
+ * hold it.
*/
ProcNumber
TwoPhaseGetDummyProcNumber(FullTransactionId fxid, bool lock_held)
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 771b006b522..ccf9de0e67c 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -304,7 +304,7 @@ InitProcGlobal(void)
* dummy PGPROCs don't need these though - they're never associated
* with a real process
*/
- if (i < MaxBackends + NUM_AUXILIARY_PROCS)
+ if (i < FIRST_PREPARED_XACT_PROC_NUMBER)
{
proc->sem = PGSemaphoreCreate();
InitSharedLatch(&(proc->procLatch));
@@ -369,7 +369,7 @@ InitProcGlobal(void)
* processes and prepared transactions.
*/
AuxiliaryProcs = &procs[MaxBackends];
- PreparedXactProcs = &procs[MaxBackends + NUM_AUXILIARY_PROCS];
+ PreparedXactProcs = &procs[FIRST_PREPARED_XACT_PROC_NUMBER];
}
/*
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index a8d2e7db1a1..3f89450c216 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -532,6 +532,7 @@ extern PGDLLIMPORT PGPROC *PreparedXactProcs;
#define MAX_IO_WORKERS 32
#define NUM_AUXILIARY_PROCS (6 + MAX_IO_WORKERS)
+#define FIRST_PREPARED_XACT_PROC_NUMBER (MaxBackends + NUM_AUXILIARY_PROCS)
/* configurable options */
extern PGDLLIMPORT int DeadlockTimeout;
--
2.47.3
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-27 17:58 Yura Sokolov <y.sokolov@postgrespro.ru>
parent: Heikki Linnakangas <hlinnaka@iki.fi>
1 sibling, 1 reply; 31+ messages in thread
From: Yura Sokolov @ 2026-02-27 17:58 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; +Cc: Andres Freund <andres@anarazel.de>; Chao Li <li.evan.chao@gmail.com>; Sami Imseih <samimseih@gmail.com>
27.02.2026 13:58, Heikki Linnakangas пишет:
> On 27/02/2026 07:00, Yura Sokolov wrote:
>> 26.02.2026 23:44, Sami Imseih пишет:
>>>> (Sorry, I've used Google Translate to write this sentence).
>>>>
>>>> When you write assert, you protect yourself from shooting your leg far in
>>>> the future. Believe me.
>>>
>>> The issue to me seems that we a few code paths that rely on the
>>> total proc calculation in several places, and changing one and
>>> forgetting to change another is what broke things.
>>
>> If there were asserts, then tests would have failed.
>> Period.
>>
>> There would no the bug. There would no this discussion.
>> If only array bounds were checked with asserts.
>
> Yeah, more asserts == good, usually.
>
> Here's my version of this. It's the same basic idea, some minor changes:
>
> - Instead of the MaxChildren, TotalProcs, TotalXactProcs variables, I
> added FIRST_PREPARED_XACT_PROC_NUMBER. Rationale: The variables still
> required you to know the layout of the PGPROCs, i.e. you still had to
> know that aux processes come after regular backends and dummy pgprocs
> after aux processes. An FIRST_PREPARED_XACT_PROC_NUMBER is more explicit
> in the places where it's needed.
>
> I have been thinking of adding variables like that anyway, because
> the current calculations with MaxBackends et al. are just so confusing.
> So I'm vaguely in favor of doing something like that in the future. But
> I think FIRST_PREPARED_XACT_PROC_NUMBER is more clear for this patch,
> and those variable names as done here (MaxBackends, MaxChildren,
> Totalprocs and TotalXactProcs) were still super confusing.
>
> - I added a slightly different the set of Getter/Setter functions:
> MyOldestMemberMXactIdSlot(),
> PreparedXactOldestMemberMXactIdSlot(ProcNumber), and
> MyOldestVisibleMXactIdSlot(). I think this is slightly less error-prone,
> making it harder to pass proc number to wrong function (although the
> assertions would catch such misuses pretty quick anyway). And these
> functions return a pointer to the slot, so we don't need separate getter
> and setter functions. That's a matter of taste, having a little less
> code looks nicer to me.
>
> What do you think?
I stick to separate getters and setters because they are used in 64bit xid
patch.
With 32bit TransactionId PostgreSQL code in many places rely on 32bit
atomicity. But uint64 is not so atomic on currently-mostly-ancient
architecures. Therefore there is a need to use
pg_atomic_read_u64/pg_atomic_write_u64. And without hiding them in
getters/setters code become ugly.
So, getters+setters are better suited for (possible far) future.
But I don't mind if you stick with slot references for now.
(I realized now, to be prepared for uint64 getters have to be used in
GetOldestMultiXactId and MultiXactIdSetOldestVisible as well. My bad.)
> I've been pondering what kind of issues this bug could cause. Because
> the OldestVisibleMXactId array is allocated just after the
> OldestMemberMXactId array, you don't get a buffer overflow, but a
> prepared xact can overwrite a backend's value in the
> OldestVisibleMXactId array. That can surely cause trouble later, but not
> sure what exactly, and it seems pretty hard to write a repro script for it.
With extremely low configured MaxBackends it will overflow to
BufferDescriptors array. Certainly only test configurations may use such
tiny settings.
I found the bug because I increased NUM_AUXILIARY_PROCS by bunch of other
specialized processes, so overflow to BufferDescriptors happened even with
parameters already set in tests.
--
regards
Yura Sokolov aka funny-falcon
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-27 18:51 Sami Imseih <samimseih@gmail.com>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
0 siblings, 2 replies; 31+ messages in thread
From: Sami Imseih @ 2026-02-27 18:51 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Chao Li <li.evan.chao@gmail.com>
>> There would no the bug. There would no this discussion.
>> If only array bounds were checked with asserts.
> Yeah, more asserts == good, usually.
Maybe. My thoughts were that If the MaxOldestSlot is set correctly at
initialization time,
and we guarantee that by asserting its size against the total number
of procs, that should
be sufficient, and we don't need to create wrapper functions or add
assertions on every
access. But. I don't have a strong opinion either way.
Anyhow, it looks like the proposals now are aiming towards eliminating
the need to account
for auxiliary procs, and that is probably OK. It's a small bit of
memory saved, but slightly bit
more code.
> With extremely low configured MaxBackends it will overflow to
> BufferDescriptors array. Certainly only test configurations may use such
> tiny settings.
>
> I found the bug because I increased NUM_AUXILIARY_PROCS by bunch of other
> specialized processes, so overflow to BufferDescriptors happened even with
> parameters already set in tests.
It's unfortunately a bit worse. Here is a repro that shows 2 prepared
transactions
being created after a shared lock is taken on a table with 1 row. A subsequent
delete is able to complete, where we would expect it to be blocked until the
prepared transactions COMMIT or ROLLBACK.
With simply adding NUM_AUXILIARY_PROCS to MaxOldestSlot, it works as expected,
and the delete is blocked.
```
-#define MaxOldestSlot (MaxBackends + max_prepared_xacts)
+#define MaxOldestSlot (MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts)
```
I also tried with Heikki's proposal and the test succeeded. I did not
review the patch thoroughly
yet, but I think this test case should be added.
The test does require 2 prepared transactions to exercise
MultiXactIdSetOldestVisible(),
which then results in reading garbage values from the array and
determining incorrect
visibility of the row.
--
Sami Imseih
Amazon Web Services (AWS)
Attachments:
v1-0001-repro.patch (application/x-patch, 6.1K) download
From 0d56ffa57803b5059bd40589f659840f80d29adc Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 27 Feb 2026 10:35:41 -0600
Subject: [PATCH v1 1/1] repro
---
.../isolation/expected/multixact-aux-proc.out | 24 +++++++++++++++
.../isolation/expected/multixact-prep-tx.out | 30 +++++++++++++++++++
src/test/isolation/isolation_schedule | 1 +
.../isolation/specs/multixact-aux-proc.spec | 29 ++++++++++++++++++
.../isolation/specs/multixact-prep-tx.spec | 29 ++++++++++++++++++
5 files changed, 113 insertions(+)
create mode 100644 src/test/isolation/expected/multixact-aux-proc.out
create mode 100644 src/test/isolation/expected/multixact-prep-tx.out
create mode 100644 src/test/isolation/specs/multixact-aux-proc.spec
create mode 100644 src/test/isolation/specs/multixact-prep-tx.spec
diff --git a/src/test/isolation/expected/multixact-aux-proc.out b/src/test/isolation/expected/multixact-aux-proc.out
new file mode 100644
index 00000000000..08bba739bc9
--- /dev/null
+++ b/src/test/isolation/expected/multixact-aux-proc.out
@@ -0,0 +1,24 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin_p1 s1_share_p1 s1_prep_p1 s1_begin_p2 s1_share_p2 s1_prep_p2 s2_lock_timeout s2_delete s2_rollback_prepared_p1 s2_rollback_prepared_p2
+step s1_begin_p1: BEGIN;
+step s1_share_p1: SELECT * FROM dont_delete WHERE id = 1 FOR SHARE;
+id|data
+--+---------
+ 1|test data
+(1 row)
+
+step s1_prep_p1: PREPARE TRANSACTION 'p1';
+step s1_begin_p2: BEGIN;
+step s1_share_p2: SELECT * FROM dont_delete WHERE id = 1 FOR SHARE;
+id|data
+--+---------
+ 1|test data
+(1 row)
+
+step s1_prep_p2: PREPARE TRANSACTION 'p2';
+step s2_lock_timeout: SET lock_timeout TO '2s';
+step s2_delete: DELETE FROM dont_delete WHERE id = 1;
+ERROR: canceling statement due to lock timeout
+step s2_rollback_prepared_p1: ROLLBACK PREPARED 'p1';
+step s2_rollback_prepared_p2: ROLLBACK PREPARED 'p2';
diff --git a/src/test/isolation/expected/multixact-prep-tx.out b/src/test/isolation/expected/multixact-prep-tx.out
new file mode 100644
index 00000000000..68470cf33d9
--- /dev/null
+++ b/src/test/isolation/expected/multixact-prep-tx.out
@@ -0,0 +1,30 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin_p1 s1_share_p1 s1_prep_p1 s1_begin_p2 s1_share_p2 s1_prep_p2 s2_lock_timeout s2_delete s2_select s2_rollback_prepared_p1 s2_rollback_prepared_p2
+step s1_begin_p1: BEGIN;
+step s1_share_p1: SELECT * FROM block_delete WHERE id = 1 FOR SHARE;
+id|data
+--+---------
+ 1|test data
+(1 row)
+
+step s1_prep_p1: PREPARE TRANSACTION 'p1';
+step s1_begin_p2: BEGIN;
+step s1_share_p2: SELECT * FROM block_delete WHERE id = 1 FOR SHARE;
+id|data
+--+---------
+ 1|test data
+(1 row)
+
+step s1_prep_p2: PREPARE TRANSACTION 'p2';
+step s2_lock_timeout: SET lock_timeout TO '2s';
+step s2_delete: DELETE FROM block_delete WHERE id = 1;
+ERROR: canceling statement due to lock timeout
+step s2_select: SELECT * FROM block_delete WHERE id = 1;
+id|data
+--+---------
+ 1|test data
+(1 row)
+
+step s2_rollback_prepared_p1: ROLLBACK PREPARED 'p1';
+step s2_rollback_prepared_p2: ROLLBACK PREPARED 'p2';
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4e466580cd4..ff7f93ea6ef 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -66,6 +66,7 @@ test: aborted-keyrevoke
test: multixact-no-deadlock
test: multixact-no-forget
test: multixact-stats
+test: multixact-prep-tx
test: lock-committed-update
test: lock-committed-keyupdate
test: update-locked-tuple
diff --git a/src/test/isolation/specs/multixact-aux-proc.spec b/src/test/isolation/specs/multixact-aux-proc.spec
new file mode 100644
index 00000000000..2cddf53d1d1
--- /dev/null
+++ b/src/test/isolation/specs/multixact-aux-proc.spec
@@ -0,0 +1,29 @@
+# Test that dummy processes for prepared transactions are properly accounted for in multixact tracking
+
+setup
+{
+ CREATE TABLE block_delete (id int PRIMARY KEY, data text);
+ INSERT INTO block_delete VALUES (1, 'test data');
+}
+
+teardown
+{
+ DROP TABLE block_delete;
+}
+
+session s1
+step s1_begin_p1 { BEGIN; }
+step s1_share_p1 { SELECT * FROM block_delete WHERE id = 1 FOR SHARE; }
+step s1_prep_p1 { PREPARE TRANSACTION 'p1'; }
+step s1_begin_p2 { BEGIN; }
+step s1_share_p2 { SELECT * FROM block_delete WHERE id = 1 FOR SHARE; }
+step s1_prep_p2 { PREPARE TRANSACTION 'p2'; }
+
+session s2
+step s2_lock_timeout { SET lock_timeout TO '2s'; }
+step s2_delete { DELETE FROM block_delete WHERE id = 1; }
+step s2_select { SELECT * FROM block_delete WHERE id = 1; }
+step s2_rollback_prepared_p1 { ROLLBACK PREPARED 'p1'; }
+step s2_rollback_prepared_p2 { ROLLBACK PREPARED 'p2'; }
+
+permutation s1_begin_p1 s1_share_p1 s1_prep_p1 s1_begin_p2 s1_share_p2 s1_prep_p2 s2_lock_timeout s2_delete s2_select s2_rollback_prepared_p1 s2_rollback_prepared_p2
diff --git a/src/test/isolation/specs/multixact-prep-tx.spec b/src/test/isolation/specs/multixact-prep-tx.spec
new file mode 100644
index 00000000000..2cddf53d1d1
--- /dev/null
+++ b/src/test/isolation/specs/multixact-prep-tx.spec
@@ -0,0 +1,29 @@
+# Test that dummy processes for prepared transactions are properly accounted for in multixact tracking
+
+setup
+{
+ CREATE TABLE block_delete (id int PRIMARY KEY, data text);
+ INSERT INTO block_delete VALUES (1, 'test data');
+}
+
+teardown
+{
+ DROP TABLE block_delete;
+}
+
+session s1
+step s1_begin_p1 { BEGIN; }
+step s1_share_p1 { SELECT * FROM block_delete WHERE id = 1 FOR SHARE; }
+step s1_prep_p1 { PREPARE TRANSACTION 'p1'; }
+step s1_begin_p2 { BEGIN; }
+step s1_share_p2 { SELECT * FROM block_delete WHERE id = 1 FOR SHARE; }
+step s1_prep_p2 { PREPARE TRANSACTION 'p2'; }
+
+session s2
+step s2_lock_timeout { SET lock_timeout TO '2s'; }
+step s2_delete { DELETE FROM block_delete WHERE id = 1; }
+step s2_select { SELECT * FROM block_delete WHERE id = 1; }
+step s2_rollback_prepared_p1 { ROLLBACK PREPARED 'p1'; }
+step s2_rollback_prepared_p2 { ROLLBACK PREPARED 'p2'; }
+
+permutation s1_begin_p1 s1_share_p1 s1_prep_p1 s1_begin_p2 s1_share_p2 s1_prep_p2 s2_lock_timeout s2_delete s2_select s2_rollback_prepared_p1 s2_rollback_prepared_p2
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-27 19:11 Sami Imseih <samimseih@gmail.com>
parent: Sami Imseih <samimseih@gmail.com>
1 sibling, 0 replies; 31+ messages in thread
From: Sami Imseih @ 2026-02-27 19:11 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Chao Li <li.evan.chao@gmail.com>
> Here is a repro that shows
sorry, reattaching another version of the repro cleaned up
from remnants of an earlier renaming of the test.
--
Sami Imseih
Amazon Web Services (AWS)
Attachments:
v2-0001-repro.patch (application/octet-stream, 3.5K) download
From 3a9b4c9ed697a812ad3a8d0225a0c23e6aa3c05e Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 27 Feb 2026 10:35:41 -0600
Subject: [PATCH v2 1/1] repro
---
.../isolation/expected/multixact-prep-tx.out | 30 +++++++++++++++++++
src/test/isolation/isolation_schedule | 1 +
.../isolation/specs/multixact-prep-tx.spec | 29 ++++++++++++++++++
3 files changed, 60 insertions(+)
create mode 100644 src/test/isolation/expected/multixact-prep-tx.out
create mode 100644 src/test/isolation/specs/multixact-prep-tx.spec
diff --git a/src/test/isolation/expected/multixact-prep-tx.out b/src/test/isolation/expected/multixact-prep-tx.out
new file mode 100644
index 00000000000..68470cf33d9
--- /dev/null
+++ b/src/test/isolation/expected/multixact-prep-tx.out
@@ -0,0 +1,30 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin_p1 s1_share_p1 s1_prep_p1 s1_begin_p2 s1_share_p2 s1_prep_p2 s2_lock_timeout s2_delete s2_select s2_rollback_prepared_p1 s2_rollback_prepared_p2
+step s1_begin_p1: BEGIN;
+step s1_share_p1: SELECT * FROM block_delete WHERE id = 1 FOR SHARE;
+id|data
+--+---------
+ 1|test data
+(1 row)
+
+step s1_prep_p1: PREPARE TRANSACTION 'p1';
+step s1_begin_p2: BEGIN;
+step s1_share_p2: SELECT * FROM block_delete WHERE id = 1 FOR SHARE;
+id|data
+--+---------
+ 1|test data
+(1 row)
+
+step s1_prep_p2: PREPARE TRANSACTION 'p2';
+step s2_lock_timeout: SET lock_timeout TO '2s';
+step s2_delete: DELETE FROM block_delete WHERE id = 1;
+ERROR: canceling statement due to lock timeout
+step s2_select: SELECT * FROM block_delete WHERE id = 1;
+id|data
+--+---------
+ 1|test data
+(1 row)
+
+step s2_rollback_prepared_p1: ROLLBACK PREPARED 'p1';
+step s2_rollback_prepared_p2: ROLLBACK PREPARED 'p2';
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4e466580cd4..ff7f93ea6ef 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -66,6 +66,7 @@ test: aborted-keyrevoke
test: multixact-no-deadlock
test: multixact-no-forget
test: multixact-stats
+test: multixact-prep-tx
test: lock-committed-update
test: lock-committed-keyupdate
test: update-locked-tuple
diff --git a/src/test/isolation/specs/multixact-prep-tx.spec b/src/test/isolation/specs/multixact-prep-tx.spec
new file mode 100644
index 00000000000..2cddf53d1d1
--- /dev/null
+++ b/src/test/isolation/specs/multixact-prep-tx.spec
@@ -0,0 +1,29 @@
+# Test that dummy processes for prepared transactions are properly accounted for in multixact tracking
+
+setup
+{
+ CREATE TABLE block_delete (id int PRIMARY KEY, data text);
+ INSERT INTO block_delete VALUES (1, 'test data');
+}
+
+teardown
+{
+ DROP TABLE block_delete;
+}
+
+session s1
+step s1_begin_p1 { BEGIN; }
+step s1_share_p1 { SELECT * FROM block_delete WHERE id = 1 FOR SHARE; }
+step s1_prep_p1 { PREPARE TRANSACTION 'p1'; }
+step s1_begin_p2 { BEGIN; }
+step s1_share_p2 { SELECT * FROM block_delete WHERE id = 1 FOR SHARE; }
+step s1_prep_p2 { PREPARE TRANSACTION 'p2'; }
+
+session s2
+step s2_lock_timeout { SET lock_timeout TO '2s'; }
+step s2_delete { DELETE FROM block_delete WHERE id = 1; }
+step s2_select { SELECT * FROM block_delete WHERE id = 1; }
+step s2_rollback_prepared_p1 { ROLLBACK PREPARED 'p1'; }
+step s2_rollback_prepared_p2 { ROLLBACK PREPARED 'p2'; }
+
+permutation s1_begin_p1 s1_share_p1 s1_prep_p1 s1_begin_p2 s1_share_p2 s1_prep_p2 s2_lock_timeout s2_delete s2_select s2_rollback_prepared_p1 s2_rollback_prepared_p2
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-28 07:43 Yura Sokolov <y.sokolov@postgrespro.ru>
parent: Sami Imseih <samimseih@gmail.com>
1 sibling, 0 replies; 31+ messages in thread
From: Yura Sokolov @ 2026-02-28 07:43 UTC (permalink / raw)
To: Sami Imseih <samimseih@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Heikki Linnakangas <hlinnaka@iki.fi>; Andres Freund <andres@anarazel.de>; Chao Li <li.evan.chao@gmail.com>
27.02.2026 21:51, Sami Imseih пишет:
> It's unfortunately a bit worse. Here is a repro that shows 2 prepared
> transactions
> being created after a shared lock is taken on a table with 1 row. A subsequent
> delete is able to complete, where we would expect it to be blocked until the
> prepared transactions COMMIT or ROLLBACK.
OH MY GOD!!!!
I saw very similar behavior in dump of production WAL logs from our client.
Main issue were in other subsystem in that case, that is why we didn't dig
deeper.
But I clearly remembered: "Ouch, it is strange, why this row were deleted
being locked for foreign key?".
Great catch!!!!
My deep respect to you, Sami Imseih!!!!
> With simply adding NUM_AUXILIARY_PROCS to MaxOldestSlot, it works as expected,
> and the delete is blocked.
--
regards
Yura Sokolov aka funny-falcon
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-02-28 09:22 Heikki Linnakangas <hlinnaka@iki.fi>
parent: Heikki Linnakangas <hlinnaka@iki.fi>
1 sibling, 1 reply; 31+ messages in thread
From: Heikki Linnakangas @ 2026-02-28 09:22 UTC (permalink / raw)
To: Sami Imseih <samimseih@gmail.com>; +Cc: Yura Sokolov <y.sokolov@postgrespro.ru>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Chao Li <li.evan.chao@gmail.com>
(added back pgsql-hackers, I think you replied in private by accident)
On 28/02/2026 00:07, Sami Imseih wrote:
> 1/
>
> +MyOldestMemberMXactIdSlot(void)
> +{
> + Assert(MyProcNumber >= 0 && MyProcNumber < MaxBackends);
>
> It would be better to use NumVisibleSlots instead of MaxBackends in
> the second assert condition.
MaxBackends makes the assertion more strict. It verifies that we use one
of the slots reserved for regular backends, not prepared xacts. I'll add
a comment on that.
> 2/
>
> +static inline MultiXactId *
> +PreparedXactOldestMemberMXactIdSlot(ProcNumber procno)
> +{
> + Assert(procno >= FIRST_PREPARED_XACT_PROC_NUMBER);
> + Assert(procno - FIRST_PREPARED_XACT_PROC_NUMBER <
> + NumMemberSlots);
> + return &OldestMemberMXactId[procno -
> + FIRST_PREPARED_XACT_PROC_NUMBER];
> +}
>
> given
>
> +#define FIRST_PREPARED_XACT_PROC_NUMBER \
> + (MaxBackends + NUM_AUXILIARY_PROCS)
>
> let's say, MaxBackends = 100 and NUM_AUXILIARY_PROCS = 38, the
> first prepared transaction procno will be 138. The current math
> will return an index of 0 (138 - 138 = 0), but this corrupts
> backend slot 0.
Oof, you're right.
> Should we not account for NumVisibleSlots to skip
> over the regular backend slots?
>
> ```
> return &OldestMemberMXactId[NumVisibleSlots + (procno -
> FIRST_PREPARED_XACT_PROC_NUMBER)];
> ```
That's not quite right either. NumVisibleSlots has nothing to do with
the OldestMemberMXactId array, MaxBackends is the right offset here.
NumVisibleSlots == MaxBackends, but that's just a coincidence.
New version attached.
- Heikki
Attachments:
v2-0001-Fix-multixacts-OldestMemberMXactId-and-OldestVisi.patch (text/x-patch, 14.5K) download
From 47b97f62aecf3a6fe62cd512efbe66e7a69c69f8 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Sat, 28 Feb 2026 11:17:17 +0200
Subject: [PATCH v2 1/1] Fix multixacts OldestMemberMXactId and
OldestVisibleMXactId usage
Due to [1], OldestMemberMXactId is no longer accessed by synthetic
dummyBackendId, but rather with pgprocno. Procs for prepared xacts are
placed after auxiliary procs, therefore calculation for MaxOldestSlot
became invalid.
On the other hand, OldestVisibleMXactId is used only for real backends,
and so never accessed at index greater than MaxBackends.
Lets separate size calculation for arrays and use converted index to
store oldest member mxact for prepared transactions.
[1] ab355e3a88de745 "Redefine backend ID to be an index into the proc array"
Author: Yura Sokolov <y.sokolov@postgrespro.ru>
Reviewed-by: Sami Imseih <samimseih@gmail.com>
Discussion: https://www.postgresql.org/message-id/7acc94b0-ea82-4657-b1b0-77842cb7a60c@postgrespro.ru
---
src/backend/access/transam/multixact.c | 119 ++++++++++++++++++-------
src/backend/access/transam/twophase.c | 7 +-
src/backend/storage/lmgr/proc.c | 4 +-
src/include/storage/proc.h | 1 +
4 files changed, 92 insertions(+), 39 deletions(-)
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 90ec87d9dd6..5dd0ddf6f5a 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -162,10 +162,6 @@ typedef struct MultiXactStateData
* immediately following the MultiXactStateData struct. Each is indexed by
* ProcNumber.
*
- * In both arrays, there's a slot for all normal backends
- * (0..MaxBackends-1) followed by a slot for max_prepared_xacts prepared
- * transactions.
- *
* OldestMemberMXactId[k] is the oldest MultiXactId each backend's current
* transaction(s) could possibly be a member of, or InvalidMultiXactId
* when the backend has no live transaction that could possibly be a
@@ -176,6 +172,10 @@ typedef struct MultiXactStateData
* member of a MultiXact, and that MultiXact would have to be created
* during or after the lock acquisition.)
*
+ * In the OldestMemberMXactId array, there's a slot for all normal
+ * backends (0..MaxBackends-1) followed by a slot for max_prepared_xacts
+ * prepared transactions.
+ *
* OldestVisibleMXactId[k] is the oldest MultiXactId each backend's
* current transaction(s) think is potentially live, or InvalidMultiXactId
* when not in a transaction or not in a transaction that's paid any
@@ -187,6 +187,9 @@ typedef struct MultiXactStateData
* than its own OldestVisibleMXactId[] setting; this is necessary because
* the relevant SLRU data can be concurrently truncated away.
*
+ * In the OldestVisibleMXactId array, there's a slot for all normal
+ * backends (0..MaxBackends-1) only. No slots for prepared transactions.
+ *
* The oldest valid value among all of the OldestMemberMXactId[] and
* OldestVisibleMXactId[] entries is considered by vacuum as the earliest
* possible value still having any live member transaction -- OldestMxact.
@@ -208,9 +211,10 @@ typedef struct MultiXactStateData
} MultiXactStateData;
/*
- * Size of OldestMemberMXactId and OldestVisibleMXactId arrays.
+ * Sizes of OldestMemberMXactId and OldestVisibleMXactId arrays.
*/
-#define MaxOldestSlot (MaxBackends + max_prepared_xacts)
+#define NumMemberSlots (MaxBackends + max_prepared_xacts)
+#define NumVisibleSlots MaxBackends
/* Pointers to the state data in shared memory */
static MultiXactStateData *MultiXactState;
@@ -218,6 +222,41 @@ static MultiXactId *OldestMemberMXactId;
static MultiXactId *OldestVisibleMXactId;
+static inline MultiXactId *
+MyOldestMemberMXactIdSlot(void)
+{
+ /*
+ * The first MaxBackends entries in the OldestMemberMXactId array are
+ * reserved for regular backends. MyProcNumber should index into one of
+ * them.
+ */
+ Assert(MyProcNumber >= 0 && MyProcNumber < MaxBackends);
+ return &OldestMemberMXactId[MyProcNumber];
+}
+
+static inline MultiXactId *
+PreparedXactOldestMemberMXactIdSlot(ProcNumber procno)
+{
+ /*
+ * The entries with indexes >= MaxBackends in the OldestMemberMXactId
+ * array are reserved for regular backends.
+ */
+ int prepared_xact_idx;
+
+ Assert(procno >= FIRST_PREPARED_XACT_PROC_NUMBER);
+ prepared_xact_idx = procno - FIRST_PREPARED_XACT_PROC_NUMBER;
+
+ Assert(MaxBackends + prepared_xact_idx < NumMemberSlots);
+ return &OldestMemberMXactId[MaxBackends + prepared_xact_idx];
+}
+
+static inline MultiXactId *
+MyOldestVisibleMXactIdSlot(void)
+{
+ Assert(MyProcNumber >= 0 && MyProcNumber < NumVisibleSlots);
+ return &OldestVisibleMXactId[MyProcNumber];
+}
+
/*
* Definitions for the backend-local MultiXactId cache.
*
@@ -308,7 +347,7 @@ MultiXactIdCreate(TransactionId xid1, MultiXactStatus status1,
Assert(!TransactionIdEquals(xid1, xid2) || (status1 != status2));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(MultiXactIdIsValid(*MyOldestMemberMXactIdSlot()));
/*
* Note: unlike MultiXactIdExpand, we don't bother to check that both XIDs
@@ -362,7 +401,7 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status)
Assert(TransactionIdIsValid(xid));
/* MultiXactIdSetOldestMember() must have been called already. */
- Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+ Assert(MultiXactIdIsValid(*MyOldestMemberMXactIdSlot()));
debug_elog5(DEBUG2, "Expand: received multi %u, xid %u status %s",
multi, xid, mxstatus_to_string(status));
@@ -536,7 +575,7 @@ MultiXactIdIsRunning(MultiXactId multi, bool isLockOnly)
void
MultiXactIdSetOldestMember(void)
{
- if (!MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]))
+ if (!MultiXactIdIsValid(*MyOldestMemberMXactIdSlot()))
{
MultiXactId nextMXact;
@@ -558,7 +597,7 @@ MultiXactIdSetOldestMember(void)
nextMXact = MultiXactState->nextMXact;
- OldestMemberMXactId[MyProcNumber] = nextMXact;
+ *MyOldestMemberMXactIdSlot() = nextMXact;
LWLockRelease(MultiXactGenLock);
@@ -586,7 +625,7 @@ MultiXactIdSetOldestMember(void)
static void
MultiXactIdSetOldestVisible(void)
{
- if (!MultiXactIdIsValid(OldestVisibleMXactId[MyProcNumber]))
+ if (!MultiXactIdIsValid(*MyOldestVisibleMXactIdSlot()))
{
MultiXactId oldestMXact;
int i;
@@ -594,7 +633,7 @@ MultiXactIdSetOldestVisible(void)
LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
oldestMXact = MultiXactState->nextMXact;
- for (i = 0; i < MaxOldestSlot; i++)
+ for (i = 0; i < NumMemberSlots; i++)
{
MultiXactId thisoldest = OldestMemberMXactId[i];
@@ -603,7 +642,7 @@ MultiXactIdSetOldestVisible(void)
oldestMXact = thisoldest;
}
- OldestVisibleMXactId[MyProcNumber] = oldestMXact;
+ *MyOldestVisibleMXactIdSlot() = oldestMXact;
LWLockRelease(MultiXactGenLock);
@@ -1152,7 +1191,7 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members,
* multi. It cannot possibly still be running.
*/
if (isLockOnly &&
- MultiXactIdPrecedes(multi, OldestVisibleMXactId[MyProcNumber]))
+ MultiXactIdPrecedes(multi, *MyOldestVisibleMXactIdSlot()))
{
debug_elog2(DEBUG2, "GetMembers: a locker-only multi is too old");
*members = NULL;
@@ -1574,8 +1613,8 @@ AtEOXact_MultiXact(void)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ *MyOldestMemberMXactIdSlot() = InvalidMultiXactId;
+ *MyOldestVisibleMXactIdSlot() = InvalidMultiXactId;
/*
* Discard the local MultiXactId cache. Since MXactContext was created as
@@ -1595,7 +1634,7 @@ AtEOXact_MultiXact(void)
void
AtPrepare_MultiXact(void)
{
- MultiXactId myOldestMember = OldestMemberMXactId[MyProcNumber];
+ MultiXactId myOldestMember = *MyOldestMemberMXactIdSlot();
if (MultiXactIdIsValid(myOldestMember))
RegisterTwoPhaseRecord(TWOPHASE_RM_MULTIXACT_ID, 0,
@@ -1615,7 +1654,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* Transfer our OldestMemberMXactId value to the slot reserved for the
* prepared transaction.
*/
- myOldestMember = OldestMemberMXactId[MyProcNumber];
+ myOldestMember = *MyOldestMemberMXactIdSlot();
if (MultiXactIdIsValid(myOldestMember))
{
ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(fxid, false);
@@ -1628,8 +1667,8 @@ PostPrepare_MultiXact(FullTransactionId fxid)
*/
LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
- OldestMemberMXactId[dummyProcNumber] = myOldestMember;
- OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
+ *PreparedXactOldestMemberMXactIdSlot(dummyProcNumber) = myOldestMember;
+ *MyOldestMemberMXactIdSlot() = InvalidMultiXactId;
LWLockRelease(MultiXactGenLock);
}
@@ -1642,7 +1681,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
* We assume that storing a MultiXactId is atomic and so we need not take
* MultiXactGenLock to do this.
*/
- OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+ *MyOldestVisibleMXactIdSlot() = InvalidMultiXactId;
/*
* Discard the local MultiXactId cache like in AtEOXact_MultiXact.
@@ -1669,7 +1708,7 @@ multixact_twophase_recover(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
oldestMember = *((MultiXactId *) recdata);
- OldestMemberMXactId[dummyProcNumber] = oldestMember;
+ *PreparedXactOldestMemberMXactIdSlot(dummyProcNumber) = oldestMember;
}
/*
@@ -1684,7 +1723,7 @@ multixact_twophase_postcommit(FullTransactionId fxid, uint16 info,
Assert(len == sizeof(MultiXactId));
- OldestMemberMXactId[dummyProcNumber] = InvalidMultiXactId;
+ *PreparedXactOldestMemberMXactIdSlot(dummyProcNumber) = InvalidMultiXactId;
}
/*
@@ -1703,17 +1742,25 @@ multixact_twophase_postabort(FullTransactionId fxid, uint16 info,
* thus double memory. Also, reserve space for the shared MultiXactState
* struct and the per-backend MultiXactId arrays (two of those, too).
*/
+static Size
+MultiXactSharedStateShmemSize()
+{
+ Size size;
+
+ size = offsetof(MultiXactStateData, perBackendXactIds);
+ size = add_size(size,
+ mul_size(sizeof(MultiXactId), NumMemberSlots));
+ size = add_size(size,
+ mul_size(sizeof(MultiXactId), NumVisibleSlots));
+ return size;
+}
+
Size
MultiXactShmemSize(void)
{
Size size;
- /* We need 2*MaxOldestSlot perBackendXactIds[] entries */
-#define SHARED_MULTIXACT_STATE_SIZE \
- add_size(offsetof(MultiXactStateData, perBackendXactIds), \
- mul_size(sizeof(MultiXactId) * 2, MaxOldestSlot))
-
- size = SHARED_MULTIXACT_STATE_SIZE;
+ size = MultiXactSharedStateShmemSize();
size = add_size(size, SimpleLruShmemSize(multixact_offset_buffers, 0));
size = add_size(size, SimpleLruShmemSize(multixact_member_buffers, 0));
@@ -1747,14 +1794,14 @@ MultiXactShmemInit(void)
/* Initialize our shared state struct */
MultiXactState = ShmemInitStruct("Shared MultiXact State",
- SHARED_MULTIXACT_STATE_SIZE,
+ MultiXactSharedStateShmemSize(),
&found);
if (!IsUnderPostmaster)
{
Assert(!found);
/* Make sure we zero out the per-backend state */
- MemSet(MultiXactState, 0, SHARED_MULTIXACT_STATE_SIZE);
+ MemSet(MultiXactState, 0, MultiXactSharedStateShmemSize());
}
else
Assert(found);
@@ -1763,7 +1810,7 @@ MultiXactShmemInit(void)
* Set up array pointers.
*/
OldestMemberMXactId = MultiXactState->perBackendXactIds;
- OldestVisibleMXactId = OldestMemberMXactId + MaxOldestSlot;
+ OldestVisibleMXactId = OldestMemberMXactId + NumMemberSlots;
}
/*
@@ -2303,7 +2350,6 @@ MultiXactId
GetOldestMultiXactId(void)
{
MultiXactId oldestMXact;
- int i;
/*
* This is the oldest valid value among all the OldestMemberMXactId[] and
@@ -2311,7 +2357,7 @@ GetOldestMultiXactId(void)
*/
LWLockAcquire(MultiXactGenLock, LW_SHARED);
oldestMXact = MultiXactState->nextMXact;
- for (i = 0; i < MaxOldestSlot; i++)
+ for (int i = 0; i < NumMemberSlots; i++)
{
MultiXactId thisoldest;
@@ -2319,6 +2365,11 @@ GetOldestMultiXactId(void)
if (MultiXactIdIsValid(thisoldest) &&
MultiXactIdPrecedes(thisoldest, oldestMXact))
oldestMXact = thisoldest;
+ }
+ for (int i = 0; i < NumVisibleSlots; i++)
+ {
+ MultiXactId thisoldest;
+
thisoldest = OldestVisibleMXactId[i];
if (MultiXactIdIsValid(thisoldest) &&
MultiXactIdPrecedes(thisoldest, oldestMXact))
diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index e4340b59640..5f65fa7b80f 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -900,9 +900,10 @@ TwoPhaseGetXidByVirtualXID(VirtualTransactionId vxid,
* Get the dummy proc number for prepared transaction
*
* Dummy proc numbers are similar to proc numbers of real backends. They
- * start at MaxBackends, and are unique across all currently active real
- * backends and prepared transactions. If lock_held is set to true,
- * TwoPhaseStateLock will not be taken, so the caller had better hold it.
+ * start at FIRST_PREPARED_XACT_PROC_NUMBER, and are unique across all
+ * currently active real backends and prepared transactions. If lock_held is
+ * set to true, TwoPhaseStateLock will not be taken, so the caller had better
+ * hold it.
*/
ProcNumber
TwoPhaseGetDummyProcNumber(FullTransactionId fxid, bool lock_held)
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 771b006b522..ccf9de0e67c 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -304,7 +304,7 @@ InitProcGlobal(void)
* dummy PGPROCs don't need these though - they're never associated
* with a real process
*/
- if (i < MaxBackends + NUM_AUXILIARY_PROCS)
+ if (i < FIRST_PREPARED_XACT_PROC_NUMBER)
{
proc->sem = PGSemaphoreCreate();
InitSharedLatch(&(proc->procLatch));
@@ -369,7 +369,7 @@ InitProcGlobal(void)
* processes and prepared transactions.
*/
AuxiliaryProcs = &procs[MaxBackends];
- PreparedXactProcs = &procs[MaxBackends + NUM_AUXILIARY_PROCS];
+ PreparedXactProcs = &procs[FIRST_PREPARED_XACT_PROC_NUMBER];
}
/*
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index a8d2e7db1a1..3f89450c216 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -532,6 +532,7 @@ extern PGDLLIMPORT PGPROC *PreparedXactProcs;
#define MAX_IO_WORKERS 32
#define NUM_AUXILIARY_PROCS (6 + MAX_IO_WORKERS)
+#define FIRST_PREPARED_XACT_PROC_NUMBER (MaxBackends + NUM_AUXILIARY_PROCS)
/* configurable options */
extern PGDLLIMPORT int DeadlockTimeout;
--
2.47.3
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-01 14:20 Yura Sokolov <y.sokolov@postgrespro.ru>
parent: Heikki Linnakangas <hlinnaka@iki.fi>
0 siblings, 1 reply; 31+ messages in thread
From: Yura Sokolov @ 2026-03-01 14:20 UTC (permalink / raw)
To: Heikki Linnakangas <hlinnaka@iki.fi>; Sami Imseih <samimseih@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Chao Li <li.evan.chao@gmail.com>
28.02.2026 12:22, Heikki Linnakangas пишет:
> New version attached.
LGTM
--
regards
Yura Sokolov
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-02 02:34 Sami Imseih <samimseih@gmail.com>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
0 siblings, 1 reply; 31+ messages in thread
From: Sami Imseih @ 2026-03-02 02:34 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; +Cc: Heikki Linnakangas <hlinnaka@iki.fi>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Chao Li <li.evan.chao@gmail.com>
> New version attached.
The comment here:
+static inline MultiXactId *
+PreparedXactOldestMemberMXactIdSlot(ProcNumber procno)
+{
+ /*
+ * The entries with indexes >= MaxBackends in the OldestMemberMXactId
+ * array are reserved for regular backends.
should say "array are reserved for prepared transactions"
otherwise, LGTM.
--
Sami Imseih
Amazon Web Services (AWS)
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-02 04:08 Chao Li <li.evan.chao@gmail.com>
parent: Sami Imseih <samimseih@gmail.com>
0 siblings, 1 reply; 31+ messages in thread
From: Chao Li @ 2026-03-02 04:08 UTC (permalink / raw)
To: Sami Imseih <samimseih@gmail.com>; Heikki Linnakangas <hlinnaka@iki.fi>; +Cc: Yura Sokolov <y.sokolov@postgrespro.ru>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
> On Mar 2, 2026, at 10:34, Sami Imseih <samimseih@gmail.com> wrote:
>
>> New version attached.
>
> The comment here:
>
> +static inline MultiXactId *
> +PreparedXactOldestMemberMXactIdSlot(ProcNumber procno)
> +{
> + /*
> + * The entries with indexes >= MaxBackends in the OldestMemberMXactId
> + * array are reserved for regular backends.
>
> should say "array are reserved for prepared transactions"
>
+1
And I saw the other code comment in multixact.c around line 162 needs an update because of this patch:
```
* Per-backend data starts here. We have two arrays stored in the area
* immediately following the MultiXactStateData struct. Each is indexed by
* ProcNumber.
```
It says “Each is indexed by ProcNumber”, but it’s no longer accurate for OldestMemberMXactId prepared-xact entries, which now use index (procno - FIRST_PREPARED_XACT_PROC_NUMBER).
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-02 17:24 Heikki Linnakangas <hlinnaka@iki.fi>
parent: Chao Li <li.evan.chao@gmail.com>
0 siblings, 1 reply; 31+ messages in thread
From: Heikki Linnakangas @ 2026-03-02 17:24 UTC (permalink / raw)
To: Chao Li <li.evan.chao@gmail.com>; Sami Imseih <samimseih@gmail.com>; +Cc: Yura Sokolov <y.sokolov@postgrespro.ru>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On 02/03/2026 06:08, Chao Li wrote:
>> The comment here:
>>
>> +static inline MultiXactId *
>> +PreparedXactOldestMemberMXactIdSlot(ProcNumber procno)
>> +{
>> + /*
>> + * The entries with indexes >= MaxBackends in the OldestMemberMXactId
>> + * array are reserved for regular backends.
>>
>> should say "array are reserved for prepared transactions"
>>
>
> +1
>
> And I saw the other code comment in multixact.c around line 162 needs an update because of this patch:
> ```
> * Per-backend data starts here. We have two arrays stored in the area
> * immediately following the MultiXactStateData struct. Each is indexed by
> * ProcNumber.
> ```
>
> It says “Each is indexed by ProcNumber”, but it’s no longer accurate for OldestMemberMXactId prepared-xact entries, which now use index (procno - FIRST_PREPARED_XACT_PROC_NUMBER).
Fixed those and some other comment work, and pushed. Thanks!
- Heikki
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-02 19:09 Sami Imseih <samimseih@gmail.com>
parent: Heikki Linnakangas <hlinnaka@iki.fi>
0 siblings, 1 reply; 31+ messages in thread
From: Sami Imseih @ 2026-03-02 19:09 UTC (permalink / raw)
To: Heikki Linnakangas <hlinnaka@iki.fi>; +Cc: Chao Li <li.evan.chao@gmail.com>; Yura Sokolov <y.sokolov@postgrespro.ru>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
> > It says “Each is indexed by ProcNumber”, but it’s no longer accurate for OldestMemberMXactId prepared-xact entries, which now use index (procno - FIRST_PREPARED_XACT_PROC_NUMBER).
>
> Fixed those and some other comment work, and pushed. Thanks!
>
Thanks! what are your thoughts about adding a test like the one
here [1] ? This allows us to test correct handling of prepared
transaction dummy procs. The asserts added will not cover
this case.
What do you think?
[1] [https://www.postgresql.org/message-id/CAA5RZ0twq5bNMq0r0QNoopQnAEv%2BJ3qJNCrLs7HVqTEntBhJ%3Dg%40mail...]
--
Sami Imseih
Amazon Web Services (AWS)
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-03 12:02 Yura Sokolov <y.sokolov@postgrespro.ru>
parent: Sami Imseih <samimseih@gmail.com>
0 siblings, 2 replies; 31+ messages in thread
From: Yura Sokolov @ 2026-03-03 12:02 UTC (permalink / raw)
To: Sami Imseih <samimseih@gmail.com>; Heikki Linnakangas <hlinnaka@iki.fi>; +Cc: Chao Li <li.evan.chao@gmail.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
02.03.2026 22:09, Sami Imseih пишет:
>>> It says “Each is indexed by ProcNumber”, but it’s no longer accurate for OldestMemberMXactId prepared-xact entries, which now use index (procno - FIRST_PREPARED_XACT_PROC_NUMBER).
>>
>> Fixed those and some other comment work, and pushed. Thanks!
>>
>
> Thanks! what are your thoughts about adding a test like the one
> here [1] ? This allows us to test correct handling of prepared
> transaction dummy procs. The asserts added will not cover
> this case.
>
> What do you think?
>
> [1] [https://www.postgresql.org/message-id/CAA5RZ0twq5bNMq0r0QNoopQnAEv%2BJ3qJNCrLs7HVqTEntBhJ%3Dg%40mail...]
I support: test for bug fixed is a good thing.
--
regards
Yura Sokolov aka funny-falcon
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-03 15:11 Heikki Linnakangas <hlinnaka@iki.fi>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
1 sibling, 0 replies; 31+ messages in thread
From: Heikki Linnakangas @ 2026-03-03 15:11 UTC (permalink / raw)
To: Yura Sokolov <y.sokolov@postgrespro.ru>; Sami Imseih <samimseih@gmail.com>; +Cc: Chao Li <li.evan.chao@gmail.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On 03/03/2026 14:02, Yura Sokolov wrote:
> 02.03.2026 22:09, Sami Imseih пишет:
>>>> It says “Each is indexed by ProcNumber”, but it’s no longer accurate for OldestMemberMXactId prepared-xact entries, which now use index (procno - FIRST_PREPARED_XACT_PROC_NUMBER).
>>>
>>> Fixed those and some other comment work, and pushed. Thanks!
>>>
>>
>> Thanks! what are your thoughts about adding a test like the one
>> here [1] ? This allows us to test correct handling of prepared
>> transaction dummy procs. The asserts added will not cover
>> this case.
>>
>> What do you think?
>>
>> [1] [https://www.postgresql.org/message-id/CAA5RZ0twq5bNMq0r0QNoopQnAEv%2BJ3qJNCrLs7HVqTEntBhJ%3Dg%40mail...]
> I support: test for bug fixed is a good thing.
Ok, here's another version of Sami's repro. I realized that it doesn't
even need concurrent sessions, so I moved it to the main regression test
suite, into the 'prepared_xacts' test. Looks good?
- Heikki
Attachments:
0001-Add-test-for-row-locking-and-multixids-with-prepared.patch (text/x-patch, 5.5K) download
From 2e0f81e75d89eb3eb4dcc6411723cac3d07af25e Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Tue, 3 Mar 2026 17:03:23 +0200
Subject: [PATCH 1/1] Add test for row-locking and multixids with prepared
transactions
This is a repro for the issue fixed in commit ccae90abdb. Backpatch to
v17 like that commit, although that's a little arbitrary as this test
would work on older versions too.
Author: Sami Imseih <samimseih@gmail.com>
Discussion: XXX
Backpatch-through: 17
---
src/test/regress/expected/prepared_xacts.out | 30 +++++++++++++
.../regress/expected/prepared_xacts_1.out | 44 +++++++++++++++++++
src/test/regress/sql/prepared_xacts.sql | 25 +++++++++++
3 files changed, 99 insertions(+)
diff --git a/src/test/regress/expected/prepared_xacts.out b/src/test/regress/expected/prepared_xacts.out
index 515a2ada9d1..71d2e591622 100644
--- a/src/test/regress/expected/prepared_xacts.out
+++ b/src/test/regress/expected/prepared_xacts.out
@@ -263,8 +263,38 @@ SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
-----
(0 rows)
+-- Test row-level locks held by prepared transactions
+CREATE TABLE pxtest_rowlock (id int PRIMARY KEY, data text);
+INSERT INTO pxtest_rowlock VALUES (1, 'test data');
+BEGIN;
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE;
+ id | data
+----+-----------
+ 1 | test data
+(1 row)
+
+PREPARE TRANSACTION 'regress_p1';
+-- Should fail because the row is locked
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT;
+ERROR: could not obtain lock on row in relation "pxtest_rowlock"
+-- Test prepared transactions that participate in multixacts. For
+-- that, lock the same row again, creating a multixid.
+BEGIN;
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE;
+ id | data
+----+-----------
+ 1 | test data
+(1 row)
+
+PREPARE TRANSACTION 'regress_p2';
+-- Should fail because the row is locked
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT;
+ERROR: could not obtain lock on row in relation "pxtest_rowlock"
+ROLLBACK PREPARED 'regress_p1';
+ROLLBACK PREPARED 'regress_p2';
-- Clean up
DROP TABLE pxtest2;
DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled
ERROR: table "pxtest3" does not exist
DROP TABLE pxtest4;
+DROP TABLE pxtest_rowlock;
diff --git a/src/test/regress/expected/prepared_xacts_1.out b/src/test/regress/expected/prepared_xacts_1.out
index 6ad3d11898a..f70b4724fc5 100644
--- a/src/test/regress/expected/prepared_xacts_1.out
+++ b/src/test/regress/expected/prepared_xacts_1.out
@@ -258,9 +258,53 @@ SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
-----
(0 rows)
+-- Test row-level locks held by prepared transactions
+CREATE TABLE pxtest_rowlock (id int PRIMARY KEY, data text);
+INSERT INTO pxtest_rowlock VALUES (1, 'test data');
+BEGIN;
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE;
+ id | data
+----+-----------
+ 1 | test data
+(1 row)
+
+PREPARE TRANSACTION 'regress_p1';
+ERROR: prepared transactions are disabled
+HINT: Set "max_prepared_transactions" to a nonzero value.
+-- Should fail because the row is locked
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT;
+ id | data
+----+-----------
+ 1 | test data
+(1 row)
+
+-- Test prepared transactions that participate in multixacts. For
+-- that, lock the same row again, creating a multixid.
+BEGIN;
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE;
+ id | data
+----+-----------
+ 1 | test data
+(1 row)
+
+PREPARE TRANSACTION 'regress_p2';
+ERROR: prepared transactions are disabled
+HINT: Set "max_prepared_transactions" to a nonzero value.
+-- Should fail because the row is locked
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT;
+ id | data
+----+-----------
+ 1 | test data
+(1 row)
+
+ROLLBACK PREPARED 'regress_p1';
+ERROR: prepared transaction with identifier "regress_p1" does not exist
+ROLLBACK PREPARED 'regress_p2';
+ERROR: prepared transaction with identifier "regress_p2" does not exist
-- Clean up
DROP TABLE pxtest2;
ERROR: table "pxtest2" does not exist
DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled
DROP TABLE pxtest4;
ERROR: table "pxtest4" does not exist
+DROP TABLE pxtest_rowlock;
diff --git a/src/test/regress/sql/prepared_xacts.sql b/src/test/regress/sql/prepared_xacts.sql
index ade3a2672a8..5d2f06798c6 100644
--- a/src/test/regress/sql/prepared_xacts.sql
+++ b/src/test/regress/sql/prepared_xacts.sql
@@ -158,7 +158,32 @@ SELECT * FROM pxtest3;
-- There should be no prepared transactions
SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
+
+-- Test row-level locks held by prepared transactions
+CREATE TABLE pxtest_rowlock (id int PRIMARY KEY, data text);
+INSERT INTO pxtest_rowlock VALUES (1, 'test data');
+
+BEGIN;
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE;
+PREPARE TRANSACTION 'regress_p1';
+
+-- Should fail because the row is locked
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT;
+
+-- Test prepared transactions that participate in multixacts. For
+-- that, lock the same row again, creating a multixid.
+BEGIN;
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE;
+PREPARE TRANSACTION 'regress_p2';
+
+-- Should fail because the row is locked
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT;
+
+ROLLBACK PREPARED 'regress_p1';
+ROLLBACK PREPARED 'regress_p2';
+
-- Clean up
DROP TABLE pxtest2;
DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled
DROP TABLE pxtest4;
+DROP TABLE pxtest_rowlock;
--
2.47.3
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-03 15:22 Tom Lane <tgl@sss.pgh.pa.us>
parent: Yura Sokolov <y.sokolov@postgrespro.ru>
1 sibling, 2 replies; 31+ messages in thread
From: Tom Lane @ 2026-03-03 15:22 UTC (permalink / raw)
To: Heikki Linnakangas <hlinnaka@iki.fi>; +Cc: Yura Sokolov <y.sokolov@postgrespro.ru>; Sami Imseih <samimseih@gmail.com>; Chao Li <li.evan.chao@gmail.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
Heikki Linnakangas <hlinnaka@iki.fi> writes:
> Ok, here's another version of Sami's repro. I realized that it doesn't
> even need concurrent sessions, so I moved it to the main regression test
> suite, into the 'prepared_xacts' test. Looks good?
Not directly relevant to the thing being tested here, but ...
I wonder if we couldn't reduce future maintenance effort for
prepared_xacts_1.out by modifying that test script to make an
early exit if prepared transactions are disabled.
regards, tom lane
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-03 16:59 Heikki Linnakangas <hlinnaka@iki.fi>
parent: Tom Lane <tgl@sss.pgh.pa.us>
1 sibling, 1 reply; 31+ messages in thread
From: Heikki Linnakangas @ 2026-03-03 16:59 UTC (permalink / raw)
To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Yura Sokolov <y.sokolov@postgrespro.ru>; Sami Imseih <samimseih@gmail.com>; Chao Li <li.evan.chao@gmail.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On 03/03/2026 17:22, Tom Lane wrote:
> Heikki Linnakangas <hlinnaka@iki.fi> writes:
>> Ok, here's another version of Sami's repro. I realized that it doesn't
>> even need concurrent sessions, so I moved it to the main regression test
>> suite, into the 'prepared_xacts' test. Looks good?
>
> Not directly relevant to the thing being tested here, but ...
> I wonder if we couldn't reduce future maintenance effort for
> prepared_xacts_1.out by modifying that test script to make an
> early exit if prepared transactions are disabled.
+1, I was just thinking the same.
Here's a patch to do that. I'll backpatch this to all supported
versions, to make backpatching test changes less painful in the future.
- Heikki
Attachments: download all (2 files, .tar.gz)
v2-0001-Skip-prepared_xacts-test-if-max_prepared_transact.patch (text/x-patch, 9.4K) download
From 7f08ba419a3183f3f207caffd36602f0a9bd885a Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Tue, 3 Mar 2026 18:51:38 +0200
Subject: [PATCH v2 1/2] Skip prepared_xacts test if max_prepared_transactions
< 2
This reduces maintenance overhead, as we no longer need to update the
dummy expected output file every time the .sql file changes.
Discussion: https://www.postgresql.org/message-id/1009073.1772551323@sss.pgh.pa.us
Backpatch-through: 14
---
src/test/regress/expected/prepared_xacts.out | 7 +-
.../regress/expected/prepared_xacts_1.out | 269 +-----------------
src/test/regress/sql/prepared_xacts.sql | 7 +-
3 files changed, 14 insertions(+), 269 deletions(-)
diff --git a/src/test/regress/expected/prepared_xacts.out b/src/test/regress/expected/prepared_xacts.out
index 515a2ada9d1..6ed81e43443 100644
--- a/src/test/regress/expected/prepared_xacts.out
+++ b/src/test/regress/expected/prepared_xacts.out
@@ -1,3 +1,7 @@
+SELECT current_setting('max_prepared_transactions')::integer < 2 AS skip_test \gset
+\if :skip_test
+\quit
+\endif
--
-- PREPARED TRANSACTIONS (two-phase commit)
--
@@ -265,6 +269,5 @@ SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
-- Clean up
DROP TABLE pxtest2;
-DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled
-ERROR: table "pxtest3" does not exist
+-- pxtest3 was already dropped
DROP TABLE pxtest4;
diff --git a/src/test/regress/expected/prepared_xacts_1.out b/src/test/regress/expected/prepared_xacts_1.out
index 6ad3d11898a..a21314768c3 100644
--- a/src/test/regress/expected/prepared_xacts_1.out
+++ b/src/test/regress/expected/prepared_xacts_1.out
@@ -1,266 +1,3 @@
---
--- PREPARED TRANSACTIONS (two-phase commit)
---
--- We can't readily test persistence of prepared xacts within the
--- regression script framework, unfortunately. Note that a crash
--- isn't really needed ... stopping and starting the postmaster would
--- be enough, but we can't even do that here.
--- create a simple table that we'll use in the tests
-CREATE TABLE pxtest1 (foobar VARCHAR(10));
-INSERT INTO pxtest1 VALUES ('aaa');
--- Test PREPARE TRANSACTION
-BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-UPDATE pxtest1 SET foobar = 'bbb' WHERE foobar = 'aaa';
-SELECT * FROM pxtest1;
- foobar
---------
- bbb
-(1 row)
-
-PREPARE TRANSACTION 'regress_foo1';
-ERROR: prepared transactions are disabled
-HINT: Set "max_prepared_transactions" to a nonzero value.
-SELECT * FROM pxtest1;
- foobar
---------
- aaa
-(1 row)
-
--- Test pg_prepared_xacts system view
-SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
- gid
------
-(0 rows)
-
--- Test ROLLBACK PREPARED
-ROLLBACK PREPARED 'regress_foo1';
-ERROR: prepared transaction with identifier "regress_foo1" does not exist
-SELECT * FROM pxtest1;
- foobar
---------
- aaa
-(1 row)
-
-SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
- gid
------
-(0 rows)
-
--- Test COMMIT PREPARED
-BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-INSERT INTO pxtest1 VALUES ('ddd');
-SELECT * FROM pxtest1;
- foobar
---------
- aaa
- ddd
-(2 rows)
-
-PREPARE TRANSACTION 'regress_foo2';
-ERROR: prepared transactions are disabled
-HINT: Set "max_prepared_transactions" to a nonzero value.
-SELECT * FROM pxtest1;
- foobar
---------
- aaa
-(1 row)
-
-COMMIT PREPARED 'regress_foo2';
-ERROR: prepared transaction with identifier "regress_foo2" does not exist
-SELECT * FROM pxtest1;
- foobar
---------
- aaa
-(1 row)
-
--- Test duplicate gids
-BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-UPDATE pxtest1 SET foobar = 'eee' WHERE foobar = 'ddd';
-SELECT * FROM pxtest1;
- foobar
---------
- aaa
-(1 row)
-
-PREPARE TRANSACTION 'regress_foo3';
-ERROR: prepared transactions are disabled
-HINT: Set "max_prepared_transactions" to a nonzero value.
-SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
- gid
------
-(0 rows)
-
-BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-INSERT INTO pxtest1 VALUES ('fff');
--- This should fail, because the gid foo3 is already in use
-PREPARE TRANSACTION 'regress_foo3';
-ERROR: prepared transactions are disabled
-HINT: Set "max_prepared_transactions" to a nonzero value.
-SELECT * FROM pxtest1;
- foobar
---------
- aaa
-(1 row)
-
-ROLLBACK PREPARED 'regress_foo3';
-ERROR: prepared transaction with identifier "regress_foo3" does not exist
-SELECT * FROM pxtest1;
- foobar
---------
- aaa
-(1 row)
-
--- Test serialization failure (SSI)
-BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-UPDATE pxtest1 SET foobar = 'eee' WHERE foobar = 'ddd';
-SELECT * FROM pxtest1;
- foobar
---------
- aaa
-(1 row)
-
-PREPARE TRANSACTION 'regress_foo4';
-ERROR: prepared transactions are disabled
-HINT: Set "max_prepared_transactions" to a nonzero value.
-SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
- gid
------
-(0 rows)
-
-BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-SELECT * FROM pxtest1;
- foobar
---------
- aaa
-(1 row)
-
--- This should fail, because the two transactions have a write-skew anomaly
-INSERT INTO pxtest1 VALUES ('fff');
-PREPARE TRANSACTION 'regress_foo5';
-ERROR: prepared transactions are disabled
-HINT: Set "max_prepared_transactions" to a nonzero value.
-SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
- gid
------
-(0 rows)
-
-ROLLBACK PREPARED 'regress_foo4';
-ERROR: prepared transaction with identifier "regress_foo4" does not exist
-SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
- gid
------
-(0 rows)
-
--- Clean up
-DROP TABLE pxtest1;
--- Test detection of session-level and xact-level locks on same object
-BEGIN;
-SELECT pg_advisory_lock(1);
- pg_advisory_lock
-------------------
-
-(1 row)
-
-SELECT pg_advisory_xact_lock_shared(1);
- pg_advisory_xact_lock_shared
-------------------------------
-
-(1 row)
-
-PREPARE TRANSACTION 'regress_foo6'; -- fails
-ERROR: prepared transactions are disabled
-HINT: Set "max_prepared_transactions" to a nonzero value.
--- Test subtransactions
-BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
- CREATE TABLE pxtest2 (a int);
- INSERT INTO pxtest2 VALUES (1);
- SAVEPOINT a;
- INSERT INTO pxtest2 VALUES (2);
- ROLLBACK TO a;
- SAVEPOINT b;
- INSERT INTO pxtest2 VALUES (3);
-PREPARE TRANSACTION 'regress_sub1';
-ERROR: prepared transactions are disabled
-HINT: Set "max_prepared_transactions" to a nonzero value.
-CREATE TABLE pxtest3(fff int);
--- Test shared invalidation
-BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
- DROP TABLE pxtest3;
- CREATE TABLE pxtest4 (a int);
- INSERT INTO pxtest4 VALUES (1);
- INSERT INTO pxtest4 VALUES (2);
- DECLARE foo CURSOR FOR SELECT * FROM pxtest4;
- -- Fetch 1 tuple, keeping the cursor open
- FETCH 1 FROM foo;
- a
----
- 1
-(1 row)
-
-PREPARE TRANSACTION 'regress_sub2';
-ERROR: prepared transactions are disabled
-HINT: Set "max_prepared_transactions" to a nonzero value.
--- No such cursor
-FETCH 1 FROM foo;
-ERROR: cursor "foo" does not exist
--- Table doesn't exist, the creation hasn't been committed yet
-SELECT * FROM pxtest2;
-ERROR: relation "pxtest2" does not exist
-LINE 1: SELECT * FROM pxtest2;
- ^
--- There should be two prepared transactions
-SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
- gid
------
-(0 rows)
-
--- pxtest3 should be locked because of the pending DROP
-begin;
-lock table pxtest3 in access share mode nowait;
-rollback;
--- Disconnect, we will continue testing in a different backend
-\c -
--- There should still be two prepared transactions
-SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
- gid
------
-(0 rows)
-
--- pxtest3 should still be locked because of the pending DROP
-begin;
-lock table pxtest3 in access share mode nowait;
-rollback;
--- Commit table creation
-COMMIT PREPARED 'regress_sub1';
-ERROR: prepared transaction with identifier "regress_sub1" does not exist
-\d pxtest2
-SELECT * FROM pxtest2;
-ERROR: relation "pxtest2" does not exist
-LINE 1: SELECT * FROM pxtest2;
- ^
--- There should be one prepared transaction
-SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
- gid
------
-(0 rows)
-
--- Commit table drop
-COMMIT PREPARED 'regress_sub2';
-ERROR: prepared transaction with identifier "regress_sub2" does not exist
-SELECT * FROM pxtest3;
- fff
------
-(0 rows)
-
--- There should be no prepared transactions
-SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
- gid
------
-(0 rows)
-
--- Clean up
-DROP TABLE pxtest2;
-ERROR: table "pxtest2" does not exist
-DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled
-DROP TABLE pxtest4;
-ERROR: table "pxtest4" does not exist
+SELECT current_setting('max_prepared_transactions')::integer < 2 AS skip_test \gset
+\if :skip_test
+\quit
diff --git a/src/test/regress/sql/prepared_xacts.sql b/src/test/regress/sql/prepared_xacts.sql
index ade3a2672a8..bac8455e861 100644
--- a/src/test/regress/sql/prepared_xacts.sql
+++ b/src/test/regress/sql/prepared_xacts.sql
@@ -1,3 +1,8 @@
+SELECT current_setting('max_prepared_transactions')::integer < 2 AS skip_test \gset
+\if :skip_test
+\quit
+\endif
+
--
-- PREPARED TRANSACTIONS (two-phase commit)
--
@@ -160,5 +165,5 @@ SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
-- Clean up
DROP TABLE pxtest2;
-DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled
+-- pxtest3 was already dropped
DROP TABLE pxtest4;
--
2.47.3
v2-0002-Add-test-for-row-locking-and-multixids-with-prepa.patch (text/x-patch, 3.5K) download
From bbc15a0dc7126515b7d165011659bf4e07803ed6 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Tue, 3 Mar 2026 17:03:23 +0200
Subject: [PATCH v2 2/2] Add test for row-locking and multixids with prepared
transactions
This is a repro for the issue fixed in commit ccae90abdb. Backpatch to
v17 like that commit, although that's a little arbitrary as this test
would work on older versions too.
Author: Sami Imseih <samimseih@gmail.com>
Discussion: https://www.postgresql.org/message-id/CAA5RZ0twq5bNMq0r0QNoopQnAEv+J3qJNCrLs7HVqTEntBhJ=g@mail.gmail.com
Backpatch-through: 17
---
src/test/regress/expected/prepared_xacts.out | 30 ++++++++++++++++++++
src/test/regress/sql/prepared_xacts.sql | 25 ++++++++++++++++
2 files changed, 55 insertions(+)
diff --git a/src/test/regress/expected/prepared_xacts.out b/src/test/regress/expected/prepared_xacts.out
index 6ed81e43443..ac4ebf6ca36 100644
--- a/src/test/regress/expected/prepared_xacts.out
+++ b/src/test/regress/expected/prepared_xacts.out
@@ -267,7 +267,37 @@ SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
-----
(0 rows)
+-- Test row-level locks held by prepared transactions
+CREATE TABLE pxtest_rowlock (id int PRIMARY KEY, data text);
+INSERT INTO pxtest_rowlock VALUES (1, 'test data');
+BEGIN;
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE;
+ id | data
+----+-----------
+ 1 | test data
+(1 row)
+
+PREPARE TRANSACTION 'regress_p1';
+-- Should fail because the row is locked
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT;
+ERROR: could not obtain lock on row in relation "pxtest_rowlock"
+-- Test prepared transactions that participate in multixacts. For
+-- that, lock the same row again, creating a multixid.
+BEGIN;
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE;
+ id | data
+----+-----------
+ 1 | test data
+(1 row)
+
+PREPARE TRANSACTION 'regress_p2';
+-- Should fail because the row is locked
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT;
+ERROR: could not obtain lock on row in relation "pxtest_rowlock"
+ROLLBACK PREPARED 'regress_p1';
+ROLLBACK PREPARED 'regress_p2';
-- Clean up
DROP TABLE pxtest2;
-- pxtest3 was already dropped
DROP TABLE pxtest4;
+DROP TABLE pxtest_rowlock;
diff --git a/src/test/regress/sql/prepared_xacts.sql b/src/test/regress/sql/prepared_xacts.sql
index bac8455e861..b0712b153e0 100644
--- a/src/test/regress/sql/prepared_xacts.sql
+++ b/src/test/regress/sql/prepared_xacts.sql
@@ -163,7 +163,32 @@ SELECT * FROM pxtest3;
-- There should be no prepared transactions
SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid;
+
+-- Test row-level locks held by prepared transactions
+CREATE TABLE pxtest_rowlock (id int PRIMARY KEY, data text);
+INSERT INTO pxtest_rowlock VALUES (1, 'test data');
+
+BEGIN;
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE;
+PREPARE TRANSACTION 'regress_p1';
+
+-- Should fail because the row is locked
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT;
+
+-- Test prepared transactions that participate in multixacts. For
+-- that, lock the same row again, creating a multixid.
+BEGIN;
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE;
+PREPARE TRANSACTION 'regress_p2';
+
+-- Should fail because the row is locked
+SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT;
+
+ROLLBACK PREPARED 'regress_p1';
+ROLLBACK PREPARED 'regress_p2';
+
-- Clean up
DROP TABLE pxtest2;
-- pxtest3 was already dropped
DROP TABLE pxtest4;
+DROP TABLE pxtest_rowlock;
--
2.47.3
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-03 17:08 Sami Imseih <samimseih@gmail.com>
parent: Heikki Linnakangas <hlinnaka@iki.fi>
0 siblings, 0 replies; 31+ messages in thread
From: Sami Imseih @ 2026-03-03 17:08 UTC (permalink / raw)
To: Heikki Linnakangas <hlinnaka@iki.fi>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; Yura Sokolov <y.sokolov@postgrespro.ru>; Chao Li <li.evan.chao@gmail.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
> > Heikki Linnakangas <hlinnaka@iki.fi> writes:
> >> Ok, here's another version of Sami's repro. I realized that it doesn't
> >> even need concurrent sessions, so I moved it to the main regression test
> >> suite, into the 'prepared_xacts' test. Looks good?
You are correct. concurrent sessions not needed. v2 LGTM.
Thanks!
--
Sami Imseih
Amazon Web Services (AWS)
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-03 18:47 Tom Lane <tgl@sss.pgh.pa.us>
parent: Tom Lane <tgl@sss.pgh.pa.us>
1 sibling, 1 reply; 31+ messages in thread
From: Tom Lane @ 2026-03-03 18:47 UTC (permalink / raw)
To: Heikki Linnakangas <hlinnaka@iki.fi>; +Cc: Yura Sokolov <y.sokolov@postgrespro.ru>; Sami Imseih <samimseih@gmail.com>; Chao Li <li.evan.chao@gmail.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
Heikki Linnakangas <hlinnaka@iki.fi> writes:
> On 03/03/2026 17:22, Tom Lane wrote:
>> I wonder if we couldn't reduce future maintenance effort for
>> prepared_xacts_1.out by modifying that test script to make an
>> early exit if prepared transactions are disabled.
> +1, I was just thinking the same.
Personally I'd put the exit test after the file's header comment,
but that's just a quibble. LGTM otherwise.
regards, tom lane
^ permalink raw reply [nested|flat] 31+ messages in thread
* Re: Fix bug in multixact Oldest*MXactId initialization and access
@ 2026-03-04 09:47 Heikki Linnakangas <hlinnaka@iki.fi>
parent: Tom Lane <tgl@sss.pgh.pa.us>
0 siblings, 0 replies; 31+ messages in thread
From: Heikki Linnakangas @ 2026-03-04 09:47 UTC (permalink / raw)
To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Yura Sokolov <y.sokolov@postgrespro.ru>; Sami Imseih <samimseih@gmail.com>; Chao Li <li.evan.chao@gmail.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On 03/03/2026 20:47, Tom Lane wrote:
> Heikki Linnakangas <hlinnaka@iki.fi> writes:
>> On 03/03/2026 17:22, Tom Lane wrote:
>>> I wonder if we couldn't reduce future maintenance effort for
>>> prepared_xacts_1.out by modifying that test script to make an
>>> early exit if prepared transactions are disabled.
>
>> +1, I was just thinking the same.
>
> Personally I'd put the exit test after the file's header comment,
> but that's just a quibble. LGTM otherwise.
Yeah I agree it would look a little neater, but I put the exit test
first so that the comment doesn't get echoed to the output when the test
isn't run. That way we don't need to remember to update
prepared_xacts_1.out if we change the header comment. It hasn't changed
for 20 years, but still :-).
Committed, thanks!
- Heikki
^ permalink raw reply [nested|flat] 31+ messages in thread
end of thread, other threads:[~2026-03-04 09:47 UTC | newest]
Thread overview: 31+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-02-24 17:35 Fix bug in multixact Oldest*MXactId initialization and access Yura Sokolov <y.sokolov@postgrespro.ru>
2026-02-25 01:02 ` Sami Imseih <samimseih@gmail.com>
2026-02-25 01:44 ` Chao Li <li.evan.chao@gmail.com>
2026-02-25 15:16 ` Yura Sokolov <y.sokolov@postgrespro.ru>
2026-02-25 21:53 ` Sami Imseih <samimseih@gmail.com>
2026-02-26 02:21 ` Chao Li <li.evan.chao@gmail.com>
2026-02-26 16:22 ` Yura Sokolov <y.sokolov@postgrespro.ru>
2026-02-26 16:34 ` Sami Imseih <samimseih@gmail.com>
2026-02-26 17:02 ` Yura Sokolov <y.sokolov@postgrespro.ru>
2026-02-26 20:44 ` Sami Imseih <samimseih@gmail.com>
2026-02-27 05:00 ` Yura Sokolov <y.sokolov@postgrespro.ru>
2026-02-27 10:58 ` Heikki Linnakangas <hlinnaka@iki.fi>
2026-02-27 17:58 ` Yura Sokolov <y.sokolov@postgrespro.ru>
2026-02-27 18:51 ` Sami Imseih <samimseih@gmail.com>
2026-02-27 19:11 ` Sami Imseih <samimseih@gmail.com>
2026-02-28 07:43 ` Yura Sokolov <y.sokolov@postgrespro.ru>
2026-02-28 09:22 ` Heikki Linnakangas <hlinnaka@iki.fi>
2026-03-01 14:20 ` Yura Sokolov <y.sokolov@postgrespro.ru>
2026-03-02 02:34 ` Sami Imseih <samimseih@gmail.com>
2026-03-02 04:08 ` Chao Li <li.evan.chao@gmail.com>
2026-03-02 17:24 ` Heikki Linnakangas <hlinnaka@iki.fi>
2026-03-02 19:09 ` Sami Imseih <samimseih@gmail.com>
2026-03-03 12:02 ` Yura Sokolov <y.sokolov@postgrespro.ru>
2026-03-03 15:11 ` Heikki Linnakangas <hlinnaka@iki.fi>
2026-03-03 15:22 ` Tom Lane <tgl@sss.pgh.pa.us>
2026-03-03 16:59 ` Heikki Linnakangas <hlinnaka@iki.fi>
2026-03-03 17:08 ` Sami Imseih <samimseih@gmail.com>
2026-03-03 18:47 ` Tom Lane <tgl@sss.pgh.pa.us>
2026-03-04 09:47 ` Heikki Linnakangas <hlinnaka@iki.fi>
2026-02-27 07:48 ` Chao Li <li.evan.chao@gmail.com>
2026-02-27 08:00 ` Yura Sokolov <y.sokolov@postgrespro.ru>
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