Skip to content

v0.6.33: shmem term cache honours subtransaction aborts (#127) - #128

Merged
styk-tv merged 3 commits into
mainfrom
fix-0-6-33-shmem-subxact-abort
Aug 20, 2026
Merged

v0.6.33: shmem term cache honours subtransaction aborts (#127)#128
styk-tv merged 3 commits into
mainfrom
fix-0-6-33-shmem-subxact-abort

Conversation

@styk-tv

@styk-tv styk-tv commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Closes #127.

What

A term first interned inside a subtransaction that aborts — the PL/pgSQL EXCEPTION pattern, where the caught error rolls back only the subtransaction while the outer transaction commits — left its staged (fingerprint → dict_id) pair in the per-backend pending list, and the outer commit published it to the cross-backend shmem cache with the dictionary row already rolled back. Every later intern of that exact lexical value, from any backend, resolved to the dangling id (pgrdf.get_term NULL, quads dereferencing to nothing) until pgrdf.shmem_reset() or a postmaster restart. One refused write inside an exception handler could poison shared vocabulary terms for every backend.

Fix

Each staged entry now carries GetCurrentSubTransactionId(), and an AbortSub callback (pgrx register_subxact_callback) discards entries at-or-above the aborting subtransaction's id. Subtransaction ids are monotonic within a backend and, while a subtransaction is open, every newer id is nested inside it — so >= my_subid is exactly the set whose dictionary INSERTs rolled back. Entries from committed subtransactions and the outer transaction survive; top-level commit/abort semantics are unchanged. Registration rides the existing once-per-transaction flag (pgrx clears subxact hooks at top-level transaction end).

Tests

Three new #[pg_test]s in storage::shmem_cache (the first was written before the fix and failed with left: 1, right: 0 — the staged entry surviving the abort):

  • shmem_subxact_abort_discards_staged_entries — abort discards
  • shmem_subxact_commit_keeps_staged_entries — committed subtransactions keep their entries (no over-discard)
  • shmem_subxact_abort_keeps_earlier_entries — an abort drops only its own entries; earlier top-level staging survives

Full suite: 384 passed / 0 failed (pg18, builder container). fmt + clippy -D warnings clean.

Proven end-to-end

On a disposable pg18 instance running 0.6.32, both repro layers confirmed the poison (issue #127 has the pure-SQL recipe: retry of an aborted-subxact intern returned an id with 0 dictionary rows; a validation-refused write followed by an identical retry failed on values provably present). After upgrading that instance to this branch's build (ALTER EXTENSION pgrdf UPDATE, pgrdf.version() == extversion == 0.6.33):

  • pure recipe: the retry interns a live id — dereferences correctly, dictionary row present
  • refused-write-then-retry recipe: the identical retried write succeeds immediately

Zero shmem_reset() calls in either proof.

Release

  • Version 0.6.33 across Cargo.toml/lock, pgrdf.control, compose per-file mount
  • sql/pgrdf--0.6.32--0.6.33.sql upgrade bridge (comment-only — behaviour ships in the .so, no DDL)
  • CHANGELOG folded

…127)

A term first interned inside a subtransaction that aborts — the
PL/pgSQL EXCEPTION pattern, where the caught error rolls back only the
subtransaction and the outer transaction commits — left its staged
(fingerprint → dict_id) pair in the per-backend pending list, and the
outer commit published it to the cross-backend shmem cache with the
dictionary row already rolled back. Every later intern of that exact
lexical value then resolved to the dangling id (get_term NULL, quads
dereferencing to nothing) until pgrdf.shmem_reset() or a postmaster
restart. One refused write inside an exception handler could poison
shared vocabulary terms for every backend.

Each staged entry now carries GetCurrentSubTransactionId() and an
AbortSub callback drops entries at-or-above the aborting id —
subtransaction ids are monotonic within a backend, so while a
subtransaction is open every newer id is nested inside it, making
>= the exact rollback set. Committed subtransactions and the outer
transaction keep their entries; top-level commit/abort semantics are
unchanged. Registration rides the existing once-per-transaction flag
(pgrx clears subxact hooks at top-level transaction end).

Three regression tests: abort discards, commit keeps, and an abort
drops only its own entries (earlier top-level staging survives).
Version to 0.6.33 across Cargo.toml/lock, pgrdf.control, the compose
per-file mount, the 0.6.32 -> 0.6.33 upgrade bridge (comment-only — the
fix ships in the .so as behaviour, no DDL), and the CHANGELOG fold.
The smoke golden pins pgrdf.version()/extversion; the 0.6.33 bump
missed it and CI regression caught exactly that (100 pass, 1 fail).
@styk-tv
styk-tv merged commit 178b453 into main Aug 20, 2026
4 checks passed
@styk-tv
styk-tv deleted the fix-0-6-33-shmem-subxact-abort branch August 20, 2026 11:30
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.

shmem term cache publishes dict ids rolled back by subtransaction abort — poisoned terms resolve to dangling ids until shmem_reset()

1 participant