RE: Bug in logical decoding with DDL and subtransactions

From: Bingshuai Li <lucian1412(at)outlook(dot)com>
To: Michael Paquier <michael(at)paquier(dot)xyz>, Tom Lane <tgl(at)sss(dot)pgh(dot)pa(dot)us>
Cc: Alexander Lakhin <exclusion(at)gmail(dot)com>, "alvherre(at)kurilemu(dot)de" <alvherre(at)kurilemu(dot)de>, "pgsql-hackers(at)lists(dot)postgresql(dot)org" <pgsql-hackers(at)lists(dot)postgresql(dot)org>, "mark(dot)dilger(at)enterprisedb(dot)com" <mark(dot)dilger(at)enterprisedb(dot)com>, Amit Kapila <amit(dot)kapila16(at)gmail(dot)com>, Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com>, Andrey Rachitskiy <pl0h0yp1(at)gmail(dot)com>, "a(dot)kozhemyakin(at)postgrespro(dot)ru" <a(dot)kozhemyakin(at)postgrespro(dot)ru>, Michael Paquier <michael(at)paquier(dot)xyz>
Subject: RE: Bug in logical decoding with DDL and subtransactions
Date: 2026-09-30 09:12:31
Message-ID: ME0P300MB0953B3141FF265CC9239276CC68B2@ME0P300MB0953.AUSP300.PROD.OUTLOOK.COM
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi Hayato, Hi Tom,

Thanks, Hayato, for reviewing and confirming the issue, and Tom for
following up. Attached is v5, with the cleanup moved out of
ReorderBufferAbort() and a test for nested subtransaction rollback.

1. Hayato's comment

> The path you added might be redundant if the top-level transaction is
> being aborted, right? Can we export the cleanup part outside
> ReorderBufferAbort()? It does the actual task only if needed.

Agreed. The cleanup loop is now in
ReorderBufferCleanupSubTxnTupleCids(), and ReorderBufferAbort() is back
to its upstream implementation. DecodeAbort() calls the helper for
each aborted xid before removing its transaction state, passing the
abort record's primary xid as well.

The helper uses the known toplevel of the xid being cleaned. If that
toplevel's xid equals the primary xid, the whole transaction is being
aborted, so it returns without scanning the tuplecid list;
ReorderBufferCleanupTXN() will free it. Otherwise it removes the
tuplecid entries written by that subtransaction.

The primary xid's own association cannot be used to decide whether
cleanup is needed. An outer subtransaction that wrote no WAL of its
own can have no known association when its abort is decoded: the abort
record is written after entering TRANS_ABORT, so
IsSubxactTopXidLogPending() does not attach the toplevel xid. Released
inner subtransactions listed in that abort record can still have known
associations, and their tuplecids must be removed from the surviving
toplevel's list.

A new isolation test, tuplecid_nested, covers that shape: the toplevel
writes WAL first, an inner subtransaction inserts into a user catalog
table and is RELEASEd, and the outer subtransaction (no WAL of its own)
is then rolled back; using the page-fill and vacuum recipe of
tuplecid_restart, the toplevel's later insert deterministically reuses
the aborted subtransaction's line pointer. With cleanup gated on the
primary xid's known association, the test dies with the original cmin
assertion; with the attached version it passes. (The other test files
are unchanged from v4.)

2. Behavior vs v4, scenario by scenario

- Partial rollback, aborted subxact wrote its own WAL (the BUG #19555 /
ddl.sql shape): cleanup runs, as in v4. The tuplecid regression test
still trips the original assertion without the fix and passes with it.
- Partial rollback, aborted outer subxact without own WAL, released
inner subxact (the nested shape): the inner subxact's entries are
cleaned, as in v4, even if the outer subxact's association is unknown.
- Toplevel abort: no scan at all (early return), and
ReorderBufferCleanupTXN() frees the whole list.
- Association of the xid being cleaned unknown (a decoding pass that
started mid-transaction): cleanup skipped, as in v4; the restart-point
invariant from my v4 mail still applies unchanged (a pass that
output-decodes the toplevel commit has replayed the subtransaction's
first record and knows the association; later passes skip the commit
entirely), and tuplecid_restart still passes.
- Two-phase aborts and ReorderBufferAbortOld() are untouched.

