Skip to content

fix: COMMIT PREPARED in the preparing session no longer crashes after a pgcolumnar write (#1323) - #1324

Merged
jdatcmd merged 5 commits into
commandprompt:mainfrom
OffgridwithJD:fix/1323-prepare-write-states
Oct 13, 2026
Merged

jdatcmd merged 5 commits into
commandprompt:mainfrom
OffgridwithJD:fix/1323-prepare-write-states

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

Fixes #1323.

PREPARE frees TopTransactionContext without reaching the COMMIT/ABORT arms of pgcolumnar_xact_callback. The pending write states and the delete-vector buffers live in children of that context, and only those arms reset their statics. So the session's next transaction, the COMMIT PREPARED itself, walked freed lists at its PRE_COMMIT and died of SIGSEGV. COMMIT PREPARED from another session never touched them; that's the path parallel_copy takes, so nothing in the corpus had reached this.

The fix

In the PREPARE arm, after the flush (so nothing is lost), let go of the write states and the delete vectors: PgColumnarDiscardAllPendingWrites and PgColumnarDiscardAllDeleteVectors, the same calls the COMMIT/ABORT arms make. 14 lines in columnar_tableam.c.

Not in the fix: the fetch cache. COMMIT/ABORT reset it too, and its entries hold contexts under TopTransactionContext, so I expected it to need the same treatment. A version that also discarded it at PREPARE changed no arm, even with an index fetch on each side of COMMIT PREPARED, the same rows, and a premise that the plan really is an Index Scan. So it's left out, and the suite says so. If you can see a path where a stale entry is served, I'd like the arm.

Tests (prepared_writes.sh, and an independent test_prepared_writes.py)

Each arm runs in one session, a transaction per statement, and asserts what that session answers:

  • an insert, a delete and an update, each prepared and committed;
  • an insert prepared and rolled back;
  • a write after COMMIT PREPARED;
  • the index fetch described above.

Then: the server log gained no "terminated by signal", and no prepared transaction is left behind.

Two of my own mistakes, both fixed before this push:

  • The first version asserted pg_postmaster_start_time() was unchanged. After a backend crash the postmaster reinitializes without restarting, so that passed on a run that crashed.
  • The index-scan premise read 0 under a mutation, because the arm before it had crashed the server. Both twins now wait for the server first.

Evidence

My unpushed #1304 option 5 branch also touches this callback (pre-prepare). Whichever lands second gets rebased.

🤖 Generated with Claude Code

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at df3d862. A real crash, a minimal fix, and arms that see it.

Reproduced here, not taken from the description: prepared_writes.sh from this head, on PG17, gives 11/11. The same suite with src/ taken from origin/main, on PG16, gives 5 passed + 6 failed: four arms got connection to server was lost, the post-COMMIT-PREPARED write too, and no backend was terminated by a signal: got [5] want [0].

The fetch cache: I tried to break it and couldn't. Entries are keyed on (storage id, group, command id), and command ids restart in every transaction. So I expected a fetch between PREPARE and COMMIT PREPARED, at the same command id as one inside the prepared transaction, to hit a descriptor whose context PREPARE had freed. Your arm fetches around COMMIT PREPARED, whose own COMMIT clears the cache, so it never touches that window.

My probe, on PG18 assert (freed memory clobbered): a primary-key Index Scan with the custom scan off, BEGIN; SELECT v WHERE id = 5; PREPARE TRANSACTION, then SELECT v WHERE id = 5 and id = 6 before COMMIT PREPARED. It returned value-5, value-6, then value-5 after, with no error. So your reading holds on that path too. If you'd like the window covered anyway, that sequence makes a cheap arm, but I wouldn't hold the PR for it.

This takes TESTS.md 98 like several others; whichever merges later renumbers.

@jdatcmd

jdatcmd commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

@OffgridwithJD, approved, but this is DIRTY against main now (#1311 and #1322 merged), so it needs a rebase. I'll re-approve by the per-file comparison.

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

@jdatcmd I rebased onto 2e34f90 (the #1322 merge). The head is now 5d3da63.

Per-file patch identity against df3d862 on 024e737. All 10 files are listed. Every src/ file and both twins are byte-identical. The CHANGELOG lines are identical too; only the context moved, below #1321's entry. The rest are restatements:

On the rebased tree:

  • PG17: prepared_writes.sh gives 11/11 and its twin 1/1.
  • Gate: rc=0 with that log plus the clean logs from 15, 16, 18 and 19.
  • Guard half: 419/419.

This still needs your first review.

@OffgridwithJD
OffgridwithJD force-pushed the fix/1323-prepare-write-states branch from df3d862 to 5d3da63 Compare October 13, 2026 12:23

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approved at 5d3da63. My own per-file check against df3d862: columnar_tableam.c, both twins, the ledger and the registries are identical. The budget, TESTS.md and expected_tests are restatements, and CHANGELOG.md differs by one blank line. #1317 is about to merge, so this may need one more renumber afterwards; same check then.

@jdatcmd

jdatcmd commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

@OffgridwithJD, #1317 merged (305e15e), so this conflicts with main again, most likely the TESTS.md number and the budget. One more rebase, please; I'll re-approve by the per-file check and merge when green.

jdatcmd and others added 5 commits October 13, 2026 12:36
…dprompt#1323), red

Red on PG17 (main at 024e737): every arm loses its connection -- insert,
delete, update, ROLLBACK PREPARED and a later write -- and the server log
records five backends terminated by a signal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…andprompt#1323)

PREPARE frees TopTransactionContext without reaching the COMMIT/ABORT arms that
reset the write states and delete-vector buffers, so the session's next
transaction walked freed lists at its PRE_COMMIT: a COMMIT PREPARED in the
preparing session died of SIGSEGV.

prepared_writes.sh 10/10 on PG17. Each discard is red under its own removal
(5 arms without the write-state discard, 3 without the delete-vector one). A
fetch-cache discard changed no arm, so it is not in the fix; the index-fetch arm
is there, with a premise that the plan really is an index scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ise (commandprompt#1323)

Under a mutation the arm before it crashed the server, and the premise's EXPLAIN
ran during recovery and read 0: a premise a mutation reddens, which commandprompt#1236 rules
out. Both twins now wait first, the pytest one in a fresh session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions (commandprompt#1323)

Clean: prepared_writes.sh and test_prepared_writes.py 11/11 on PG15-19. Census
1698 -> 1703; rows 2070; the gate agrees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

@jdatcmd I rebased onto 305e15e (the #1317 merge). The head is now e1532d1.

Per-file patch identity against df3d862 on 024e737. Seven of the ten files are byte-identical in their +/- lines, CHANGELOG included. The other three are restatements:

  • Budget: 1734 → 1739, rows 2145 + 11 = 2156, dated 411 + 6 = 417. The census of the merged ledger agrees: 2156 rows, 1739 never.
  • TESTS.md: section 101, after tier_partition at 100.
  • cluster_tests: 516 → 517, by collection.

On the rebased tree:

  • PG17: prepared_writes.sh gives 11/11 and test_prepared_writes.py 1/1.
  • Gate: rc=0.
  • Guard half: 419/419.

@OffgridwithJD
OffgridwithJD force-pushed the fix/1323-prepare-write-states branch from 5d3da63 to e1532d1 Compare October 13, 2026 12:37

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approved at e1532d1. My own per-file check against 5d3da63: everything is identical except the budget, TESTS.md and expected_tests, which are restatements. The budget's 1739 equals this head's never rows. Merging when CI is green.

@jdatcmd

jdatcmd commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

Merging now: approved at e1532d1, CI green.

@jdatcmd
jdatcmd merged commit 036d452 into commandprompt:main Oct 13, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

COMMIT PREPARED in the preparing session crashes the backend after a pgcolumnar write

2 participants