[Bug] ParsePrepareRecord() parses XLOG_XACT_PREPARE with a stale xl_xact_prepare layout: gid decodes as empty in pg_waldump and two-phase logical decoding
- Dominant language
- C
- Stars
- 1.4k
- Forks
- 247
- Avg merge
- 4d 3h
- Merged PRs (30d)
- 39
Description
### Apache Cloudberry version
main (`89baa48a4f4`, 2026-09-07) and local `3.0.0-devel+dev.8551.gc781604c5ba` (PostgreSQL 16.9 base). The mismatch has existed since the GPDB-era commits `b0208894eb5` / `68c0ff2d4ec` / `b74f174b6cd` (2019) that extended `TwoPhaseFileHeader`.
### What happened
`ParsePrepareRecord()` in `src/backend/access/rmgrdesc/xactdesc.c` walks a `XLOG_XACT_PREPARE` record using the layout of `xl_xact_prepare` (`src/include/access/xact.h`). But the record is actually written by `StartPrepare()`/`EndPrepare()` (`twophase.c`) as a `TwoPhaseFileHeader` (`src/include/access/twophase_xlog.h`), and Cloudberry extended that struct with fields that `xl_xact_prepare` never got:
```
TwoPhaseFileHeader (what is written, 96 bytes) xl_xact_prepare (what is parsed, 72 bytes)
... ...
int32 nabortrels; int32 nabortrels;
int32 ncommitdbs; <- extra int32 ncommitstats;
int32 nabortdbs; <- extra int32 nabortstats;
int32 ncommitstats; int32 ninvalmsgs;
int32 nabortstats; bool initfileinval;
int32 ninvalmsgs; uint16 gidlen;
bool initfileinval; XLogRecPtr origin_lsn;
Oid tablespace_oid_to_delete_on_abort; <- extra TimestampTz origin_timestamp;
Oid tablespace_oid_to_delete_on_commit; <- extra
uint16 gidlen;
XLogRecPtr origin_lsn;
TimestampTz origin_timestamp;
```
Consequences of reading through the wrong struct:
- `xlrec->gidlen` is read from offset 54, which is the upper half of the real `nabortstats` (normally 0), so `parsed->twophase_gid` is an **empty string**.
- `xlrec->ncommitstats` / `nabortstats` / `ninvalmsgs` actually read `ncommitdbs` / `nabortdbs` / `ncommitstats`; `initfileinval` and `origin_lsn` read from unrelated bytes.
- `bufptr` starts at offset 72 instead of 96, so `parsed->subxacts`, `xlocators`, `abortlocators`, `stats`, `abortstats`, `msgs` all point at the wrong bytes. The parser also never skips the `commitdbs` / `abortdbs` (`DbDirNode`) arrays that `EndPrepare()` writes between `abortrels` and the stats arrays.
Two user-visible symptoms:
1. **`pg_waldump`** prints an empty gid for every PREPARE record (every distributed transaction on a segment):
```
rmgr: Transaction ... desc: PREPARE gid : 2026-09-09 05:06:49.215203 CST
rmgr: Transaction ... desc: COMMIT_PREPARED 5256: 2026-09-09 05:06:49.216803 CST gxid = 34413
```
(`COMMIT_PREPARED` is parsed by `ParseCommitRecord()` and is fine, which shows the gid really is `34413` in the WAL.)
2. **Logical decoding with `two_phase`** emits `PREPARE TRANSACTION ''` with an empty gid, while the matching `COMMIT PREPARED` carries the real gid. A pgoutput subscriber would try to `PREPARE TRANSACTION ''`. Worse, `DecodePrepare()` passes the shifted `parsed->subxacts` / `nsubxacts` into `SnapBuildCommitTxn()` and `ReorderBufferPrepare()`, so decoding can consume garbage subxact ids.
Crash recovery is not affected because `xact_redo()` for PREPARE goes through `twophase.c`'s own `TwoPhaseFileHeader`-based parsing, not `ParsePrepareRecord()`.
### What you think should happen instead
`xl_xact_prepare` must have the same layout as `TwoPhaseFileHeader` (the upstream PostgreSQL comment in `twophase.c` says the two must stay in sync), and `ParsePrepareRecord()` must skip the `commitdbs` / `abortdbs` arrays. Then `pg_waldump` shows `PREPARE gid 34413: ...` and two-phase logical decoding emits `PREPARE TRANSACTION '34413'`.
A static assert comparing `sizeof(xl_xact_prepare)` and `sizeof(TwoPhaseFileHeader)` (or simply typedef-ing one to the other) would keep this from regressing.
### How to reproduce
On a demo cluster with `wal_level = logical` (needed only for step 2; step 1 works with `replica`):
```sql
-- via coordinator
create table t2pc(id int primary key, v text) distributed by (id);
insert into t2pc select g, 'x' from generate_series(1,6) g; -- touches all segments -> 2PC
```
1. `pg_waldump` on any primary segment:
```
pg_waldump -r Transaction /pg_wal/ | grep -E 'PREPARE|COMMIT_PREPARED' | tail -2
```
shows `PREPARE gid : ` (empty gid) followed by `COMMIT_PREPARED ... gxid = `.
2. two-phase logical decoding on a segment (utility mode):
```sql
-- PGOPTIONS='-c gp_role=utility' psql -p
select * from pg_create_logical_replication_slot('s2pc', 'test_decoding', false, true);
-- run the INSERT above on the coordinator
select data from pg_logical_slot_peek_changes('s2pc', NULL, NULL, 'include-xids', '1');
```
output:
```
BEGIN 5256
table public.t2pc: INSERT: id[integer]:2 v[text]:'x'
...
PREPARE TRANSACTION '', txid 5256
COMMIT PREPARED '34413', txid 5256
```
### Operating System
CentOS 7 (kernel 3.10.0-1160), gcc 12.2.1
### Anything else
Found while prototyping an MPP logical-replication receiver, where the segment-side gid (which is the distributed xid) is the key for aligning per-segment streams. The fix is small and self-contained; happy to send a PR.
### Are you willing to submit PR?
- [x] Yes I am willing to submit a PR!
### Code of Conduct
- [x] I agree to follow this project's [Code of Conduct](https://www.apache.org/foundation/policies/conduct)
Contributor guide
Research direction
Start with ParsePrepareRecord() in src/backend/access/rmgrdesc/xactdesc.c and compare src/include/access/xact.h with TwoPhaseFileHeader in src/include/access/twophase_xlog.h; trace StartPrepare()/EndPrepare() in twophase.c. Reproduce the issue with the supplied pg_waldump and logical-decoding commands. Done means PREPARE displays and emits the real gid, with correct two-phase decoding and no regression in the related record layout.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, postgresql
- Domain
- databases, distributed-systems
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100