Compared with v4, this adds one exported helper,
ReorderBufferCleanupSubTxnTupleCids(). ReorderBufferAbort() retains its
existing signature. The tuplecid subxid field and the additional subxid
argument to ReorderBufferAddNewTupleCids() are unchanged from v4.

3. Verification matrix (all --enable-cassert --enable-debug unless
noted; make check in contrib/test_decoding):

ENFORCE below denotes a build with
ENFORCE_REGRESSION_TEST_NAME_RESTRICTIONS defined.

branch base apply regress + isolation
master 6a93535798a clean (also non-assert and
ENFORCE builds) 21 + 16
REL_19_STABLE ab274a1e966 clean 21 + 16
REL_18_STABLE ea24eadda01 only the known trivial
test-list conflicts 20 + 15
REL_17_STABLE 583b414e511 branch version attached 20 + 15
REL_16_STABLE b8d50e14252 branch version attached 20 + 15
REL_15_STABLE 38e07ce9847 branch version attached 20 + 15
REL_14_STABLE 42ebc601fec branch version attached 20 + 14

tuplecid, tuplecid_restart and tuplecid_nested pass everywhere.

4. Tom's question about the recent buildfarm failures

I have some possible leads, but have not isolated the change that made
the failure more frequent. In the prion history I inspected, the first
failure with this signature was on September 15 at 21:39, at
862092932c9; the preceding green run was at 09:13, at ff39a858b984.
There are seven commits between those revisions.

Alexander's candidate a4b26b8f7 removes 30 lines of ddl.sql before the
tr_sub_ddl section, although the failing section itself is unchanged.
Another commit in that interval, ddce1da5b1b, adds a pg_opclass.dat
entry. Both could affect catalog layout, on which this bug's TID
collision depends. That is a possible explanation for why the revert
could matter, but I have not established that either change caused the
increase in failures.

The failures are intermittent on prion and skink. Locally, 50 runs at
prion's first failing revision with the same configure flags produced
no crashes. Michael's report of frequent failures under concurrent
check-world builds provides a useful lead about the role of the
environment, but these observations do not rule out a recent code
change as the trigger.

Thanks to Michael for the reproducer. I also tried it, adding two
pg_log_standby_snapshot() calls to ddl.sql, under CPU load:

- An unfixed tree at 862092932c9, built with cassert and
RELCACHE_FORCE_RELEASE, CATCACHE_FORCE_RELEASE and
REALLOCATE_BITMAPSETS, hit the original cmin assertion in all five
runs, at the tr_sub_ddl get_changes call.
- The attached v5 on 6a93535798a, built with cassert and debug but
without those three defines, completed ddl.sql without the assertion
in all five runs. make check still reported a failure because the
expected output lacked the two added queries' output; the remaining
decoded output matched.

Those runs used different baselines and build configurations, so they
do not isolate the patch's effect. They show that I can reproduce the
reported failure and that the v5 build completes the reproducer. The
nested test's failure with the association-gated variant and success
with v5 provide separate evidence for the nested rollback fix.

The patch removes the aborted subtransaction's tuplecids before the
surviving toplevel commit is processed. If prion or skink still hits
the assertion after the fix, I'll follow up with that evidence.

Does this address your comment, Hayato? Happy to adjust further.

Thanks,
Bingshuai Li

Attachment Content-Type Size
v5-0001-Fix-stale-tuplecid-records-left-behind-by-aborted.patch application/octet-stream 35.5 KB
v5-REL_14_STABLE-0001-Fix-stale-tuplecid-records-left-behind-by-aborted.patch application/octet-stream 33.7 KB
v5-REL_15_STABLE-0001-Fix-stale-tuplecid-records-left-behind-by-aborted.patch application/octet-stream 34.7 KB
v5-REL_16_STABLE-0001-Fix-stale-tuplecid-records-left-behind-by-aborted.patch application/octet-stream 35.4 KB
v5-REL_17_STABLE-0001-Fix-stale-tuplecid-records-left-behind-by-aborted.patch application/octet-stream 35.4 KB

In response to

Responses

Browse pgsql-hackers by date

  From Date Subject
Next Message Hannu Krosing 2026-09-30 09:15:08 Re: Direct TOAST v2, faster, smaller and no migration needed
Previous Message Hannu Krosing 2026-09-30 09:07:46 Re: Direct TOAST v2, faster, smaller and no migration needed