Repository navigation
Conversation
6850c08 to
3b6f413
Compare
c3ee91f to
c7e7712
Compare
d8fed9e to
40f3392
Compare
There was a problem hiding this comment.
🔍 OCR found 296 issue(s).
- 25 inline, 271 in summary (inline capped at 25)
⚠️ 1 warning(s) during review
📄 src/backend/access/flux/flux_dirtymap.c (L272-L277)
Lock-free read with no acquire barrier. pg_atomic_read_u64 provides NO barrier semantics in this tree (see the explicit pg_atomic_read_membarrier_u64 variant in port/atomics.h). The claimed correctness rests on a happens-before with the writer's buffer-content lock, but the reader here takes no lock and issues no pg_read_barrier(), so on weakly-ordered architectures (ARM64, PPC64, RISC-V) the load of the slot is not ordered against the reader's subsequent read of the on-page tuple flags in the scan path. A reader that observes a stale-clean slot skips the per-tuple sLog before-image probe and can serve an already-overwritten tuple version -- a silent MVCC/data-correctness hazard, not merely a missed optimization. Add a paired acquire barrier (or use pg_atomic_read_membarrier_u64) here, and confirm the writer publishes with release ordering before the page becomes reachable by a scanner. Confidence: high.
📄 src/backend/access/flux/flux_dirtymap.c (L267-L270)
Data race: part->full is a plain bool written under the partition spinlock (part->full = true) but read here lock-free without the spinlock or any atomic/barrier. A lock-free read of a variable concurrently written under a lock is undefined behavior under the C memory model; a torn or reordered read is not guaranteed correct on all supported platforms, and the comment's false->true-once argument does not make a non-atomic read well-defined. Use a pg_atomic_flag/pg_atomic_uint32 for full (write with a barrier under the lock, read with the matching atomic accessor). Confidence: moderate.
📄 src/backend/access/flux/flux_dirtymap.c (L158-L162)
FluxDirtyMapShmemInit() is an exported, empty stub whose only content is a comment saying init is handled by the callbacks; grep shows it has no callers anywhere in the tree (only the two extern declarations). This is dead code -- drop the function and its declarations in flux_dirtymap.h/flux.h, or make it do something. Speculative scaffolding with no caller should not be committed. Confidence: high.
📄 src/backend/access/flux/flux_dirtymap.c (L115-L115)
The >> 7 shift is coupled to FLUX_DIRTYMAP_PARTITIONS == 128 == 2^7: flux_dirtymap_part consumes the low 7 bits and this discards them to pick the initial slot. The relationship is implicit and undocumented at the shift site; if FLUX_DIRTYMAP_PARTITIONS is ever changed, this silently produces correlated partition/slot selection and worse distribution. Derive the shift from pg_ceil_log2 / a named constant tied to FLUX_DIRTYMAP_PARTITIONS, or at minimum add a comment and a StaticAssertDecl that FLUX_DIRTYMAP_PARTITIONS == (1 << 7). Confidence: high.
📄 src/backend/access/flux/flux_dirtymap.c (L70-L71)
relid is a per-database OID (callers pass RelationGetRelid(relation)), but this map lives in cluster-wide shared memory. The key (relid << 32) | blkno therefore collides across databases: two relations with the same OID in different databases map to the same key. That is safe (a false positive only forces an extra sLog probe) but silently defeats the fast-path filter for such pages, and the header's "Each (relid, blkno) page owns a distinct 64-bit key" claim is false cross-database. Either use a database-global identifier (e.g. include dbid/relfilenumber) or document the cross-DB collision explicitly. Confidence: moderate.
📄 src/backend/access/flux/flux_fsm.c (L160-L170)
FluxVacuumFSM is dead and redundant code, and the comment is misleading. The only caller (flux_operations.c:6280) invokes FluxVacuumFSM(onerel, RelationGetNumberOfBlocks(onerel)), so new_nblocks and the old_nblocks computed here from the same RelationGetNumberOfBlocks(rel) are always equal -- the new_nblocks < old_nblocks branch is unreachable.
Even if it were reached, this is incorrect: FreeSpaceMapPrepareTruncateRel() only prepares the truncation. Per its contract (freespace.c), the caller must also smgrtruncate() the FSM fork and call FreeSpaceMapVacuumRange(); neither is done here, which would leave dangling FSM fork pages and stale upper-level FSM entries.
Moreover the work is already done: FluxTruncateRelation() calls RelationTruncate(), which fully handles FSM truncation (prepare + smgrtruncate on FSM_FORKNUM + FreeSpaceMapVacuumRange). Recommend removing FluxVacuumFSM and its call site entirely (confidence: high).
📄 src/backend/access/flux/flux_lock.c (L334-L346)
Correctness defect: this mapping diverges from FluxLockTuple/FluxUnlockTuple, which map the four tuple modes to four distinct LOCKMODEs (AccessShareLock / RowShareLock / ExclusiveLock / AccessExclusiveLock, matching heap's tupleLockExtraInfo). Here KeyShare+Share collapse to ShareLock and NoKeyExclusive+Exclusive collapse to ExclusiveLock. As a result LockHeldByMe() is queried with a LOCKMODE that was never acquired (e.g. a LockTupleKeyShare lock is taken as AccessShareLock but looked up as ShareLock), so this function can never correctly report a lock that FluxLockTuple acquired -- defeating its stated purpose. Use the identical 4-way mapping as FluxLockTuple. (high confidence)
💡 Suggested change
Before:
switch (mode)
{
case LockTupleKeyShare:
case LockTupleShare:
lockmode = ShareLock;
break;
case LockTupleNoKeyExclusive:
case LockTupleExclusive:
lockmode = ExclusiveLock;
break;
default:
return false;
}
After:
switch (mode)
{
case LockTupleKeyShare:
lockmode = AccessShareLock;
break;
case LockTupleShare:
lockmode = RowShareLock;
break;
case LockTupleNoKeyExclusive:
lockmode = ExclusiveLock;
break;
case LockTupleExclusive:
lockmode = AccessExclusiveLock;
break;
default:
return false;
}
📄 src/backend/access/flux/flux_lock.c (L327-L328)
Dead scaffolding: FluxHoldsTupleLock, FluxLockMultipleTuples, FluxLockPage, FluxUnlockPage, and FluxLockRelationForDDL are defined and declared in access/flux.h, but none of them have any caller in the tree (verified by search across src/backend and src/include). PostgreSQL rejects speculative, unwired code (YAGNI). Drop these until an actual caller exists; keep only FluxLockTuple/FluxUnlockTuple, which are used by flux_handler.c and flux_operations.c. (high confidence)
📄 src/backend/access/flux/flux_lock.c (L71-L87)
DRY: the 24-line LockTupleMode->LOCKMODE switch is copy-pasted verbatim into FluxLockTuple, FluxUnlockTuple (and a third, divergent copy in FluxHoldsTupleLock). Because the copies must stay in lockstep for lock acquire/release/query to match, this is exactly the kind of duplication that produced the divergence bug flagged above. Extract a single static helper (e.g. static LOCKMODE flux_tuple_lockmode(LockTupleMode mode)) and call it from all sites. (high confidence)
📄 src/backend/access/flux/flux_lock.c (L13-L15)
Stale/inaccurate header comment. The NOTES advertise "buffer locks" and "deadlock detection", but this file only wraps the heavyweight LOCKTAG lock manager: there is no buffer-lock logic, and deadlock detection is provided by the lock manager itself, not here. Comments must describe what the code actually does. Reword to describe the LOCKTAG_TUPLE/LOCKTAG_PAGE wrappers only. (moderate confidence)
📄 src/backend/access/flux/flux_lock.c (L22-L22)
Unused includes. access/tableam.h is included but no tableam symbol is referenced in this file, and storage/bufmgr.h is only mentioned in a comment (no LockBuffer/ReadBuffer call). Drop both to keep the patch minimal. (moderate confidence)
📄 src/backend/access/flux/flux_lock.c (L254-L267)
O(n^2) bubble sort to order TIDs. The comment asserts N is "typically small" but nothing bounds ntids, so a large array becomes a quadratic hazard. Reuse the standard sort infrastructure (qsort_arg with an ItemPointerCompare-based comparator, as index_delete_sort/heap code already do) instead of hand-rolling a bubble sort. (moderate confidence)
📄 src/backend/access/flux/flux_pvs.c (L109-L109)
Missing CHECK_FOR_INTERRUPTS() in a loop that can iterate up to FLUX_PVS_MAX_CHAIN_DEPTH (10000) times, each performing a buffered UNDO-fork read (RelUndoReadRecord acquires a buffer lock and may do I/O; it does not itself call CHECK_FOR_INTERRUPTS). A long/pathological chain cannot be cancelled promptly by the client or by recovery-conflict/termination. Add CHECK_FOR_INTERRUPTS() at the top of the loop. (medium confidence)
💡 Suggested change
Before:
if (depth++ > FLUX_PVS_MAX_CHAIN_DEPTH)
After:
CHECK_FOR_INTERRUPTS();
if (depth++ > FLUX_PVS_MAX_CHAIN_DEPTH)
📄 src/backend/access/flux/flux_pvs.c (L151-L152)
verptr is a RelUndoRecPtr, which is typedef'd to uint64 (src/include/access/relundo.h). The tree's convention for formatting a uint64 is UINT64_FORMAT, not an ad-hoc (unsigned long long)/%llu cast. Use UINT64_FORMAT here and at the other two %llu sites in this file to stay consistent and portable across toolchains. (medium confidence)
💡 Suggested change
Before:
"FLUX PVS: RELUNDO_UPDATE without tuple at %llu",
(unsigned long long) verptr);
After:
"FLUX PVS: RELUNDO_UPDATE without tuple at " UINT64_FORMAT,
verptr);
📄 src/backend/access/flux/flux_pvs.c (L6-L7)
Stale header comment: the version pointer no longer lives in a "trailing" position of the on-page image. FluxTupleGetVersionPtr() reads the fixed header field hdr->t_verptr, and flux.h explicitly notes it "was formerly an unaligned 8-byte trailer located by item_len - 8." The phrases "stamp a trailing RelUndoRecPtr (verptr)" and "whose own trailing verptr -- preserved verbatim through the reverse-apply" describe the old layout and now misdescribe current behavior. Update the header to reflect the fixed t_verptr header field. (high confidence)
📄 src/backend/access/flux/flux_pvs.c (L125-L126)
Memory-ownership footgun on the error path. allocated (and the current payload) are palloc'd in CurrentMemoryContext. If RelUndoReadRecord() or the palloc() of next_image throws elog(ERROR) after a prior iteration already set allocated, both the prior allocated buffer and any live payload leak until the surrounding context resets. The elog(WARNING) sites are fine (non-throwing), but the throwing calls are not protected. Since the leak is bounded by the caller's per-query/executor MemoryContext reset this is not a hard bug, but the split contract (caller frees *out_data on true, must not touch it on false) is easy to misuse. Consider documenting/allocating in an explicit context, or note the reliance on context reset. (moderate confidence)
📄 src/backend/access/flux/flux_mvcc.c (L150-L151)
Portability/correctness (high): xact_start_ts_slots[] holds uint64 values that are written here (and in cleanup/prepare/resolve) and read lockless in FluxGetOldestActiveTimestamp(). On 32-bit platforms a uint64 load/store is not atomic, so a concurrent reader can observe a torn value. That torn timestamp feeds VACUUM/defrag's reclamation horizon (FluxGetOldestActiveTimestamp -> FluxTupleDeadToAll), so a torn read can compute a too-new horizon and prematurely reclaim tuples still visible to a running transaction. The comment's claim of "a compiler barrier via volatile access" is also inaccurate: the field is a plain uint64, not volatile, and a barrier is not a substitute for an atomic 64-bit access. Use pg_atomic_uint64 (port/atomics) for these slots.
📄 src/backend/access/flux/flux_mvcc.c (L133-L133)
Dead scaffolding (YAGNI): serializable_horizon is only ever written (initialized to 1 here and in the two init paths, then computed as Min(existing, commit_ts) in FluxCommitTransaction). No code reads it. Moreover the Min() is semantically inverted: seeded at 1 and only ever taking the minimum, it can only shrink toward 1 and never represents a meaningful horizon. Remove the field and its update in FluxCommitTransaction.
📄 src/backend/access/flux/flux_mvcc.c (L277-L280)
Dead duplicate code: FluxMvccShmemInit() is never called — the wired path is the PG_SHMEM_SUBSYSTEM(FluxMvccShmemCallbacks) registration (see subsystemlist.h), whose FluxMvccShmemInit_cb duplicates this initialization verbatim. Keeping both is a DRY/maintenance hazard (the two copies can drift) and both register on_shmem_exit(FluxShmemExit, 0). Remove FluxMvccShmemInit() (and its extern in flux.h), keeping only the subsystem callbacks.
📄 src/backend/access/flux/flux_mvcc.c (L581-L582)
Inconsistent horizon-invalidation predicate vs a genuine data-loss risk. FluxCleanupTransactionState() bumps the generation only when my_ts == oldest_active_ts (strict equality), while FluxResolvePreparedSlot() uses slot_ts <= oldest_active_ts. With the strict == here, if two backends share the same cached oldest_active_ts but the departing one is truly the oldest and its stored my_ts no longer equals the (possibly stale) cached value, the cache is not invalidated and FluxGetOldestActiveTimestamp() can keep returning a too-new horizon -> premature reclamation. Since correctness (avoiding data loss) outranks the invalidation-frequency optimization, use the conservative <= here too (matching FluxResolvePreparedSlot).
📄 src/backend/access/flux/flux_mvcc.c (L1178-L1181)
Hot-path stack usage: FluxTupleSatisfiesMVCC is called per-tuple on every scan, and each uncommitted-branch declares SLogTupleOp e[SLOG_MAX_TUPLE_OPS] (128 * sizeof(SLogTupleOp) ~= 5KB) on the stack. Two such branches exist (xmin and xmax). This is large for the visibility hot path. Consider a smaller bounded buffer sized to what the loop actually needs, or reuse a single scratch buffer.
📄 src/backend/access/flux/flux_mvcc.c (L522-L522)
ASCII-only violation: these comments contain em-dash (U+2014) characters, which the source style forbids. Replace with -- or a plain hyphen. (Other lines at 710 and 721 have the same issue.)
📄 src/backend/access/flux/flux_relundo.c (L99-L99)
This installs the 2PC prepare hook TableAMPrepare_hook = AtPrepare_Flux, but this file and this function are documented as being solely about the AM-neutral per-relation UNDO (RELUNDO) hooks (see the file header: "FLUX adapters for the AM-neutral per-relation UNDO hooks", and this function's comment: "Wire the FLUX implementations into the AM-neutral UNDO core"). AtPrepare_Flux is a 2PC/PrepareTransaction concern (declared in flux.h, defined in flux_operations.c), unrelated to relundo. Installing it here silently piggybacks an undocumented concern onto the RELUNDO installer. Either move this assignment to a FLUX 2PC/AM init path, or update both comments to state that this installer also wires the 2PC prepare hook, so the file's stated purpose matches what it does. Confidence: moderate.
📄 src/backend/access/flux/flux_stats.c (L50-L52)
These two functions (and the entire FluxRelationStats machinery) have no callers anywhere in the tree: FluxCollectRelationStats/FluxLogRelationStats are never invoked, and no code reads any FluxRelationStats field (compression_ratio, bloat_factor, etc.). This is speculative/dead scaffolding. Per the minimalism (YAGNI) gate, do not add unwired infrastructure; either wire it into the FLUX ANALYZE path in this patch or drop it. (high confidence)
📄 src/backend/access/flux/flux_stats.c (L249-L253)
int64 must be printed with INT64_FORMAT, not by casting to (long long) and using %lld; %lld is not guaranteed across all supported compilers/platforms (portability hard gate). Same applies to the %llu / (unsigned long long) uses below for commit_ts_min/max (use UINT64_FORMAT). Also the message text should not be split with per-field casts to long long.
💡 Suggested change
Before:
"%lld pages, %lld live tuples, %lld dead tuples",
RelationGetRelationName(rel),
(long long) stats->total_pages,
(long long) stats->total_live_tuples,
(long long) stats->total_dead_tuples)));
After:
"%lld pages, " INT64_FORMAT " live tuples, " INT64_FORMAT " dead tuples",
RelationGetRelationName(rel),
stats->total_pages,
stats->total_live_tuples,
stats->total_dead_tuples)));
📄 src/backend/access/flux/flux_stats.c (L281-L284)
Use UINT64_FORMAT for uint64 values instead of casting to (unsigned long long) with %llu (portability hard gate).
💡 Suggested change
Before:
"commit-ts word range [%llu .. %llu]",
RelationGetRelationName(rel),
(unsigned long long) stats->commit_ts_min,
(unsigned long long) stats->commit_ts_max)));
After:
"commit-ts word range [" UINT64_FORMAT " .. " UINT64_FORMAT "]",
RelationGetRelationName(rel),
stats->commit_ts_min,
stats->commit_ts_max)));
📄 src/backend/access/flux/flux_stats.c (L135-L138)
Out-of-bounds read on corrupt/unexpected items. hdr->t_flags and hdr->t_commit_ts are dereferenced unconditionally, but the code never verifies item_len >= sizeof(FluxTupleHeader) for a normal (non-overflow) item. FluxIsOverflowRecordInline only rules out overflow-record magic; a short/corrupt normal item that is not an overflow record will fall through to total_tuple_bytes += item_len and then read hdr->t_flags/hdr->t_commit_ts past the item boundary. Guard the header dereference with an explicit item_len >= sizeof(FluxTupleHeader) check (or skip/report such items) before reading header fields.
📄 src/backend/access/flux/flux_slot.c (L746-L751)
This same-buffer fast path frees rslot->tuple but never frees rslot->values_block, unlike tts_flux_clear(). A slot materialized into values-only state has SHOULDFREE set with rslot->tuple == NULL and rslot->values_block populated; re-storing into this slot with the same (or Invalid) buffer clears the SHOULDFREE flag without freeing the block, leaking it (and leaving a stale non-NULL values_block pointer). Free the block here too and reset the pointer to keep ownership handling consistent with tts_flux_clear().
💡 Suggested change
Before:
if (unlikely(TTS_SHOULDFREE(slot)))
{
if (rslot->tuple)
pfree(rslot->tuple);
slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
}
After:
if (unlikely(TTS_SHOULDFREE(slot)))
{
if (rslot->tuple)
{
pfree(rslot->tuple);
rslot->tuple = NULL;
}
if (rslot->values_block)
{
pfree(rslot->values_block);
rslot->values_block = NULL;
}
slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
}
📄 src/backend/access/flux/flux_slot.c (L205-L206)
tuple_has_compressed is dead code: it is assigned false and immediately silenced with (void); it is never read. Remove the variable and its declaration (YAGNI / patch hygiene).
📄 src/backend/access/flux/flux_slot.c (L745-L745)
Non-ASCII em-dash characters violate the ASCII-only rule for source (git diff --check / pgsql-hackers convention). Replace with '--' or a plain hyphen.
💡 Suggested change
Before:
/* Same buffer — just free any materialized data */
After:
/* Same buffer -- just free any materialized data */
📄 src/backend/access/flux/flux_slot.c (L755-L755)
Non-ASCII em-dash character; use ASCII '--' instead.
💡 Suggested change
Before:
/* Different buffer — full clear (releases old pin) and acquire new */
After:
/* Different buffer -- full clear (releases old pin) and acquire new */
📄 src/backend/access/flux/flux_tuple.c (L603-L608)
Dropped-column read corruption. ALTER TABLE DROP COLUMN in PostgreSQL is a metadata-only operation (no table rewrite): the column is marked attisdropped while existing tuples keep their on-disk bytes. The form path and FluxComputeDataSize skip dropped attributes (no bytes written/reserved), which is correct only for tuples formed after the drop. But here the deform path skips the dropped attribute WITHOUT advancing data_ptr. For any tuple that was formed while the column was still live, its bytes are physically present on-page, so skipping without advancing desynchronizes data_ptr and misreads every subsequent attribute (garbage/crash). Heap's heap_deform_tuple does NOT special-case attisdropped for pointer advancement precisely for this reason; it advances based on the physical bytes. Confidence: high.
📄 src/backend/access/flux/flux_tuple.c (L1054-L1059)
Same dropped-column desynchronization bug as in FluxDeformTupleUpTo: this slot path also skips a dropped attribute without advancing data_ptr. A tuple physically containing a since-dropped column's bytes (the common case after metadata-only DROP COLUMN) will misread all following attributes. The read path must advance data_ptr past a dropped attribute's stored bytes when they are physically present, mirroring heap. Confidence: high.
📄 src/backend/access/flux/flux_tuple.c (L509-L513)
Dead scaffolding / latent footgun. is_compressed, is_overflowed, and work_values exist only to support compression/overflow that FLUX no longer implements: they are never set true, so this cleanup loop is dead code. Worse, work_values is a shallow memcpy of the caller's Datum pointers; if either flag were ever set (e.g. someone re-enables the disabled path), this loop would pfree caller-owned varlena pointers -> double-free/use-after-free. Per YAGNI, drop the tracking arrays and the work_values copy entirely and form directly from values. Confidence: high.
📄 src/backend/access/flux/flux_tuple.c (L408-L413)
Dead code: has_compressed is initialized to false and never set anywhere, so this block never executes and FLUX_TUPLE_COMPRESSED/FLUX_INFOMASK_COMPRESSED are never set. The (void) has_compressed; cast is a tell-tale of removed functionality. Remove the flag and the block (FLUX does not compress attributes, as the surrounding comments state). Confidence: high.
📄 src/backend/access/flux/flux_tuple.c (L281-L283)
Misleading documentation. This comment claims FluxFormTupleUpdate COW-references over-threshold varlenas against the OLD tuple's overflow chain using old_ovptrs/old_ovpresent. But flux_form_tuple_internal ignores both parameters entirely (FLUX has no on-page overflow; it TOASTs via the heap path). The comment describes behavior that does not exist, and old_ovptrs/old_ovpresent are dead parameters. Either remove the parameters and rewrite the comment to match the actual (TOAST-only) behavior. Confidence: high.
📄 src/backend/access/flux/flux_tuple.c (L839-L840)
Dead code (YAGNI): FluxPageGetLiveTuples has no callers anywhere in the tree, and the comment itself admits snapshot_ts is unused. Remove this helper (and its declaration in flux.h) until a caller exists. Confidence: high.
📄 src/backend/access/flux/flux_tuple.c (L769-L770)
Dead code (YAGNI): FluxPageUpdateTuple has no callers anywhere in the tree (the in-place UPDATE path in flux_operations.c does not use it). Beyond being unused, its in-place branch sets the item length via ItemIdSetNormal(itemid, ItemIdGetOffset(itemid), new_size) without MAXALIGN'ing new_size, which would violate the page's alignment invariant that FluxPageIndexTupleDelete/PageRepairFragmentation rely on (they assert offset == MAXALIGN(offset)). Recommend removing the function rather than shipping an unused, subtly-incorrect routine. Confidence: high on dead-code; high on the missing MAXALIGN if ever wired up.
📄 src/backend/access/flux/flux_undo.c (L6-L10)
This whole apply path appears to be dead code, and the header comment contradicts the actual design. flux_handler.c states verbatim: "FLUX's own row DML does not emit UNDO_RMID_FLUX records" -- row DML rolls back via the RELUNDO fork (relundo_apply.c), not this RM. A codebase-wide search finds no producer that ever writes FLUX_UNDO_INSERT/UPDATE/DELETE records; the only references are here. So apply_flux_undo_insert()/apply_flux_undo_restore_tuple() can never be reached, yet this header asserts "FLUX writes one UNDO record per tuple INSERT, UPDATE and DELETE." Per the minimalism/YAGNI and comment-accuracy rules, either wire up a producer or drop this unreachable RM; at minimum the header must be corrected to describe what actually happens. (high confidence)
📄 src/backend/access/flux/flux_undo.c (L8-L9)
The header (here and in the dispatch note) references a FLUX_UNDO_DELTA_UPDATE subtype and "reverse-diff reconstruction for DELTA_UPDATE", but no such subtype exists: flux_undo.h defines only FLUX_UNDO_INSERT/UPDATE/DELETE, and neither flux_undo_apply() nor flux_undo_desc() has a DELTA_UPDATE case (a DELTA_UPDATE record would hit the WARNING default in apply and print "UNKNOWN" in desc). This is aspirational/stale comment text describing behavior that does not exist. Remove the DELTA_UPDATE references or add the missing subtype and cases. (high confidence)
📄 src/backend/access/flux/flux_undo.c (L185-L188)
Buffer over-read/over-write on the shared page: this copies sizeof(FluxTupleHeader) (40 bytes) out of, and back into, the on-page slot without verifying ItemIdGetLength(lp) >= sizeof(FluxTupleHeader). A corrupted or short item at offnum leads to reading/writing past the tuple bounds on the buffer page. The restore-tuple helper guards its length (ItemIdGetLength(lp) < old_len), but the insert path has no such check. Add a length check before the memcpy and skip (return) if the slot is smaller than the header. (high confidence)
💡 Suggested change
Before:
len = ItemIdGetLength(lp);
slot = (char *) PageGetItem(page, lp);
START_CRIT_SECTION();
After:
len = ItemIdGetLength(lp);
if (len < sizeof(FluxTupleHeader))
{
ereport(WARNING,
(errmsg_internal("FLUX UNDO: item (%u, %u) shorter than tuple header (%zu < %zu), skipping",
blkno, offnum, len, sizeof(FluxTupleHeader))));
return;
}
slot = (char *) PageGetItem(page, lp);
START_CRIT_SECTION();
📄 src/backend/access/flux/flux_undo.c (L130-L132)
xlrec is a stack struct populated field-by-field with no prior memset, so the padding gap in xl_undo_apply (2 bytes before tuple_len, given UndoRecPtr(8)+TransactionId(4)+RelFileLocator(12)+BlockNumber(4)+OffsetNumber(2)+uint16+uint16 leaves tuple_len needing 4-byte alignment) is written to WAL as uninitialized stack garbage. That is nondeterministic WAL content, which trips wal_consistency_checking and is a portability concern since padding is platform-dependent. Zero-initialize the struct before filling it (e.g. xl_undo_apply xlrec = {0}; or MemSet(&xlrec, 0, sizeof(xlrec));). (moderate confidence)
💡 Suggested change
Before:
xlrec.urec_ptr = urec_ptr;
xlrec.xid = xid;
xlrec.target_locator = rel->rd_locator;
After:
MemSet(&xlrec, 0, sizeof(xlrec));
xlrec.urec_ptr = urec_ptr;
xlrec.xid = xid;
xlrec.target_locator = rel->rd_locator;
📄 src/backend/access/flux/flux_undo.c (L313-L316)
UndoRecPtr is typedef'd to uint64 (access/undodefs.h), so per project convention use UINT64_FORMAT rather than %llu with an (unsigned long long) cast. The cast-based form is a portability/style deviation. This applies to every urec_ptr/payload_len log site in this file. (moderate confidence)
📄 src/backend/access/flux/flux_undo.c (L26-L26)
The header claims "The callback mirrors heapam_undo.c's control flow", but there is no heapam_undo.c in this tree (heap/FLUX undo lives in relundo_apply.c / flux_relundo.c). Comments must describe reality; fix or drop this reference. (high confidence)
📄 src/backend/access/flux/flux_vm.c (L72-L74)
FluxVMInit is an exported no-op whose entire body is a comment. It has no callers anywhere in the tree (verified: only the declaration in flux.h and this definition exist). An exported empty function is speculative scaffolding that invites misuse (callers may assume it actually creates/initializes the VM fork, which it does not). Per YAGNI/minimalism, remove it (and its declaration in flux.h) until a real caller exists.
📄 src/backend/access/flux/flux_vm.c (L246-L248)
Comment/code drift: this claims "This function does not require any locks", but the body takes BUFFER_LOCK_SHARE on the VM buffer below. The PostgreSQL heap VM (visibilitymap_get_status) intentionally reads the VM byte without a content lock precisely because a stale/torn read is safe (callers recheck under the heap page lock). Either drop the SHARE lock to match the heap-VM contract and fix the comment, or keep the lock and correct the comment to describe reality. As written the comment is false.
📄 src/backend/access/flux/flux_vm.c (L344-L351)
TOCTOU / lock-leak footgun: the SHARE lock is taken and released here around a plain byte read, and the result is trusted by callers after the lock is dropped. If any error were ever raised between LockBuffer(SHARE) and LockBuffer(UNLOCK), the content lock would leak (there is no PG_TRY/finally; ResourceOwner releases the pin, not the LWLock). More importantly, the surrounding lock adds no correctness value over the heap-VM pattern (which reads the byte lock-free) since the caller acts on the result after unlocking. Recommend reading the byte without the content lock, matching visibilitymap.c, to avoid both the leak surface and the misleading interlock.
📄 src/backend/access/flux/flux_vm.c (L375-L375)
FluxVMPinBuffer calls flux_vm_readbuf(rel, mapBlock, true) with extend=true, so merely pinning the VM page for a heap block will create/extend the VM fork (relation extension + smgr inval) even on read-only/scan paths. Pinning for a check should never mutate the fork. This function also has no callers in the tree (verified). Either remove it as dead scaffolding, or if it is intended for read paths, pass extend=false and handle InvalidBuffer.
📄 src/backend/access/flux/flux_vm.c (L384-L386)
FluxVMExtend, FluxVMTruncate, FluxVMGetPageSize, and FluxVMMapHeapToVM have no callers anywhere in the tree (verified: only declarations in flux.h). This is dead/speculative code. In particular FluxVMTruncate uses FlushRelationBuffers(rel) which flushes ALL forks of the relation (heavier and different from visibilitymap_truncate, which is scoped to the VM fork) - shipping it unused hides that behavioral question. Remove until real callers exist.
📄 src/backend/access/flux/flux_vm.c (L551-L551)
Aspirational/future-tense comments and speculative scaffolding: the (void) tuple; cast exists only to silence an unused parameter that is 'reserved for future timestamp checking', and the surrounding comments ('A future optimization could...', 'For now, conservatively clear the bits') describe behavior that does not exist. PostgreSQL review discipline flags future-tense comments and unused 'flexibility'. Since the parameter is never used, drop it from the signature (and the flux.h declaration) and remove the aspirational comment; add the parameter back when the optimization is actually implemented.
📄 src/backend/access/flux/flux_vm.c (L619-L620)
Crash/replica-consistency ordering: FluxVMSet (called just above) WAL-logs and durably sets the VM ALL_VISIBLE/ALL_FROZEN bit, then here PD_ALL_VISIBLE is set on the heap page with only MarkBufferDirtyHint (not WAL-logged, and FluxVMSet registers the heap buffer REGBUF_NO_IMAGE|REGBUF_NO_CHANGE without bumping its LSN). Consequently, after crash/on a replica the VM bit can be durably set while the heap PD_ALL_VISIBLE is clear. This relaxes the standard heap-VM invariant (PD_ALL_VISIBLE must be set and crash-safe on the heap page whenever the VM ALL_VISIBLE bit is set). It is only safe if every reader treats the VM bit as authoritative and PD_ALL_VISIBLE strictly as a fast-path hint. Verify all scan/index-only paths honor that, and document the invariant here; otherwise an index-only scan could skip a heap fetch for a page whose visibility was never re-verified. Additionally, setting ALL_FROZEN implies ALL_VISIBLE - if a caller ever passes all_frozen=true with all_visible=false, FluxVMSet would set FROZEN without VISIBLE; consider asserting all_frozen implies all_visible.
📄 src/backend/access/hash/hashinsert.c (L248-L249)
Missing NULL guard on heapRel before calling RelationAmSupportsUndo(). That function unconditionally dereferences rel->rd_tableam, and UndoBufferIsActive() calls RelationGetRelid(rel) -- both crash if heapRel is NULL. The sibling nbtree hook added in this same patch guards with heaprel != NULL && RelationAmSupportsUndo(heaprel) and documents that "The heaprel parameter is NULL during index builds and recovery" (nbtinsert.c:1429-1432). This hash path is inconsistent and vulnerable to a NULL-pointer dereference. Add the same heapRel != NULL check. (high confidence)
💡 Suggested change
Before:
if (RelationAmSupportsUndo(heapRel) && UndoBufferIsActive(heapRel))
HashUndoLogInsert(rel, heapRel, buf, itup_off);
After:
if (heapRel != NULL && RelationAmSupportsUndo(heapRel) && UndoBufferIsActive(heapRel))
HashUndoLogInsert(rel, heapRel, buf, itup_off);
📄 src/backend/access/flux/flux_xlog.c (L2430-L2433)
The block-0 main-fork memcpy patches data_len bytes at data_offset into the on-page tuple slot without validating the range fits within the slot. data_offset and data_len are uint16 values read directly from the WAL record; on corrupt or mismatched WAL this is an out-of-bounds write on the shared buffer during redo (heap/page corruption on primary crash-recovery and standbys). The undo-side (block 1) in the FOLD variant validates its ranges extensively, so this main-fork path is also inconsistent. Add a bounds check before the memcpy, e.g. if ((uint32) xlrec->data_offset + xlrec->data_len > ItemIdGetLength(itemid)) elog(PANIC, ...), mirroring the file's stated redo PANIC policy.
💡 Suggested change
Before:
tuple = (FluxTupleHeader *) PageGetItem(page, itemid);
/* Patch the changed data bytes */
memcpy(((char *) tuple) + xlrec->data_offset, new_data, xlrec->data_len);
After:
tuple = (FluxTupleHeader *) PageGetItem(page, itemid);
/* Validate the patch range lies within the tuple slot */
if ((uint32) xlrec->data_offset + (uint32) xlrec->data_len >
ItemIdGetLength(itemid))
elog(PANIC, "FLUX CAS_UPDATE redo: patch range (offset %u + len %u) "
"exceeds tuple slot (%u) at offset %u",
xlrec->data_offset, xlrec->data_len,
(unsigned) ItemIdGetLength(itemid), xlrec->offnum);
/* Patch the changed data bytes */
memcpy(((char *) tuple) + xlrec->data_offset, new_data, xlrec->data_len);
📄 src/backend/access/flux/flux_xlog.c (L2485-L2488)
Same missing bounds validation as flux_xlog_cas_update_redo: this block-0 main-fork memcpy writes data_len bytes at data_offset into the tuple slot with no check that data_offset + data_len <= ItemIdGetLength(itemid). This is especially glaring here because block 1 (the relundo before-image) is guarded by a series of PANIC checks (urec_len, page_offset, new_pd_lower, page-end overflow) while the main-fork patch has none. On corrupt/mismatched WAL this is an out-of-bounds write during redo. Add the same slot-bounds guard before the memcpy.
💡 Suggested change
Before:
tuple = (FluxTupleHeader *) PageGetItem(page, itemid);
memcpy(((char *) tuple) + xlrec->data_offset, new_data, xlrec->data_len);
tuple->t_commit_ts = xlrec->new_commit_ts;
After:
tuple = (FluxTupleHeader *) PageGetItem(page, itemid);
if ((uint32) xlrec->data_offset + (uint32) xlrec->data_len >
ItemIdGetLength(itemid))
elog(PANIC, "FLUX CAS_UPDATE_UNDO redo: patch range (offset %u + len %u) "
"exceeds tuple slot (%u) at offset %u",
xlrec->data_offset, xlrec->data_len,
(unsigned) ItemIdGetLength(itemid), xlrec->offnum);
memcpy(((char *) tuple) + xlrec->data_offset, new_data, xlrec->data_len);
tuple->t_commit_ts = xlrec->new_commit_ts;
📄 src/backend/access/flux/flux_xlog.c (L1463-L1470)
The multi-insert redo cursor walk advances by SizeOfFluxMultiInsertTuple + thdr->datalen per tuple without ever bounding the cursor against the record's main-data length. ntuples (xlrec->ntuples) and each thdr->datalen are uint16 values taken directly from the WAL record, and palloc0(ntuples * sizeof(...)) plus the loop both trust them unchecked. A corrupt/mismatched record with an inflated ntuples or datalen over-reads past the main-data region during replay. The single-INSERT path validates its body length against XLogRecGetDataLen(record); the batch path should do the same. Compute the end of the per-tuple region ((char *) xlrec + XLogRecGetDataLen(record) minus any logical-image trailer) and PANIC if the cursor would advance past it before dereferencing thdr/copying body.
📄 src/backend/access/hash/hash_undo.c (L202-L209)
Stale-offset corruption hazard (high confidence). On abort this marks LP_DEAD at hdr.blkno/hdr.offset without verifying the item pointer still refers to the tuple this transaction inserted. Hash pages are physically compacted via PageIndexMultiDelete() in _hash_squeezebucket() (hashovfl.c) and hashbucketcleanup() (hash.c), which shifts the offsets of surviving tuples. Between insert and undo, hdr.offset can therefore point at an unrelated, live index entry; ItemIdIsNormal(lp) only proves the slot is in use, not that it is the same tuple. Marking the wrong entry LP_DEAD silently drops a valid index entry (index corruption / wrong query results). You must re-validate identity before killing the slot (e.g. re-check the heap TID / hash value carried in the tuple against the item at hdr.offset), or otherwise guarantee hash offsets are stable for the life of an uncommitted tuple.
📄 src/backend/access/hash/hash_undo.c (L208-L211)
MarkBufferDirty() and the CLR XLogInsert() run unconditionally, even when ItemIdIsNormal(lp) is false and no page change was made. That dirties a buffer and emits a compensation WAL record for a no-op, wasting WAL and buffer writes on every such record. Move MarkBufferDirty()/XLogInsert() inside the ItemIdIsNormal(lp) branch so WAL is only generated when the page actually changed.
📄 src/backend/access/hash/hash_undo.c (L99-L100)
Dead code / unwired scaffolding (medium confidence). The only caller, _hash_doinsert() in hashinsert.c, gates this call with RelationAmSupportsUndo(heapRel) && UndoBufferIsActive(heapRel). UndoBufferIsActive(heapRel) is therefore always true here, so the else (non-piggyback UndoRecordSetCreate/Insert/Free) branch is unreachable, and xid is computed but only used in that dead branch. Drop the unreachable else branch and the now-unused xid local (YAGNI), or move the gating so both paths are actually reachable.
📄 src/backend/access/hash/hash_undo.c (L18-L19)
Comment does not match current behavior. The header claims hooks are gated solely by RelationAmSupportsUndo(heapRel), but the actual call site in hashinsert.c gates on RelationAmSupportsUndo(heapRel) && UndoBufferIsActive(heapRel), and the gating lives at the call site, not "all hooks" in this file. Update the comment to describe what the code does now.
📄 src/backend/access/nbtree/nbtinsert.c (L1432-L1436)
This UNDO call only executes on the no-split code path (the else branch of if (PageGetFreeSpace(page) < itemsz)). When the insert forces a page split, the tuple is inserted inside _bt_split() and control never reaches here, so no nbtree UNDO record is written for split-inducing inserts. _bt_split() contains no NbtreeUndoLogInsert call. On abort, index entries inserted via a page split will not be cleaned up, defeating the stated purpose ("enables cleanup of this index entry if the transaction aborts"). The split path must also emit UNDO. [correctness, high confidence]
📄 src/backend/access/nbtree/nbtinsert.c (L1434-L1435)
The posting-list-split case (postingoff != 0) is not handled correctly. On that path itup was already replaced by a mutable copy whose TID was swapped via _bt_swap_posting(), and the existing posting tuple at the original offset was overwritten in place with nposting (fewer TIDs). This call records only the newly inserted entry; the UNDO apply (nbtree_undo_apply, NBTREE_UNDO_INSERT_LEAF) merely marks that single item LP_DEAD. It does not restore oposting, so on abort the heap TID moved out of the original posting tuple is lost from the index (index corruption). The posting-split case needs its own UNDO handling (e.g. record oposting for restoration), or must be logged distinctly. [correctness, high confidence]
📄 src/backend/access/nbtree/nbtinsert.c (L1432-L1432)
Inconsistent gating versus the hash AM. hashinsert.c gates on RelationAmSupportsUndo(heapRel) && UndoBufferIsActive(heapRel), while here the UndoBufferIsActive case is handled inside NbtreeUndoLogInsert (it falls back to creating a standalone UndoRecordSet). If both AMs are meant to share the same UNDO lifecycle, confirm this divergence is intentional; the standalone-UndoRecordSet fallback creates a separate transaction UNDO set path that hash never takes. [maintainability, moderate confidence]
📄 src/backend/access/rmgrdesc/atmdesc.c (L38-L45)
The switch in atm_desc() has no default case. For any info value other than XLOG_ATM_ABORT/XLOG_ATM_FORGET (e.g. corrupted or forward-version WAL in pg_waldump), nothing is appended to buf, yielding empty/misleading descriptor output with no indication the record type was unrecognized. This is inconsistent with the same subsystem: atm_redo() in atm.c uses default: elog(PANIC, ...), and sibling descriptor files (undodesc.c, relundodesc.c) always handle a default case. Add a default branch that reports the unknown op code.
💡 Suggested change
Before:
case XLOG_ATM_FORGET:
{
xl_atm_forget *xlrec = (xl_atm_forget *) data;
appendStringInfo(buf, "xid %u", xlrec->xid);
}
break;
}
After:
case XLOG_ATM_FORGET:
{
xl_atm_forget *xlrec = (xl_atm_forget *) data;
appendStringInfo(buf, "xid %u", xlrec->xid);
}
break;
default:
appendStringInfo(buf, "UNKNOWN");
break;
}
📄 src/backend/access/nbtree/nbtree_undo.c (L460-L463)
Buffer-overflow / data-corruption hazard (high confidence). The DEDUP undo restores a full page image via memcpy(page, payload + SizeOfNbtreeUndoDedup, hdr.page_len), but hdr.page_len (a uint16 read from the on-disk UNDO payload) is never validated. payload_len is only checked against SizeOfNbtreeUndoDedup, not against SizeOfNbtreeUndoDedup + hdr.page_len, and there is no check that hdr.page_len == PageGetPageSize(page) (i.e. BLCKSZ). A truncated/corrupt record, or a page_len larger than the target buffer page, overflows the shared buffer page inside a critical section -> memory corruption + PANIC. Validate both bounds before the memcpy and treat a mismatch as UNDO_APPLY_ERROR.
💡 Suggested change
Before:
/* Restore the full pre-dedup page image */
memcpy(page,
payload + SizeOfNbtreeUndoDedup,
hdr.page_len);
After:
if (payload_len < SizeOfNbtreeUndoDedup + hdr.page_len ||
hdr.page_len != PageGetPageSize(page))
{
UnlockReleaseBuffer(buffer);
relation_close(indexrel, RowExclusiveLock);
return UNDO_APPLY_ERROR;
}
/* Restore the full pre-dedup page image */
memcpy(page,
payload + SizeOfNbtreeUndoDedup,
hdr.page_len);
📄 src/backend/access/nbtree/nbtree_undo.c (L369-L370)
Index-corruption hazard (high confidence). The item at hdr.offset is marked LP_DEAD based only on P_ISLEAF + offset <= PageGetMaxOffsetNumber + ItemIdIsNormal. There is no check that this item is actually the tuple this transaction inserted. hdr.offset was captured at insert time, but the same-transaction (or concurrent) page can be compacted, split, or deduplicated before undo runs, so the offset may now address an unrelated live index entry. Marking it dead silently drops a valid index pointer -> wrong query results / index corruption. The full IndexTuple is already stored in the payload; compare it against the item at hdr.offset (and/or match the heap TID) before marking LP_DEAD, and skip if it does not match.
📄 src/backend/access/nbtree/nbtree_undo.c (L87-L87)
Footgun macro (medium confidence). Unlike every other SizeOf* macro in the tree (e.g. nbtxlog.h, heapam_xlog.h, brin_xlog.h all wrap (offsetof(...) + sizeof(...))), this definition is unparenthesized. It is already reused in mixed arithmetic (payload_size = SizeOfNbtreeUndoDedup + page_size, payload + SizeOfNbtreeUndoDedup) and in comparisons (payload_len < SizeOfNbtreeUndoInsertLeaf); any future use in a context binding tighter than + would silently compute the wrong size and cause payload under/over-reads. Wrap the expression in parentheses. Same issue for SizeOfNbtreeUndoInsertUpper, SizeOfNbtreeUndoDedup and SizeOfNbtreeUndoDelete below.
💡 Suggested change
Before:
#define SizeOfNbtreeUndoInsertLeaf offsetof(NbtreeUndoInsertLeaf, itup_sz) + sizeof(Size)
After:
#define SizeOfNbtreeUndoInsertLeaf (offsetof(NbtreeUndoInsertLeaf, itup_sz) + sizeof(Size))
📄 src/backend/access/nbtree/nbtree_undo.c (L102-L102)
Unparenthesized SizeOf* macro (see the same issue on SizeOfNbtreeUndoInsertLeaf). Wrap in parentheses.
💡 Suggested change
Before:
#define SizeOfNbtreeUndoInsertUpper offsetof(NbtreeUndoInsertUpper, itup_sz) + sizeof(Size)
After:
#define SizeOfNbtreeUndoInsertUpper (offsetof(NbtreeUndoInsertUpper, itup_sz) + sizeof(Size))
📄 src/backend/access/nbtree/nbtree_undo.c (L115-L115)
Unparenthesized SizeOf* macro. This one is especially risky because it participates in pointer arithmetic (payload + SizeOfNbtreeUndoDedup) and size arithmetic (SizeOfNbtreeUndoDedup + page_size). Wrap in parentheses.
💡 Suggested change
Before:
#define SizeOfNbtreeUndoDedup offsetof(NbtreeUndoDedup, page_len) + sizeof(uint16)
After:
#define SizeOfNbtreeUndoDedup (offsetof(NbtreeUndoDedup, page_len) + sizeof(uint16))
📄 src/backend/access/nbtree/nbtree_undo.c (L128-L128)
Unparenthesized SizeOf* macro (same class as above). Wrap in parentheses.
💡 Suggested change
Before:
#define SizeOfNbtreeUndoDelete offsetof(NbtreeUndoDelete, ndeleted) + sizeof(uint16)
After:
#define SizeOfNbtreeUndoDelete (offsetof(NbtreeUndoDelete, ndeleted) + sizeof(uint16))
📄 src/backend/access/nbtree/nbtree_undo.c (L250-L254)
Dead code + inaccurate comment (medium confidence). NbtreeUndoLogDedup has no caller anywhere in the tree (only this definition and its prototype in nbtree.h); _bt_dedup_pass does not call it. So the comment "Called from _bt_dedup_pass() before the page is modified" describes behavior that does not exist, and the entire NBTREE_UNDO_DEDUP log path (plus its apply case) is unreachable scaffolding. Per YAGNI, either wire this up in the dedup path or drop the function, the DEDUP subtype, and its apply case until it is actually used. Note the buffer-overflow finding above becomes moot if this is removed.
📄 src/backend/access/nbtree/nbtree_undo.c (L264-L265)
palloc of a full BLCKSZ buffer is not freed on the error path (low/medium confidence). If UndoRecordSetCreate/UndoRecordAddPayload/UndoRecordSetInsert ereport(ERROR), the payload allocation leaks until the surrounding MemoryContext is reset. Either allocate in a short-lived context that is guaranteed to be reset, or wrap in PG_TRY/PG_FINALLY to pfree on error. (Only relevant if this function is actually wired up, see the dead-code comment above.)
📄 src/backend/access/nbtree/nbtree_undo.c (L515-L521)
Aspirational / inaccurate comment (low confidence). The file header lists DELETE among implemented subtypes ("re-insert deleted tuples"), but the apply path only returns UNDO_APPLY_SKIPPED for DELETE, and uses 'for now' language. Per the project's comment-accuracy rule, comments must describe current behavior, not future intent. Reword to state that DELETE (and INSERT_POST/SPLIT/NEWROOT) undo is currently a no-op and the entries are handled elsewhere; drop the 'for now'/'eventually' phrasing.
📄 src/backend/access/rmgrdesc/relundodesc.c (L94-L95)
Portability bug: urec_ptr is RelUndoRecPtr, a typedef uint64 (see relundo_xlog.h). Casting a 64-bit value to unsigned long and printing with %lu truncates on LLP64 platforms (Windows/MSVC), where long is 32-bit, producing wrong pg_waldump output. Use UINT64_FORMAT and cast to uint64 instead.
💡 Suggested change
Before:
appendStringInfo(buf, "urec_ptr %lu",
(unsigned long) xlrec->urec_ptr);
After:
appendStringInfo(buf, "urec_ptr " UINT64_FORMAT,
(uint64) xlrec->urec_ptr);
📄 src/backend/access/rmgrdesc/relundodesc.c (L44-L57)
DRY/maintainability hazard: this hardcodes the urec_type -> name mapping with magic integer literals (1/2/3/4), duplicating the canonical RelUndoRecordType enum in relundo.h (RELUNDO_INSERT=1, RELUNDO_DELETE=2, RELUNDO_UPDATE=3, RELUNDO_TUPLE_LOCK=4). If the enum ever changes, this table silently drifts and misreports record types. Reference the enum constants (e.g. case RELUNDO_INSERT:) instead of raw literals.
📄 src/backend/access/rmgrdesc/relundodesc.c (L103-L107)
The switch has no default case. For an unknown/unhandled info value (a future record type or a corrupted record encountered during forensic pg_waldump use), buf is left untouched and empty output is produced silently. The redo path (relundo_redo) handles this with default: elog(PANIC, ...). Consider adding a default that appends a diagnostic (e.g. the raw info byte) so waldump output is never silently empty.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L54-L60)
These local '_fe' struct duplicates do not match the actual on-disk WAL record layouts, so pg_waldump will misparse FLUX records.
Authoritative layouts (src/include/access/flux_xlog.h, backend #ifndef FRONTEND branch, and the write paths in flux_xlog.c / flux_handler.c):
- xl_flux_insert = {offnum(2), flags(2), tuple_len(uint32), commit_ts(uint64)}. FluxXLogInsert() sets xlrec.tuple_len and has NO xact_ts. But xl_flux_insert_fe here has {offnum, flags, commit_ts, xact_ts} -- it drops tuple_len and invents xact_ts, so the memcpy reads garbage and prints wrong commit_ts/xact_ts.
- xl_flux_delete: same problem (real record has tuple_len(uint32), no xact_ts).
- xl_flux_update: real record has old_tuple_len/new_tuple_len/dst_block_id, no xact_ts.
- xl_flux_lock = {offnum(2), flags(2), infomask(uint8), lock_mode(uint8)} = 6 bytes (see flux_handler.c:3044-3052). xl_flux_lock_fe adds nonexistent xmax(uint32)/infomask2(uint16) and widens infomask to uint16, so 'datalen >= sizeof(...)' is almost always false -> always prints 'lock (truncated)', or reads out of bounds.
- xl_flux_multi_insert lacks xact_ts in the real record too.
Root cause and fix: flux_xlog.h ALREADY defines frontend-safe copies of these structs under '#else /* FRONTEND /' and already declares flux_desc()/flux_identify() as frontend-safe prototypes. This whole file should '#include "access/flux_xlog.h"' and use those shared structs and the shared XLOG_FLUX_ / FLUX_WAL_* macros, rather than re-declaring drifted local copies. As written, the two definitions have already diverged -- the exact footgun the duplication creates.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L28-L30)
The XLOG_FLUX_* opcodes and FLUX_WAL_CROSS_PAGE flag are copy-pasted here with a 'keep in sync with flux_xlog.h' comment. flux_xlog.h defines these outside the '#ifndef FRONTEND' guard (lines 63-98), i.e. they are already frontend-includable. Duplicating them means new/changed opcodes silently print 'UNKNOWN' or misidentify with no compile-time check. Include the header and delete these local copies.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L391-L393)
The CAS_UPDATE branch hard-codes byte offsets (data, data+4, data+6) and a magic minimum length of 14 with a comment about '2 padding', encoding struct-padding assumptions into the parser. This is non-portable across ABIs where the padding differs. The real record is xl_flux_cas_update = {offnum(2), flags(2), data_offset(2), data_len(2), new_commit_ts(uint64)} (flux_xlog.h:225-233). Parse it via the matching (frontend-safe) struct like the other branches instead of manual byte indexing; note data_offset is at offset 4 only if there is no padding after the two leading uint16s, which is exactly the assumption that breaks portability.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L403-L403)
The '(int)' cast on sizeof (a Size/unsigned) is unnecessary and inconsistent with every other branch, which compares datalen against sizeof(...) directly. It also reintroduces a signed/unsigned comparison. Drop the cast for consistency: 'if (datalen >= sizeof(xl_flux_cas_update_undo_fe))'.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L164-L166)
flux_comp_type_name maps bare integer literals 0-4 to compression names. If the backend FluxCompressionType enum is reordered/extended, this silently misreports. Reference the shared enum values (or document why the frontend cannot include them) to keep them in sync.
📄 src/backend/access/transam/rmgr.c (L43-L45)
These new includes break the alphabetical ordering of the existing include group (which runs brin_xlog.h ... xact.h, then catalog/, commands/, replication/, storage/, utils/). Appending them after utils/relmapper.h violates both the grouping and the sort. Interleave them alphabetically among the other access/* headers (e.g. atm.h after access/xlog_internal grouping, relundo_xlog.h/undo_xlog.h in order) so the block stays pgindent/convention clean and reads as if always written that way. (low confidence on exact committer preference, but the group is clearly sorted.)
📄 src/backend/access/transam/rmgr.c (L44-L44)
rmgrlist.h only needs atm_redo/atm_desc/atm_identify, which are declared in the frontend-safe access/atm_xlog.h (atm.h's own comment notes atm_xlog.h is the minimal header and includes it transitively). Including the full access/atm.h here pulls in the backend-only ATM API plus lwlock.h/timestamp.h that this rmgr symbol table does not use, and is inconsistent with the other entries here which all include the lightweight *_xlog.h. Prefer access/atm_xlog.h for a minimal include.
💡 Suggested change
Before:
#include "access/atm.h"
After:
#include "access/atm_xlog.h"
📄 src/backend/access/rmgrdesc/undodesc.c (L71-L81)
Bug: the operation_type decoding is wrong. The only defined subtypes (flux_undo.h) are FLUX_UNDO_INSERT=0x0001, FLUX_UNDO_UPDATE=0x0002, FLUX_UNDO_DELETE=0x0003, and xl_undo_apply.operation_type is set to exactly these (flux_undo.c). Here 0x0002 is decoded as DELETE and 0x0003 as UPDATE -- they are swapped -- and 0x0004-0x0006 (PRUNE/INPLACE/HOT_UPDATE) do not exist as subtypes and can never occur. pg_waldump will label UPDATE undo records as DELETE and vice versa. Use the named FLUX_UNDO_* constants and drop the nonexistent cases.
💡 Suggested change
Before:
switch (xlrec->operation_type)
{
case 0x0001:
op_name = "INSERT";
break;
case 0x0002:
op_name = "DELETE";
break;
case 0x0003:
op_name = "UPDATE";
break;
After:
switch (xlrec->operation_type)
{
case FLUX_UNDO_INSERT:
op_name = "INSERT";
break;
case FLUX_UNDO_UPDATE:
op_name = "UPDATE";
break;
case FLUX_UNDO_DELETE:
op_name = "DELETE";
break;
📄 src/backend/access/rmgrdesc/undodesc.c (L37-L41)
Portability: start_ptr is UndoRecPtr (typedef uint64). PostgreSQL convention for uint64/UndoRecPtr is the UINT64_FORMAT macro, not a cast to (unsigned long long) with %llu (sibling descriptors such as fluxdesc.c use UINT64_FORMAT). The same applies to discard_ptr, new_size, old_seal_ptr, and urec_ptr below. Please switch all of these to UINT64_FORMAT and drop the (unsigned long long) casts.
💡 Suggested change
Before:
appendStringInfo(buf, "log %u, start %llu, len %u, xid %u",
xlrec->log_number,
(unsigned long long) xlrec->start_ptr,
xlrec->length,
xlrec->xid);
After:
appendStringInfo(buf, "log %u, start " UINT64_FORMAT ", len %u, xid %u",
xlrec->log_number,
xlrec->start_ptr,
xlrec->length,
xlrec->xid);
📄 src/backend/access/transam/twophase_rmgr.c (L17-L18)
Include ordering: access/flux.h should be placed before access/multixact.h. PostgreSQL convention (enforced at commit) is alphabetical ordering of #include directives within a group, and flux sorts before multixact. Confidence: high.
💡 Suggested change
Before:
#include "access/multixact.h"
+#include "access/flux.h"
After:
#include "access/flux.h"
#include "access/multixact.h"
📄 src/backend/access/transam/xlog.c (L7959-L7962)
This per-phase checkpoint timing instrumentation is unrelated to the UNDO/ATM feature this patch implements. It bundles a separate diagnostic change into a feature patch (a top rejection reason on -hackers: a patch that does more than one thing). It also adds a second, always-on LOG line for every checkpoint when log_checkpoints is enabled, on top of the existing LogCheckpointEnd() call immediately below (line 7972) which already reports write/sync/total timing. This increases log volume and maintenance burden. Additionally, the variables are named *_ms (milliseconds) but printed as %.3f s after dividing by 1000.0, which is misleading during maintenance. Recommend dropping this instrumentation from this patch (or splitting it into its own patch); if genuinely needed, fold it into the existing checkpoint statistics rather than emitting a separate line.
📄 src/backend/access/transam/xlogrecovery.c (L1880-L1884)
Correctness (high confidence): both undo phases run at the end of the redo loop unconditionally, but this point is reached even in the archive/base-backup recovery case where WAL ran out before the consistent recovery point. In that scenario StartupXLOG() only FATALs after PerformWalRecovery() returns (xlog.c: "WAL ends before consistent recovery point"). By then PerformUndoRecovery()/PerformRelUndoRecovery() have already reverse-applied before-images destructively and without emitting WAL, using a CLOG that is not yet consistent -- so "loser" classification is unreliable and the on-disk data is corrupted before recovery is even declared failed. Note the reachedRecoveryTarget branch above already guards this via if (!reachedConsistency) ereport(FATAL...), but the normal end-of-WAL exit does not. Because plain crash recovery never sets reachedConsistency, the correct gate is the archive-recovery underrun case, e.g. bail out (or skip undo) when ArchiveRecoveryRequested && !reachedConsistency, mirroring the existing consistency check.
📄 src/backend/access/transam/xlogrecovery.c (L36-L37)
Minimal-diff: access/undolog.h is added here but no symbol it declares (UndoLog*, UndoRecPtr macros, undo_* GUCs, etc.) is referenced anywhere in this file. The undo-phase calls resolve via access/undo_xlog.h (UndoRecoveryNeeded/PerformUndoRecovery) and access/relundo.h (PerformRelUndoRecovery). Drop the unused include.
💡 Suggested change
Before:
+#include "access/undo_xlog.h"
+#include "access/undolog.h"
After:
+#include "access/undo_xlog.h"
📄 src/backend/access/transam/xact.c (L2775-L2778)
This comment is contradicted by the code it guards. XactUndoHasUnrecoverableUndo() returns false unconditionally, and its own doc states both cluster-wide and per-relation UNDO are now 2PC-recoverable (last_batch_lsn saved in xl_xact_prepare and fed to ATMAddAborted on ROLLBACK PREPARED; per-relation chains serialized via RegisterTwoPhaseRecord). So this guard never fires and the elaborate claim that "nothing on the COMMIT PREPARED / ROLLBACK PREPARED path ... ever reads it back" and that "Silently proceeding would make ROLLBACK PREPARED a no-op ... corrupting data" describes the opposite of current behavior. Rewrite the comment to match what the code does now (a preserved choke point that currently accepts all UNDO), or remove the stale rationale entirely.
📄 src/backend/access/transam/xact.c (L338-L341)
These four hooks are defined here and set to NULL, but they are never assigned and never invoked anywhere in the tree (verified across src/backend and src/include). This is dead scaffolding (YAGNI). Either wire up the invocation/assignment sites this patch needs, or drop the four PendingPhysOps*_hook definitions (and their header externs) until a caller exists.
📄 src/backend/access/transam/xact.c (L3046-L3047)
This comment contradicts the code added a few lines below. It claims "Physical UNDO application is NOT needed during standard transaction abort", but AtAbort_XactUndo() (line 3098) and WaitForPendingRelUndo() (line 3174) do apply per-relation UNDO synchronously during exactly this abort path. A reader following this comment would wrongly conclude the abort performs no compensation. Reword to reflect that per-relation UNDO is in fact applied here, and clarify that zeroing s->undoRecPtr only clears the chain-head tracking (which AtAbort_XactUndo does not read; it uses XactUndo.last_batch_lsn).
📄 src/backend/access/undo/atm.c (L78-L85)
On-disk format portability hazard. AtmStateRecord (and AtmStateHeader) are written/read as raw C structs via write()/read(), and the CRC is computed over the padded struct bytes (COMP_CRC32C(crc, &rec, sizeof(rec))). This depends on struct padding, bool width/representation, and field alignment, all of which are compiler/ABI-specific. The file is not portable across architectures/compilers, and even on one platform a padding-byte change between builds can flip the CRC. Per the portability hard gate, on-disk formats must not depend on struct padding or bool width. Serialize explicit fixed-width fields (e.g. write xid/oids as uint32, last_batch_lsn as uint64, revert_complete as a uint8) into a packed buffer, or memset the record to zero the padding before writing and CRC-ing. Compare ReadTwoPhaseFile/RecreateTwoPhaseFile which serialize an explicit layout.
📄 src/backend/access/undo/atm.c (L500-L502)
Unvalidated count from an on-disk file drives allocation before the CRC is checked. hdr.count is read straight from the file and used to palloc(hdr.count * sizeof(AtmStateRecord)) here, but the CRC verification happens only after reading records (below). A corrupt/hostile count therefore triggers a huge palloc (OOM/abort) in the startup path, before any integrity check. On 32-bit builds the multiply can also overflow Size. Model this on ReadTwoPhaseFile: bound the size via fstat and reject when it exceeds MaxAllocSize (and the multiply against overflow) before allocating.
📄 src/backend/access/undo/atm.c (L147-L148)
Stale/misleading comment. It claims ATMAddAborted "Returns false if the sLog is full, signaling the caller to fall back to synchronous rollback." But the underlying SLogTxnInsert is documented to "never fail for lack of space; the return type is retained for API compatibility and is always true" (slog.c). ATMAddAborted can therefore never return false, so the described fallback is dead. Update the comment to describe current behavior, and consider whether the bool return (and the now-unreachable "ATM full" fallbacks in callers) should be dropped.
📄 src/backend/access/undo/logical_revert_worker.c (L235-L242)
Starvation/livelock in multi-database clusters (high confidence). ATMGetNextUnreverted -> SLogTxnGetNextUnreverted iterates the ATM in ascending xid order and always returns the globally-oldest unreverted entry; it neither filters by database nor advances a cursor. When that oldest entry belongs to another database, this worker goto sleep, and on every subsequent wakeup it re-discovers the same foreign entry and sleeps again -- it never reaches its own database's entries as long as an older foreign entry exists. Since only the launcher's connected DB (and any DB whose entry happens to be oldest) gets serviced, aborted-transaction UNDO for other databases is never applied, breaking the guaranteed-rollback invariant. The ATM lookup must accept a database filter (e.g. pass MyDatabaseId down to SLogTxnGetNextUnreverted) so the worker retrieves the oldest entry for its own database.
📄 src/backend/access/undo/logical_revert_worker.c (L324-L329)
Committing an errored transaction (high confidence). After PG_CATCH catches an error mid-application, control falls through to CommitTransactionCommand() -- it commits a transaction that aborted internally, instead of aborting it. The sibling worker in undoworker.c uses the correct pattern: if (IsTransactionState()) AbortCurrentTransaction(); followed by LWLockReleaseAll(); UnlockBuffers(); AtEOXact_Buffers(false); .... Here the catch block does none of that, so any LWLock/buffer pin/fd held at the point of failure leaks, and the subsequent commit runs commit-time processing on inconsistent state. Replace the fall-through commit with an abort in the catch path (and release locks/buffers), committing only on the success path.
📄 src/backend/access/undo/logical_revert_worker.c (L369-L370)
Convention: avoid unbounded sprintf (moderate confidence). Use snprintf with the correct buffer bound, consistent with LogicalRevertLauncherRegister below (which already uses snprintf) and the rest of the tree (launcher.c, autovacuum.c). Although the literals are fixed and cannot overflow today, the project rule is to never use sprintf for bgw_* fields.
💡 Suggested change
Before:
sprintf(worker.bgw_library_name, "postgres");
sprintf(worker.bgw_function_name, "LogicalRevertWorkerMain");
After:
snprintf(worker.bgw_library_name, MAXPGPATH, "postgres");
snprintf(worker.bgw_function_name, BGW_MAXLEN, "LogicalRevertWorkerMain");
📄 src/backend/access/undo/logical_revert_worker.c (L501-L501)
Robustness: hardcoding the "postgres" database (moderate confidence). If the postgres database has been dropped, this connection fails, the launcher never starts, and the entire logical-revert subsystem silently stops -- breaking the guaranteed-rollback guarantee for every database. The autovacuum launcher deliberately avoids binding to a named database. Prefer a shared-catalog-only connection (BackgroundWorkerInitializeConnection(NULL, NULL, BGWORKER_BYPASS_ALLOWCONN) style, or a connection that does not depend on a specific user DB) so a pg_database scan is always possible.
📄 src/backend/access/undo/logical_revert_worker.c (L577-L581)
Stale narrative comment (low confidence). This comment references removed history ("the original one-shot design"); comments should describe current behavior, not the evolution of the code. Drop the comparison and just state the interval and rationale.
📄 src/backend/access/undo/README (L642-L646)
These documented GUC defaults do not match the code. In undolog.c, undo_worker_naptime defaults to 10000 (10s) and undo_retention_time defaults to 60000 (60s), not 60000/3600000 as stated here. (Note also that postgresql.conf.sample shows undo_retention_time = 300s, so the sources already disagree among themselves.) Correct the README to the actual code defaults.
💡 Suggested change
Before:
undo_worker_naptime Sleep interval between discard cycles (ms)
Default: 60000 (1 minute)
undo_retention_time Minimum retention time for UNDO records (ms)
Default: 3600000 (1 hour)
After:
undo_worker_naptime Sleep interval between discard cycles (ms)
Default: 10000 (10 seconds)
undo_retention_time Minimum retention time for UNDO records (ms)
Default: 60000 (60 seconds)
📄 src/backend/access/undo/README (L1135-L1135)
Stale file citation: there is no undo_flush.c in the tree (the flush/write buffer described elsewhere in this README lives in undobuffer.c). This lock-ordering table gives a false source location for a deadlock-safety invariant, which is worse than no citation. Point Level 4 at the real file/line, or drop the line reference.
📄 src/backend/access/undo/README (L1122-L1122)
Non-ASCII characters violate the project's ASCII-only rule for source/docs: this line uses a Unicode em-dash. Replace with a plain ASCII '--'.
💡 Suggested change
Before:
locks sit outside this hierarchy — they must be acquired before any
After:
locks sit outside this hierarchy -- they must be acquired before any
📄 src/backend/access/undo/README (L1142-L1142)
Non-ASCII characters: this line uses a Unicode arrow (U+2192). Replace with ASCII '->'. (git diff --check/pgindent conventions require ASCII-only.)
💡 Suggested change
Before:
- The revert worker acquires locks 2→5 in sequence during UNDO apply;
After:
- The revert worker acquires locks 2->5 in sequence during UNDO apply;
📄 src/backend/access/undo/README (L755-L759)
This 'Modified Core Files' list is inaccurate for this change. None of heapam.c, heapam_handler.c, or pruneheap.c are modified in this patch (they have no diff), and RelationHasUndo() does not exist anywhere in the tree. The in-place-update AM being added here is the flux AM (src/backend/access/flux/*), not the heap. Likewise ipci.c is not modified (see below). List the files this patch actually touches, or the README documents a code layout that isn't present.
📄 src/backend/access/undo/README (L764-L764)
RelationHasUndo() is not declared in heapam.h (or anywhere in the tree); this reference and the heapam.c reference above are stale. Remove or replace with the actual helper/API used by the flux AM.
📄 src/backend/access/undo/README (L341-L342)
Inaccurate initialization flow: ipci.c does not call UndoShmemSize()/UndoShmemInit() (it is not modified by this patch and contains no such call). The undo shared memory is wired via undo.c's shmem-request/init hook (UndoShmemInit_internal registered as .init_fn), and per-backend InitializeUndo() is invoked from postinit.c, not ipci.c. Update this section to describe the actual hook-based wiring.
📄 src/backend/access/undo/README (L762-L762)
ipci.c is not modified by this patch and does not perform UNDO shared-memory initialization (see the Initialization Flow note). Remove this line or replace it with the file that actually wires undo shmem (undo.c's shmem hook; per-backend InitializeUndo() from postinit.c).
📄 src/backend/access/undo/README (L824-L824)
This benchmark block references HEAP_UNDO_DELETE_VISIBILITY_ONLY, a symbol that does not exist anywhere in the tree, and attributes results to a heap-integration path (heapam.c DELETE UNDO) that this patch does not add -- the AM here is flux, not heap. Hard-coded, dated benchmark numbers describing a different/earlier code layout will rot immediately and are exactly what -hackers rejects. Either back these with a reproducible benchmark against the code actually being committed, or drop the specific numbers/symbol from the README.
📄 src/backend/access/undo/README (L638-L638)
Aspirational/future-tense content in a shipping design doc. Per the project's comment-accuracy rules, describe what the code does now. If physical truncation/deletion of reclaimed log files is not implemented, state that reclaimed space is not yet physically truncated rather than embedding a 'Future:' TODO in the numbered behavior list.
📄 src/backend/access/undo/relundo.c (L138-L138)
Stale static-state leak across an error window. relundo_pending_metabuf is a file-static that holds a locked+pinned metapage from RelUndoReserve() (when a new page is allocated) until RelUndoFinish()/RelUndoFinishWithTuple()/RelUndoCancel() consumes it. If the caller's intervening DML throws ereport/elog(ERROR) between reserve and finish/cancel, the resource owner releases the buffer's lock/pin on abort, but this static is NOT reset. The flux update path (flux_operations.c) has a real error window here: after RelUndoReserve() it runs repalloc(), CheckForSerializableConflictIn(), and FluxXLogPrepareLogicalImage() (all can throw) before RelUndoFinish(), and its PG_CATCH does not reset this static or call RelUndoCancel(). The next same-backend RelUndoReserve() that allocates a page then adopts a stale/dangling buffer (the Assert(BufferIsValid(...)) passes on the dead index), risking a wrong metapage in a WAL record or operating on an already-released/reused buffer. Register a subxact/xact-abort or resource-owner cleanup that resets relundo_pending_metabuf to InvalidBuffer, or restructure so the pending metapage cannot survive an error.
📄 src/backend/access/undo/relundo.c (L493-L494)
Leftover DEBUG1 instrumentation on a documented hot path. RelUndoReserve() and RelUndoFinishWithTuple() emit many per-operation elog(DEBUG1, ...) calls (including per-tuple details), which contradicts this file's own hot-path contention claims: even gated at DEBUG1 the call sites evaluate arguments and incur call overhead per reserve/finish. Remove this development instrumentation before commit.
📄 src/backend/access/undo/relundo.c (L865-L866)
Format-specifier truncation on LLP64 (Windows/MSVC). RelUndoRecPtr is a 64-bit type (uint64), but this logs it via (unsigned long) with %lu; unsigned long is 32-bit on LLP64, silently truncating the pointer in the log. Use UINT64_FORMAT and drop the cast (or better, remove this DEBUG1 line entirely per the hot-path note).
📄 src/backend/access/undo/relundo.c (L1334-L1339)
Dead speculative code (YAGNI). RelUndoDropRelation() has no callers anywhere in the tree; its body performs a side-effecting smgrexists() probe (which per this file's own relundo_fork_nblocks_fast comment closes/reopens the fork fd) and then does nothing, ending with an aspirational "If in the future..." comment. Remove the function (and its declaration in relundo.h), or wire it up. As written it is scaffolding with a cost and no effect.
📄 src/backend/access/undo/relundo.c (L149-L149)
Hash shift constant is not derived from the cache size. relundo_oid_hash hardcodes >> (32 - 6), where 6 == log2(64) is tied to RELUNDO_HEAD_CACHE_SIZE. The cache comment explicitly invites bumping the size to 128/256, but that would leave this magic 6 stale and silently degrade the hash distribution (fewer distinct slots) with no compile error. Derive the shift from the size constant (e.g. a companion RELUNDO_HEAD_CACHE_BITS macro used both here and in the size define) so they stay in lockstep.
📄 src/backend/access/undo/relundo_apply.c (L471-L474)
Memory leak on every DELETE/UPDATE record applied. RelUndoReadRecordWithTuple palloc's a combined header buffer (SizeOfRelUndoRecordHeader + payload_size) and returns it, but this call site discards the return value; only tuple_data_buf/tlen are captured and freed. The header allocation is never freed. In a batched bulk rollback (up to RELUNDO_APPLY_MAX_BATCH = 128 records folded into one CLR) this leaks 128 header+payload buffers into the current MemoryContext before the batch's CLR is written. Capture the returned pointer and pfree it, or refactor to reuse the already-read header instead of re-reading the record.
💡 Suggested change
Before:
RelUndoReadRecordWithTuple(rel, current_ptr,
&tuple_data_buf, &tlen);
for (i = 0; i < del_payload->ntids; i++)
After:
RelUndoRecordHeader *rec = RelUndoReadRecordWithTuple(rel, current_ptr,
&tuple_data_buf, &tlen);
for (i = 0; i < del_payload->ntids; i++)
📄 src/backend/access/undo/relundo_apply.c (L497-L500)
Same header leak here as in the DELETE case: the RelUndoRecordHeader * returned by RelUndoReadRecordWithTuple is discarded and never pfree'd, leaking header+payload on every UPDATE record rolled back.
📄 src/backend/access/undo/relundo_apply.c (L113-L114)
Portability/format-specifier bug. RelUndoRecPtr is typedef uint64 (relundo.h:80). Casting it to unsigned long and printing with %lu truncates to 32 bits on LLP64 (Windows/MSVC) and all 32-bit platforms, corrupting the logged pointer. Use UINT64_FORMAT with the value cast to uint64 per project convention. This applies to all five such casts in this file (lines 114, 152, 160, 171, 327).
💡 Suggested change
Before:
elog(DEBUG1, "RelUndoApplyChain: starting rollback at %lu",
(unsigned long) current_ptr);
After:
elog(DEBUG1, "RelUndoApplyChain: starting rollback at " UINT64_FORMAT,
(uint64) current_ptr);
📄 src/backend/access/undo/relundo_apply.c (L608-L608)
Leftover debugging scaffolding on the rollback hot path. These per-call trace lines ("calling PageGetItemId", "got ItemId %p", "calling ItemIdSetUnused") are not production logging; they add elog overhead and argument formatting per tuple even when suppressed, and printing a raw kernel pointer (%p) leaks address info. Remove them. The other DEBUG1/2/3 traces throughout this module (RelUndoApplyChain, RelUndoApplyDelete/Update/TupleLock, RelUndoLogApplyCLR, RelUndoReadRecordWithTuple) read the same way and should be dropped before this is commit-ready.
📄 src/backend/access/undo/relundo_apply.c (L1036-L1038)
Silent corruption masking. On the payload_size < tlen mismatch this logs a WARNING and returns without setting *tuple_data_out/*tuple_len_out. The caller (RELUNDO_DELETE/UPDATE) then sees tuple_data_buf == NULL and silently skips the restore (if (tuple_data_buf && tlen > 0)), so a corrupt/inconsistent UNDO record produces an incorrect rollback (before-image not restored) with only a WARNING. During recovery this leaves an uncommitted value visible - a wrong-results bug. This should be ereport(ERROR, errcode(ERRCODE_DATA_CORRUPTED)) rather than a recoverable WARNING that continues.
📄 src/backend/access/undo/relundo_apply.c (L620-L621)
Unconditional LP_UNUSED on a non-normal item. When !ItemIdIsNormal(lp) this only emits a WARNING and then still calls ItemIdSetUnused(lp). Unlike the sibling apply helpers (RelUndoApplyDelete/Update/TupleLock) which ereport(ERROR) on an unexpected item state, this one presses on. If the slot is not in the state expected for an INSERT rollback (e.g. already reused/redirected), blindly marking it unused can clobber unrelated data. Prefer erroring out for consistency with the other helpers, or document precisely why proceeding is safe here.
📄 src/backend/access/undo/relundo_discard.c (L483-L484)
Lock-ordering violation: this acquires a heavyweight relation lock (AccessExclusiveLock) while the caller (RelUndoDiscard) still holds the metapage buffer content lock EXCLUSIVE (metabuf is passed in and only released after this returns). This inverts the standard PostgreSQL ordering, which the subsystem's own README (src/backend/access/undo/README, "Lock Ordering") states explicitly: "Standard PostgreSQL buffer-content locks and heavyweight locks ... must be acquired before any UNDO LWLock", and more generally heavyweight locks are acquired before buffer content locks. The comment claims to mirror lazy_truncate_heap(), but that function takes the AccessExclusiveLock while holding only a heavyweight lock (ShareUpdateExclusiveLock), never a buffer content lock. Acquiring the heavyweight lock's partition LWLock while holding the buffer content LWLock is a real ordering hazard (deadlock / assertion risk under concurrency). The truncate should be performed after releasing the metapage buffer content lock, or the AEL should be taken before locking the metapage buffer.
📄 src/backend/access/undo/relundo_discard.c (L375-L376)
Misleading comment: flag 0 (no REGBUF_STANDARD / REGBUF_FORCE_IMAGE) does NOT force a full page image. A backup image is taken only on the first modification since the last checkpoint; otherwise no FPI is logged and replay relies on the delta re-application in relundo_redo_discard (which sets hdr->prev_blkno explicitly on BLK_NEEDS_REDO). The reasoning about eliding a "hole" applies to REGBUF_STANDARD; with flag 0 there is no hole elision to worry about because the whole page is included in any FPI that is taken. Reword to describe what flag 0 actually does (default logging; delta re-applied at redo) rather than claiming "Log the full page image (flag 0)".
📄 src/backend/access/undo/relundo_discard.c (L215-L231)
This chain-walk loop (Pass 1) can iterate over an unbounded number of pages during a whole-chain discard of a large fork, all while holding the metapage buffer content lock EXCLUSIVE. There is no CHECK_FOR_INTERRUPTS() in the loop body, so a VACUUM/discard sweep over a large UNDO fork cannot be cancelled and blocks all concurrent reservers (which serialize on the same metapage lock) for the entire walk. Add CHECK_FOR_INTERRUPTS() in the loop. The same applies to the Pass 2 counting loop below.
📄 src/backend/access/undo/relundo_discard.c (L10-L14)
This file header describes the discard mechanism inaccurately. Discardability is decided by relundo_page_is_discardable() comparing the page's 32-bit max_xid against oldest_xmin via TransactionIdPrecedes() (modular 32-bit TransactionId arithmetic), not by "a page's generation counter ... compared ... using modular 16-bit arithmetic". The 16-bit generation counter (meta->counter) is used only for ABA defense on page recycling in relundo_allocate_page(), unrelated to discard. Rewrite the header to describe the actual max_xid/oldest_xmin comparison so the comment matches current behavior.
💡 Suggested change
Before:
* Discard walks the page chain from the tail (oldest) toward the head
* (newest). Each page's generation counter is compared against the
* oldest-visible cutoff using modular 16-bit arithmetic. If a page's
* counter precedes the cutoff, all records on that page are safe to
* discard and the page is moved to the free list.
After:
* Discard walks the page chain from the head (newest) toward the tail
* (oldest). Each page's max_xid (the largest urec_xid of any record on
* the page) is compared against oldest_xmin. If a page's max_xid
* precedes oldest_xmin, all records on that page are safe to discard and
* the page is moved to the free list.
📄 src/backend/access/undo/relundo_page.c (L80-L83)
WAL bug (high confidence): this init path dirties shared on-disk state (MarkBufferDirty(buf)) but emits no WAL record and never calls PageSetLSN. Compare with the correct metapage-init path in relundo.c (~line 1300) which mirrors these exact field assignments but then emits an XLOG_RELUNDO_INIT record and stamps the LSN. Here, the caller only WAL-logs the metapage as block 1 when staged.is_new_page is true (RelUndoFinish); a dirty, LSN-less metapage can be flushed by the checkpointer ahead of any WAL, violating WAL-before-data, and a standby restoring the FPI reconstructs a magic-0 metapage. This init/reinit must emit its own XLOG_RELUNDO_INIT (guarded by !InRecovery) and PageSetLSN, exactly like RelUndoInitializeMetapage.
📄 src/backend/access/undo/relundo_page.c (L96-L98)
Wrong errcode (medium confidence): this is an UNDO fork, not an index. The rest of the UNDO subsystem uses ERRCODE_DATA_CORRUPTED (e.g. relundo_recovery.c:297, flux_tuple.c) for such corruption. Use ERRCODE_DATA_CORRUPTED here (and at the version check below) for a semantically correct SQLSTATE.
💡 Suggested change
Before:
ereport(ERROR,
(errcode(ERRCODE_INDEX_CORRUPTED),
errmsg("UNDO fork for relation \"%s\" has no blocks",
After:
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("UNDO fork for relation \"%s\" has no blocks",
📄 src/backend/access/undo/relundo_page.c (L80-L83)
WAL bug (high confidence): the reinit path dirties the metapage (MarkBufferDirty(buf)) with no WAL record and no PageSetLSN. This branch is reachable from the discard path (relundo_discard.c:111, EXCLUSIVE); after reinit all chains are empty, so RelUndoDiscardSlot logs nothing and RelUndoTruncateEmptyChain returns early (old_nblocks == 1), leaving a dirty, unlogged, LSN-less metapage. It is also reachable from the SHARE reserve path. Emit an XLOG_RELUNDO_INIT record and PageSetLSN here (guarded by !InRecovery), matching RelUndoInitializeMetapage, so the reinit is crash- and replica-consistent.
📄 src/backend/access/undo/relundo_page.c (L117-L120)
Data-loss footgun (medium confidence): the comment asserts "an uninitialized metapage means no UNDO records were ever written, so there's nothing to lose," but the code reaches this branch for ANY invalid magic, including real corruption of a metapage that still references live data pages (via head_blkno/tail_blkno). Unconditionally reinitializing then wipes those chain pointers and silently orphans live UNDO. If reinit is intended only for the missing-init case, verify the fork block count / that data pages are absent before wiping, or downgrade to ERROR on unexpected magic when the fork has >1 block.
📄 src/backend/access/undo/relundo_page.c (L299-L302)
Comment-accuracy nit (low confidence): the "Verified: ..." note narrates review/verification status rather than explaining why. This is exactly the WAL-coverage claim that is incomplete (it only holds for the is_new_page path). Reword to explain the invariant the code relies on, and drop the verification-status phrasing.
📄 src/backend/access/undo/relundo_recovery.c (L320-L322)
Return-type mismatch: this function conceptually returns a BlockNumber (the previous page in the chain, or InvalidBlockNumber at the tail), but is declared to return int. prev is a BlockNumber (uint32) and can be InvalidBlockNumber (0xFFFFFFFF). Returning that through int is an implicit unsigned->signed narrowing that only round-trips correctly by relying on two's-complement behavior, and the forward declaration at the top of the file has the same int type. This is a portability/maintainability smell in a subsystem where BlockNumber is deliberately unsigned. Declare the return type as BlockNumber (and update the forward declaration).
💡 Suggested change
Before:
static int
RelUndoRecoveryApplyPage(Relation rel, BlockNumber blkno)
{
After:
static BlockNumber
RelUndoRecoveryApplyPage(Relation rel, BlockNumber blkno)
{
📄 src/backend/access/undo/relundo_recovery.c (L76-L76)
Forward declaration mirrors the same return-type mismatch: RelUndoRecoveryApplyPage returns a BlockNumber (including InvalidBlockNumber == 0xFFFFFFFF) but is typed int. Change to BlockNumber to keep the declaration and definition consistent and avoid the implicit unsigned/signed conversion.
💡 Suggested change
Before:
static int RelUndoRecoveryApplyPage(Relation rel, BlockNumber blkno);
After:
static BlockNumber RelUndoRecoveryApplyPage(Relation rel, BlockNumber blkno);
📄 src/backend/access/undo/relundo_worker.c (L373-L380)
Committing a transaction after a caught ERROR is incorrect. process_relundo_work_item()'s PG_CATCH calls FlushErrorState() but never AbortCurrentTransaction(); control then returns here and CommitTransactionCommand() runs on a transaction that hit ERROR. The transaction's resource owner, buffer pins/locks, and LWLocks were never unwound, so the commit runs on inconsistent state (leaked locks/buffers, possible assertion failure or use-after-error). The established pattern (autovacuum.c, undoworker.c) is EmitErrorReport() -> AbortCurrentTransaction() -> LWLockReleaseAll()/UnlockBuffers()/... -> FlushErrorState(). Also note this main loop has no sigsetjmp() top-level recovery, so any ERROR escaping the inner PG_TRY (e.g. from RelUndoQueueMarkComplete or CommitTransactionCommand itself) crashes the worker with no cleanup.
📄 src/backend/access/undo/relundo_worker.c (L319-L323)
Error recovery here is incomplete and will corrupt the enclosing transaction. After a caught ERROR you must abort the transaction before flushing the error state, otherwise the caller's CommitTransactionCommand() runs on a half-aborted transaction. Follow the tree's convention: EmitErrorReport(); AbortCurrentTransaction(); then release residual resources (LWLockReleaseAll(), UnlockBuffers(), etc.) and finally FlushErrorState(). As written, buffer content locks pinned by RelUndoApplyChain and the RowExclusiveLock on the relation are leaked into the next loop iteration.
📄 src/backend/access/undo/relundo_worker.c (L149-L151)
Silent data-loss hazard. This is called during transaction abort to schedule UNDO of aborted rows; dropping the item with only a WARNING means the aborted tuple data is never restored, leaving the relation permanently inconsistent after ROLLBACK. A full fixed-size queue must not silently discard UNDO work. Also, errmsg text violates conventions: it must start lowercase, must not be a complete sentence, and reloid should not be interpolated into user-facing text as raw digits (%u).
📄 src/backend/access/undo/relundo_worker.c (L168-L169)
Portability defect: RelUndoRecPtr is a uint64 (typedef uint64 in relundo.h). Casting to (unsigned long) and printing with %lu truncates on LLP64 (Windows) and 32-bit platforms where unsigned long is 32 bits. Use UINT64_FORMAT with a (uint64) cast.
💡 Suggested change
Before:
elog(DEBUG1, "Queued per-relation UNDO work for database %u, relation %u (ptr=%lu)",
dboid, reloid, (unsigned long) start_urec_ptr);
After:
elog(DEBUG1, "Queued per-relation UNDO work for database %u, relation %u (ptr=" UINT64_FORMAT ")",
dboid, reloid, (uint64) start_urec_ptr);
📄 src/backend/access/undo/relundo_worker.c (L282-L283)
Same uint64 truncation issue: RelUndoRecPtr is uint64; %lu with an (unsigned long) cast is wrong on Windows/32-bit. Use UINT64_FORMAT with a (uint64) cast.
💡 Suggested change
Before:
elog(LOG, "Per-relation UNDO worker processing: database %u, relation %u, UNDO ptr %lu",
item->dboid, item->reloid, (unsigned long) item->start_urec_ptr);
After:
elog(LOG, "Per-relation UNDO worker processing: database %u, relation %u, UNDO ptr " UINT64_FORMAT,
item->dboid, item->reloid, (uint64) item->start_urec_ptr);
📄 src/backend/access/undo/relundo_worker.c (L56-L58)
These are declared plain int globals with a "GUC parameters" comment, but neither max_relundo_workers nor relundo_worker_naptime is registered in guc_tables.c (verified: no matches). As written they are silently unconfigurable, and the comment is misleading. Either register them properly in guc_tables.c with bounds + documentation, or drop the GUC framing and use named constants.
📄 src/backend/access/undo/relundo_worker.c (L687-L694)
This block is a near-verbatim copy of launcher_spawn_worker() above, differing only in bgw_notify_pid (MyProcPid vs 0) and the WARNING vs DEBUG1 log level on failure. Extract a shared helper to build the BackgroundWorker struct (DRY) instead of duplicating the setup. Also uses sprintf() for bgw_library_name/bgw_function_name; prefer bounded strlcpy/snprintf for consistency with the rest of the file.
📄 src/backend/access/undo/relundo_worker.c (L701-L702)
errmsg convention violation: user-facing messages must start lowercase and not be complete sentences. "could not register per-relation UNDO worker for database %u" starts with lowercase "could" which is fine, but the sibling messages elsewhere in this file (WARNING/ereport) mix styles. Confirm all ereport strings begin lowercase and drop capitalization from the elog(LOG)/elog(DEBUG1) human-readable text for consistency with tree conventions.
📄 src/backend/access/undo/relundo_worker.c (L715-L716)
Stale/contradictory comment. The code opens the relation with RowExclusiveLock, but this comment claims the worker acquires AccessExclusiveLock and that the sync-rollback path depends on that lock level. This describes prior (removed) behavior; it will mislead future readers about the synchronous-rollback contract. Update the comment to match the current RowExclusiveLock behavior. (The "Previously this used AccessExclusiveLock..." note in process_relundo_work_item is likewise history-in-comments and should be dropped.)
📄 src/backend/access/undo/relundo_worker.c (L706-L707)
Potential handle leak / lost worker. pending_undo_handle is overwritten here without checking whether a previous handle is still set. If StartRelUndoWorker() is called again before WaitForPendingRelUndo() consumes the prior handle, the earlier BackgroundWorkerHandle is leaked and that worker is never waited on, breaking the 'truly synchronous' rollback contract for the first spawn. Either assert/require pending_undo_handle == NULL here, or pfree the old handle first.
📄 src/backend/access/undo/slog.c (L344-L346)
The comment says this "never fails for lack of space" and the return is "always true", but SLogShmemInit calls dsa_set_size_limit(..., slog_dsa_max_size_mb * 1024*1024). When the ATM grows past that limit, slog_atm_set() -> dsa_allocate() raises ERROR ("out of shared memory"), not a benign no-op. Since this runs under txn_lock in LW_EXCLUSIVE on the transaction-abort path, an ERROR here is a footgun (abort-time failure). The GUC's own long_desc claims "new entries cannot be stored and a WARNING is emitted" -- neither is implemented. Either handle the limit with dsa_allocate_extended(..., DSA_ALLOC_NO_OOM)-style logic and return false + WARNING as documented, or fix the comment and GUC doc to reflect that the limit causes an ERROR.
📄 src/backend/access/undo/slog.c (L609-L613)
Inconsistent NULL handling: this function dereferences *xid_out, *dboid_out, and *lsn_out unconditionally, while the sibling SLogTxnLookupByXid guards lsn_out with if (lsn_out). If a caller passes NULL for any of these, this is a NULL-pointer crash. Guard the output stores (or document that all three are required and Assert them).
📄 src/backend/access/undo/slog.c (L68-L70)
Comment describes implementation history rather than current behavior. Per project convention, comments must describe what the code does now, not what it replaced. Drop the "the same ordering property the old skip-list provided" clause (keep the description of the xid-major ordering the range ops rely on).
💡 Suggested change
Before:
* contiguously (xid-major, reloid-minor ascending) -- the same ordering
* property the old skip-list provided, which the per-xid range
* operations below rely on.
After:
* contiguously (xid-major, reloid-minor ascending), which the per-xid
* range operations below rely on.
📄 src/backend/access/undo/slog.c (L73-L74)
Over-long comment line ("All access is serialized externally by the" runs well past the ~78-column wrap of the surrounding block). Reflow so this file passes pgindent/formatting cleanly.
💡 Suggested change
Before:
* than reserving a fixed shared-memory pool. All access is serialized externally by the
* existing txn_lock (LWLock): SET/DELETE under LW_EXCLUSIVE, FIND and
After:
* than reserving a fixed shared-memory pool. All access is serialized
* externally by the existing txn_lock (LWLock): SET/DELETE under
* LW_EXCLUSIVE, FIND and
📄 src/backend/access/undo/slog.c (L347-L350)
YAGNI/minimalism: none of the transaction-sLog API functions declared here (SLogTxnInsert/Lookup/LookupByXid/Remove/RemoveByXid/MarkReverted/GetNextUnreverted/CollectUnrevertedDatabases/GetOldestUnrevertedLSN/SnapshotForCheckpoint) nor the GUC slog_dsa_max_size_mb have any caller anywhere in the tree. This is speculative scaffolding for a not-yet-wired path. For a pgsql-hackers submission, either wire up the consumer in the same (or an ordered subsequent) commit, or drop the unused surface until it is needed.
📄 src/backend/access/undo/relundo_xlog.c (L456-L459)
xlrec->slot comes from a potentially corrupted WAL record and is used to index the fixed-size tail_blkno[RELUNDO_NUM_HEADS] / head_blkno[RELUNDO_NUM_HEADS] arrays without any bounds check. A record with slot >= RELUNDO_NUM_HEADS causes an out-of-bounds write into the metapage buffer during redo (memory corruption on the recovery path). This is inconsistent with the function's own defensive PANICs on other fields (npages_freed, old_tail_blkno, new_tail_blkno). Add a bound check before the indexing, e.g. PANIC when xlrec->slot >= RELUNDO_NUM_HEADS.
💡 Suggested change
Before:
/* Advance the live data chain tail (and head if it is now empty). */
meta->tail_blkno[xlrec->slot] = xlrec->new_tail_blkno;
if (whole_chain)
meta->head_blkno[xlrec->slot] = InvalidBlockNumber;
After:
if (xlrec->slot >= RELUNDO_NUM_HEADS)
elog(PANIC, "relundo_redo_discard: invalid slot %u", xlrec->slot);
/* Advance the live data chain tail (and head if it is now empty). */
meta->tail_blkno[xlrec->slot] = xlrec->new_tail_blkno;
if (whole_chain)
meta->head_blkno[xlrec->slot] = InvalidBlockNumber;
📄 src/backend/access/undo/slog_flathash.c (L325-L334)
The memset here is dead: it zeroes bucket->entry.key and the immediately-following memcpy overwrites the entire field. Drop the redundant memset. This occurs in both flat_hash_apply_insert and flat_hash_apply_create_aborted.
More substantively, bucket->entry.key is write-only dead data: a codebase search shows it is written only here and never read anywhere (consumers match/read bucket->key and entry.ops[]/entry.nops). Since bucket->key already stores the same SLogTupleKey, entry.key duplicates it in every bucket of a shared-memory-sized (up to 2M buckets) structure, wasting memory. Consider removing the entry.key field entirely.
💡 Suggested change
Before:
bucket->entry.nops = 0;
memset(&bucket->entry.key, 0, sizeof(SLogTupleKey));
memcpy(&bucket->entry.key, &op->key, sizeof(SLogTupleKey));
memset(bucket->entry.ops, 0, sizeof(bucket->entry.ops));
ht->num_entries++;
}
entry = &bucket->entry;
/* Check if this xid already has an op (overwrite) */
After:
bucket->entry.nops = 0;
memcpy(&bucket->entry.key, &op->key, sizeof(SLogTupleKey));
memset(bucket->entry.ops, 0, sizeof(bucket->entry.ops));
ht->num_entries++;
}
entry = &bucket->entry;
/* Check if this xid already has an op (overwrite) */
📄 src/backend/access/undo/slog_flathash.c (L712-L714)
Silent op loss with no diagnostic. Unlike flat_hash_apply_insert (which emits elog(WARNING, "SLOG_LOST_OP ...") on a full table / full ops array), this path drops the ABORTED marker with only a bare comment. Its caller SLogTupleRemoveBySubXid (slog_tuple.c ~1337) applies the op and does NOT verify storage (no SLogFlatHashHasOpForXid re-probe/retry like the INSERT path does), so a full table here silently loses an abort marker that visibility code relies on to find the aborted tuple. At minimum emit a WARNING mirroring flat_hash_apply_insert so the loss is observable; ideally give the caller a way to detect and fall back.
💡 Suggested change
Before:
}
/* No free slot — operation lost */
}
After:
}
elog(WARNING, "SLOG_LOST_OP create_aborted relid=%u xid=%u",
op->key.relid, op->xid);
}
📄 src/backend/access/undo/slog_flathash.c (L337-L340)
Inconsistent xid comparison convention. These handlers use raw == (entry->ops[i].xid == op->xid, entry->ops[i].xid == op->tuple_op.xid, subxid == op->tuple_op.subxid), while SLogFlatHashHasOpForXid in this same file and the read-side in slog_tuple.c use TransactionIdEquals. The fields are bare TransactionId, so == is functionally correct today, but the mixed style is a maintainability hazard (and would silently break if these ever become epoch-qualified). Standardize on TransactionIdEquals throughout.
💡 Suggested change
Before:
if (entry->ops[i].in_use &&
entry->ops[i].xid == op->tuple_op.xid)
{
/* Overwrite existing op for same xid */
After:
if (entry->ops[i].in_use &&
TransactionIdEquals(entry->ops[i].xid, op->tuple_op.xid))
{
/* Overwrite existing op for same xid */
📄 src/backend/access/undo/slog_flathash.c (L103-L106)
The idx = h & (capacity - 1) masking in the probe functions is only correct when capacity is a power of two. The callers do guarantee this today (SLogFlatHashCapacity() rounds up to a power of two, SLogNumPartitions is rounded to a power of two, and the <64 clamp uses 64), but this critical invariant is neither documented nor guarded at the API boundary. If a future caller passes a non-power-of-two capacity, lookups silently miss existing keys (corruption). Add an assertion here.
💡 Suggested change
Before:
SLogFlatHash *ht = (SLogFlatHash *) data;
int i;
ht->capacity = capacity;
After:
SLogFlatHash *ht = (SLogFlatHash *) data;
int i;
/* Probe masking (h & (capacity-1)) requires a power-of-2 capacity */
Assert(capacity > 0 && (capacity & (capacity - 1)) == 0);
ht->capacity = capacity;
📄 src/backend/access/undo/slog_flathash.c (L59-L60)
max_backends is unused (only cast to void "for call-site compatibility"). Both callers in slog_tuple.c pass MaxBackends but the value is never used in the size computation. Per YAGNI, drop the unused parameter from SLogFlatHashShmemSize/SLogFlatHashPartitionedShmemSize and their declarations rather than carrying dead parameter surface.
📄 src/backend/access/undo/undo_bufmgr.c (L6-L7)
This entire module is dead code. None of its eight public functions (UndoMakeBufferTag, ReadUndoBuffer, ReadUndoBufferExtended, ReleaseUndoBuffer, UnlockReleaseUndoBuffer, MarkUndoBufferDirty, InvalidateUndoBuffers, InvalidateUndoBufferRange) is called anywhere in the tree - a codebase-wide search finds references only inside this file and its header. Worse, the companion header undo_bufmgr.h states the actual architecture is "UNDO-in-WAL": "There are no separate UNDO segment files" and "UNDO data is written via pwrite() and read via pread(), bypassing shared_buffers entirely." That directly contradicts this file's stated premise of routing undo I/O through shared_buffers. This is speculative scaffolding for a path that isn't wired up (YAGNI); it should be removed until a real caller exists. (high confidence)
📄 src/backend/access/undo/undo_bufmgr.c (L242-L247)
last_block is silently ignored: only &first_block is passed to DropRelationBuffers, which (per its contract and FindAndDropRelationBuffers) drops every buffer with blockNum >= firstDelBlock for the fork - there is no upper bound. A caller passing a bounded range [first_block, last_block] to reclaim only part of an undo log will actually have all buffers at or above first_block dropped without write-back, potentially discarding still-live undo pages beyond last_block. This is a silent data-loss/corruption footgun and the API is misleading by design. Either drop the unusable last_block parameter (and document the >= semantics), or invalidate the bounded range explicitly (e.g. loop over first_block..last_block). (high confidence)
📄 src/backend/access/undo/undo_bufmgr.c (L190-L191)
The comment claims pages are dropped "without being written back to disk, since the underlying undo log files are being removed," but this function only evicts buffers from the pool - it never unlinks or truncates any file (no smgrunlink/smgrtruncate here). If file removal is not guaranteed to happen after this call, stale on-disk undo pages remain; if the log number is later reused, ReadUndoBuffer could read stale data. The comment overstates what the code does; align the comment with the actual behavior and ensure ordering vs. the file unlink is documented/enforced. (moderate confidence)
📄 src/backend/access/undo/slog_tuple.c (L841-L842)
Behavior contradicts the function's own contract. The header comment states "Returns false silently if the hash is full (instead of ERROR/PANIC)", but this path unconditionally returns true and never verifies the op was actually stored. Unlike SLogTupleInsert(), which probes SLogFlatHashHasOpForXid() to detect a silently dropped op, this recovery path applies SLOG_FLAT_OP_INSERT and assumes success. If the partition (or the per-TID ops array) is full during standby redo, flat_hash_apply_insert drops the marker but this function still reports success. The caller cannot detect the overflow, and the standby's visibility check for that UNCOMMITTED tuple will be wrong (it depends on the sLog entry being present). Either check SLogFlatHashHasOpForXid() and return false on failure to honor the contract, or fix the comment to state that overflow is not detected here.
📄 src/backend/access/undo/slog_tuple.c (L139-L139)
Non-ASCII character in a comment (the multiplication sign U+00D7). The OCR standard mandates ASCII-only source. Replace with 'x'.
💡 Suggested change
Before:
* 0 = auto (heuristic: 4 × NumCPUs, clamped [16..256], power of 2).
After:
* 0 = auto (heuristic: 4 x NumCPUs, clamped [16..256], power of 2).
📄 src/backend/access/undo/slog_tuple.c (L841-L842)
Documentation/behavior mismatch (footgun). The header comment claims this "Returns false silently if the hash is full (instead of ERROR/PANIC)", but the implementation unconditionally returns true here and never checks whether the INSERT op was actually stored. When the partition is full, flat_hash_apply_insert() emits a SLOG_LOST_OP table_full WARNING and silently drops the op (it returns without storing). During standby redo the UNCOMMITTED marker is therefore silently lost while success is reported, so the AM's standby visibility check cannot find the marker -- breaking visibility on the standby. Unlike SLogTupleInsert, this path does not probe with SLogFlatHashHasOpForXid. Either verify the op was stored and return false when it was dropped, or fix the comment to describe the actual (silent-drop, always-true) behavior.
📄 src/backend/access/undo/slog_tuple.c (L138-L139)
Non-ASCII characters in comments violate the ASCII-only source rule (hard gate). Replace the em-dash and the multiplication sign with ASCII equivalents (e.g., ' - ' and 'x').
💡 Suggested change
Before:
* GUC: slog_num_partitions — number of flat hash partitions.
* 0 = auto (heuristic: 4 × NumCPUs, clamped [16..256], power of 2).
After:
* GUC: slog_num_partitions - number of flat hash partitions.
* 0 = auto (heuristic: 4 x NumCPUs, clamped [16..256], power of 2).
📄 src/backend/access/undo/slog_tuple.c (L158-L158)
Non-ASCII em-dash in comment violates the ASCII-only source rule (hard gate). Replace with ASCII ' - '.
💡 Suggested change
Before:
/* Explicit GUC value — clamp and round to power of 2 */
After:
/* Explicit GUC value - clamp and round to power of 2 */
📄 src/backend/access/undo/slog_tuple.c (L352-L352)
Non-ASCII arrow in comment violates the ASCII-only source rule (hard gate). Replace with ASCII '->'.
💡 Suggested change
Before:
* These inline functions encapsulate the key→partition routing so that
After:
* These inline functions encapsulate the key->partition routing so that
📄 src/backend/access/undo/slog_tuple.c (L800-L800)
Non-ASCII em-dash in comment violates the ASCII-only source rule (hard gate). Replace with ASCII ' - '.
💡 Suggested change
Before:
/* Table or per-TID array was full — try eviction */
After:
/* Table or per-TID array was full - try eviction */
📄 src/backend/access/undo/slog_tuple.c (L2295-L2295)
Non-ASCII arrow in comment violates the ASCII-only source rule (hard gate). Replace with ASCII '->'.
💡 Suggested change
Before:
* Fast path: top-level transaction with no savepoint → use the hash.
After:
* Fast path: top-level transaction with no savepoint -> use the hash.
📄 src/backend/access/undo/slog_tuple.c (L2445-L2445)
Non-ASCII em-dash in comment violates the ASCII-only source rule (hard gate). Replace with ASCII ' - '.
💡 Suggested change
Before:
/* Found it — store local copy for savepoint rollback */
After:
/* Found it - store local copy for savepoint rollback */
📄 src/backend/access/undo/slog_tuple.c (L2470-L2470)
Non-ASCII em-dash in comment violates the ASCII-only source rule (hard gate). Replace with ASCII ' - '.
💡 Suggested change
Before:
/* Tracked key not found — this shouldn't happen, but is non-fatal */
After:
/* Tracked key not found - this shouldn't happen, but is non-fatal */
📄 src/backend/access/undo/undoapply.c (L325-L325)
Correctness/data-integrity footgun: the return value is batches_processed > 0, which is true as soon as any batch was read successfully, regardless of records_skipped. But records_skipped is incremented for real failures too: ApplyOneUndoRecord() returns false (and increments records_skipped) for UNDO_APPLY_ERROR and for an unknown RM ID, not only for the legitimate UNDO_APPLY_SKIPPED (dropped relation) case.
The top-level abort caller in xactundo.c treats this boolean as "rollback fully applied" and therefore does NOT fall back to ATM/deferred processing when it is true. So a batch in which one or more rm_undo callbacks errored is reported as success, and a partially-applied in-place UPDATE/DELETE is left on the page -- the exact corruption the header comment claims to prevent. Distinguish genuine skips from errors and only return success when no record errored (or surface the error count so the caller can defer to ATM).
📄 src/backend/access/undo/undoapply.c (L170-L174)
The chain-walk loop (and the inner forward-scan loop that follows) has no CHECK_FOR_INTERRUPTS(). This function runs in the logical revert background worker (process_revert_entry -> ApplyUndoChainFromWAL) over a chain that can span many batches; without a CHECK_FOR_INTERRUPTS() a long or pathological chain delays SIGTERM/query-cancel handling. Add CHECK_FOR_INTERRUPTS() at the top of the batch loop (the inline-abort path holds interrupts, but the worker path does not, so this is safe/required there).
📄 src/backend/access/undo/undoapply.c (L253-L255)
urec_payload_len is never validated against urec_len. The forward scan checks that the whole record (urec_len) fits within the batch buffer, but the reverse-apply pass uses header.urec_payload_len as the payload length passed to rm_undo, with payload = record_starts[i] + SizeOfUndoRecordHeader. If a corrupted/overwritten WAL record has urec_payload_len > urec_len - SizeOfUndoRecordHeader, rm_undo reads past the record body (out-of-bounds read into the next record or past end). At write time urec_len == SizeOfUndoRecordHeader + payload_len, so add a check in the forward scan such as hdr.urec_payload_len > hdr.urec_len - SizeOfUndoRecordHeader and reject the record, consistent with the SIGBUS/corruption hardening applied elsewhere in this subsystem.
📄 src/backend/access/undo/undo_xlog.c (L270-L275)
Page-corruption hazard during redo (HAS_TUPLE path). Unlike the forward-apply path (flux_undo.c:253-263), which explicitly refuses to restore when ItemIdGetLength(lp) < old_len ("the slot was shrunk by a later in-place update and cannot be safely grown from here"), this redo path calls ItemIdSetNormal(lp, ..., xlrec->tuple_len) and memcpy's tuple_len bytes into on-page storage sized for the current item length. When tuple_len > ItemIdGetLength(lp), this overruns the item and corrupts the heap page during crash recovery, diverging from the forward path. Add the same xlrec->tuple_len <= ItemIdGetLength(lp) guard before mutating.
Separately, Assert(datalen >= xlrec->tuple_len) guards WAL-derived (potentially corrupt on-disk) data; Asserts compile out in production, leaving an out-of-bounds read in the following memcpy. This must be a runtime ereport(ERROR).
📄 src/backend/access/undo/undo_xlog.c (L347-L351)
Page-corruption hazard during redo (delta path). The validation checks prefix+suffix <= cur_len and prefix+changed+suffix == old_tuple_len, but never checks old_tuple_len <= cur_len. It then does ItemIdSetNormal(lp, ..., old_tuple_len) and memcpy(cur_htup, restored, old_tuple_len) into on-page storage sized for cur_len. A CLR with old_tuple_len > cur_len (a legitimate scenario: the new/current tuple can be shorter than the pre-update tuple) overruns the on-page item and corrupts the heap page during recovery. Add old_tuple_len > cur_len to the validation error condition. (Note: this whole UNDO_CLR_HAS_DELTA redo branch has no forward emitter anywhere in the tree, so it is currently reachable only via corrupt/replayed WAL - the bounds check is mandatory regardless.)
💡 Suggested change
Before:
if (prefix_len > cur_len ||
suffix_len > cur_len ||
prefix_len + suffix_len > cur_len ||
(Size) (prefix_len + changed_len + suffix_len) != (Size) old_tuple_len ||
datalen < hdr_size + changed_len)
After:
if (prefix_len > cur_len ||
suffix_len > cur_len ||
prefix_len + suffix_len > cur_len ||
old_tuple_len > cur_len ||
(Size) (prefix_len + changed_len + suffix_len) != (Size) old_tuple_len ||
datalen < hdr_size + changed_len)
📄 src/backend/access/undo/undo_xlog.c (L400-L401)
WAL-derived length guarded by Assert (compiled out in production). vis_datalen comes from the WAL record and can be short/corrupt on disk; an Assert leaves an out-of-bounds read in the following memcpy(&vis_rec, vis_data, SizeOfUndoApplyVisibility) in production builds. Use a runtime ereport(ERROR) instead. Same applies to Assert(datalen >= SizeOfUndoApplyHot) in the HOT-restore branch below.
📄 src/backend/access/undo/undo_xlog.c (L1006-L1008)
Deferred transactions are misfiled and never applied (silent data-integrity loss). PerformUndoRecovery() runs in the startup process during the recovery undo phase (xlogrecovery.c:1884), before any database connection is established, so MyDatabaseId is InvalidOid here. The logical revert worker only processes ATM entries where entry_dboid == MyDatabaseId (logical_revert_worker.c:241) and workers are only spawned for real OIDs found in pg_database - so a deferred entry filed under InvalidOid is never picked up and its UNDO is never applied. The per-transaction database OID must be recovered from WAL, not read from process-global state.
📄 src/backend/access/undo/undo_xlog.c (L1224-L1226)
Wrong timeline for historical LSN during recovery. XLogFilePath() here uses GetWALInsertionTimeLine() to build the segment path, but a historical batch_lsn on the recovery/undo path belongs to the replay timeline (see xlogutils.c read_local_xlog_page_guts, which uses GetXLogReplayRecPtr() during recovery). After a timeline switch, using the insertion TLI can point at a nonexistent file and trigger a false "recycled" skip - silently dropping a legitimate rollback (tuples stuck under their UNCOMMITTED/retention flag until VACUUM) - or, conversely, mask a genuinely recycled segment and let the later XLogReadRecord hit SIGBUS. Select the replay timeline when in recovery.
📄 src/backend/access/undo/undo_xlog.c (L630-L630)
Non-ASCII characters violate the ASCII-only source rule. This line contains a Unicode multiplication sign and an approximately-equal sign. Replace with ASCII (e.g. "1 million entries x ~40 bytes each ~= 40 MB").
💡 Suggested change
Before:
* 1 million entries × ~40 bytes each ≈ 40 MB, which is reasonable for
After:
* 1 million entries x ~40 bytes each ~= 40 MB, which is reasonable for
📄 src/backend/access/undo/undo_xlog.c (L1213-L1213)
Non-ASCII character (em dash) violates the ASCII-only source rule. Replace with ASCII (e.g. " -- if our target is well behind the").
💡 Suggested change
Before:
* Compare against GetRedoRecPtr() — if our target is well behind the
After:
* Compare against GetRedoRecPtr() -- if our target is well behind the
📄 src/backend/access/undo/undo_xlog.c (L652-L660)
O(n^2) recovery-time cost. UndoRecoveryTrackBatch() is invoked per XLOG_UNDO_BATCH record and per heap record with embedded UNDO (xact.c:6667), each doing a linear scan of undo_recovery_entries; UndoRecoveryRemoveXid() (called per commit/abort) is likewise O(n). With many in-flight transactions in a large crash this is quadratic over redo. The struct is even named a "hash table entry" while implemented as a linear array. Use simplehash/dynahash keyed by xid. Also note removed entries are only tombstoned (xid=Invalid) and never compacted, so the array only grows.
📄 src/backend/access/undo/undo_xlog.c (L38-L39)
Committed, known layering breach shipped in the diff. These two FIXME(reviewer-item-2) blocks document that the AM-agnostic UNDO core hard-codes heap WAL-format knowledge (RM_HEAP_ID branches testing XLH_*_HAS_UNDO and deriving the embedded xl_undo_batch offset from SizeOfHeapInsert/Delete/Update). This is a footgun: any change to heap WAL record layout silently breaks UNDO batch reading (misinterpreted payload -> corrupt undo stream). A merged FIXME describing a known design breach is WIP, not commit-ready - either extract behind an rmgr callback as the FIXME proposes, or provide a clear rationale/thread reference for deferring.
📄 src/backend/access/undo/undobuffer.c (L325-L328)
UndoBufferFlush() emits a WAL record (XLogBeginInsert/XLogRegisterData/XLogInsert) but is NOT wrapped in a critical section. The parallel path UndoRecordSetInsert() (undoinsert.c) documents that it "always runs inside the caller's critical section (it emits a WAL record)". Confirmed the actual caller violates this: in hashinsert.c, HashUndoLogInsert() -> UndoBufferAddRecordParts() -> UndoBufferFlush() is invoked at line 249, AFTER END_CRIT_SECTION() at line 241. Emitting WAL outside a critical section means an ERROR (e.g. OOM in XLogInsert, or in the state updates below) after the record is durably inserted can leave the in-memory UNDO chain (chain_prev / SetCurrentTransactionUndoRecPtr) inconsistent with what is on disk, and more generally breaks the standard WAL-write discipline (a failure mid-insert must PANIC, not unwind). Wrap the WAL insert and the subsequent chain-state updates in START_CRIT_SECTION()/END_CRIT_SECTION(), or require callers to hold one. (high confidence)
📄 src/backend/access/undo/undobuffer.c (L322-L323)
The batch persistence is hard-coded to UNDOPERSISTENCE_PERMANENT with no regard for the relation's persistence. For a temporary or unlogged relation this is wrong on two counts: (1) temp relations must never emit WAL at all, yet UndoBufferFlush() unconditionally calls XLogInsert(RM_UNDO_ID, ...); (2) the recorded persistence level is a lie for unlogged/temp data and will drive the wrong recovery/discard behavior. UndoBufferBegin() should capture rel->rd_rel->relpersistence (mapped to UndoPersistenceLevel) and Flush must skip WAL / use the correct level for non-permanent relations. Verify callers gate on RelationNeedsWAL before activating the buffer. (high confidence)
📄 src/backend/access/undo/undobuffer.c (L91-L93)
Unsigned overflow / infinite-loop hazard in the growth loop. new_cap is Size (unsigned); new_cap *= 2 can wrap to 0 (or to a value < needed) for very large undo_t2buf.len + additional, after which the while condition may never terminate (0*2==0) or repalloc gets a too-small size, leading to a heap overflow in the subsequent memcpy in UndoTier2AddRecord/AddRecordParts. payload_len flows from AM callers (e.g. itemsz/itup_sz in nbtree_undo.c) and is only truncated to uint32 in urec_len/urec_payload_len with no bound check. Add an explicit overflow guard (e.g. cap at MaxAllocSize and ereport(ERROR) if record_size or the doubled capacity would exceed it) before allocating. (high confidence)
📄 src/backend/access/undo/undobuffer.c (L190-L192)
The static undo_t2buf is never reset on transaction abort. If an ERROR is thrown between flux_begin_bulk_insert() (UndoBufferBegin) and flux_finish_bulk_insert() (UndoBufferEnd), active stays true with a stale xid/relid and partial data. There is no AtEOXact/AtAbort hook clearing this state (searched xact.c and xactundo.c: no UndoBuffer reset). The next command in a new transaction can then see active==true with the previous xid, and worse, Flush would emit a batch stamped with a stale/invalid xid. Register an AtEOXact/AtAbort reset (or reset unconditionally in UndoBufferBegin when xid != GetCurrentTransactionId()). (high confidence)
📄 src/backend/access/undo/undobuffer.c (L269-L272)
Dead / unwired public API. UndoBufferTakePayload(), UndoBufferReset(), and UndoBufferHasPendingData() have no callers anywhere in the tree (verified via search: only their declarations in undobuffer.h). The file header and undobuffer.h describe an "embed the buffer directly into the AM's WAL record ... after XLogInsert() the caller must invoke UndoBufferReset()" workflow that is not actually implemented by any AM -- only the overflow path (UndoBufferFlush) is used. Per YAGNI, drop the unused embed/take/reset scaffolding (and correct the header comment claiming records are "embedded directly inside the AM's WAL record", which does not happen), or wire it up. (high confidence)
📄 src/backend/access/undo/undobuffer.c (L323-L323)
On-disk/WAL format depends on an enum's ABI. SizeOfUndoBatch uses sizeof(UndoPersistenceLevel) and xl_undo_batch embeds the enum directly; enum width is implementation-defined and not portable for a serialized WAL format across compilers/platforms. Additionally the serialized UndoRecordHeader is written by struct layout and replayed verbatim, so it relies on padding being identical on every platform. Use a fixed-width field (e.g. uint8) for persistence and document the fixed layout of UndoRecordHeader as an on-disk contract. (moderate confidence)
📄 src/backend/access/undo/undolog.c (L170-L172)
These functions have no callers anywhere in the tree (verified by searching the whole codebase): UndoLogGetOldestDiscardPtr, UndoLogGetInsertPtr, UndoLogGetDiscardPtr, UndoLogSync, UndoFlushGetMaxWritePtr, UndoLogTryPressureDiscard, UndoLogPath, and ExtendUndoLogSmgrFile are only defined here and declared in undolog.h. Per PostgreSQL minimalism/YAGNI standards, dead exported functions should be removed rather than kept as scaffolding. Only UndoLogCloseFiles, UndoFlushResetMaxWritePtr, UndoLogDeleteSegmentFile, UndoLogSealAndRotate, and ExtendUndoLogFile actually have live callers.
📄 src/backend/access/undo/undolog.c (L423-L425)
This comment is inaccurate on two counts. (1) It claims the stubs "still have live callers," but UndoLogSync, UndoFlushGetMaxWritePtr, UndoLogTryPressureDiscard, UndoLogPath, and ExtendUndoLogSmgrFile have zero callers in the tree. (2) It refers to keeping callers "uniform across both UNDO modes," but the header states the segment-file mode was removed; there is no second in-tree mode. Comments must describe what the code does now, not an aspirational or removed design. Recommend removing the truly-dead stubs and correcting the rationale for any remaining no-ops.
📄 src/backend/access/undo/undolog.c (L476-L477)
UndoLogPath builds a "base/undo/%012u" path for segment files that, per this file's own header, no longer exist. It has no callers, so it is dead and actively misleading (a footgun: any future caller would get a path to a nonexistent file). Remove it along with the other unused legacy stubs.
📄 src/backend/access/undo/undoinsert.c (L116-L117)
The rationale here is inaccurate and hides a truncation footgun. XLogRegisterData(const void *data, uint32 len) takes a uint32 length parameter (xloginsert.c:372), and total main data is hard-capped: XLogRecordAssemble errors out when mainrdata_len > PG_UINT32_MAX. So there IS a size limit on main data. The uset->buffer_size value (type Size/size_t) is passed straight into that uint32 parameter below, and xlrec.total_len is an explicit (uint32) narrowing cast — both silently truncate for a batch > 4GB, and there is no Assert or bound check enforcing the "few hundred KB" invariant the comment claims. Since UndoRecordSet grows its buffer unboundedly by doubling (undorecord.c:223) with no flush threshold, a pathological transaction can reach this. Add an explicit bound check/Assert on uset->buffer_size before narrowing, and correct the comment.
💡 Suggested change
Before:
* XLogRegisterData has no size limit on main data (tracked as uint64 in
* xloginsert.c), so even a 256KB batch is fine. The WAL insertion lock
After:
* Bound the batch size before narrowing to uint32. XLogRegisterData()
* takes a uint32 length and XLogRecordAssemble() rejects main data larger
* than PG_UINT32_MAX, so a batch must stay well under that limit. The WAL
* insertion lock
📄 src/backend/access/undo/undoinsert.c (L108-L109)
Unguarded narrowing: uset->buffer_size is Size (size_t) but xlrec.total_len is uint32. If a batch legitimately exceeds UINT32_MAX the cast silently truncates, and the WAL record's total_len will disagree with the actual payload written by XLogRegisterData(uset->buffer, uset->buffer_size) below, corrupting redo/rollback parsing (UndoReadBatchFromWAL reads total_len bytes). nrecords (int, cast to uint32) is safe for non-negative values, but buffer_size needs an explicit bound check before this point. Add Assert(uset->buffer_size <= PG_UINT32_MAX) (or an ereport) before building the header.
💡 Suggested change
Before:
xlrec.nrecords = (uint32) uset->nrecords;
xlrec.total_len = (uint32) uset->buffer_size;
After:
Assert(uset->buffer_size <= PG_UINT32_MAX);
xlrec.nrecords = (uint32) uset->nrecords;
xlrec.total_len = (uint32) uset->buffer_size;
📄 src/backend/access/undo/undoinsert.c (L111-L111)
This path unconditionally emits a WAL record via XLogInsert() for every persistence level. Callers reach here with UNDOPERSISTENCE_TEMP and UNDOPERSISTENCE_UNLOGGED (GetUndoPersistenceLevel in xactundo.c maps RELPERSISTENCE_TEMP/UNLOGGED to those levels, and InsertXactUndoData -> UndoRecordSetInsert runs for all three). WAL-logging UNDO for temporary relations is unnecessary work on the primary and produces WAL that a standby will replay for objects it cannot see; temp UNDO must not be crash/replica-consistent the way permanent UNDO is. Confirm this is intended, or gate the XLogInsert on persistence (skip WAL for TEMP, and handle UNLOGGED per the unlogged-relation rules). At minimum the persistence handling here deserves an explicit comment on why temp/unlogged batches are WAL-logged. (moderate confidence — depends on the wider architecture, but the unconditional insert is in this file.)
📄 src/backend/access/undo/undoinsert.c (L28-L29)
Aspirational/future-tense comment for behavior that already shipped: "kept for callers that haven't been updated yet" implies a pending migration. These are shipped as permanent no-ops with live callers (xactundo.c calls UndoWalBatchFlush/Reset). Either the callers should be cleaned up in this patch (removing the no-ops as dead scaffolding per the minimalism discipline), or the comment should state plainly that these are retained no-ops for the deferred-WAL API that no longer defers, without the "not yet" framing. Same wording repeats on both function headers below.
📄 src/backend/access/undo/undorecord.c (L308-L320)
Unaligned memory access (portability hard gate). dest advances by record_size, which includes the variable payload_len, so the second and subsequent records start at arbitrary, non-MAXALIGN'ed offsets. Casting (UndoRecordHeader *) dest and then writing header->urec_prev (an 8-byte UndoRecPtr at struct offset 16) through that pointer performs an unaligned 8-byte store, which is undefined behavior and traps (SIGBUS) on strict-alignment targets (some ARM/PPC/sparc). The read paths (undoapply.c/undo_xlog.c) already memcpy into an aligned local; the write side here does not. Build the header in a local UndoRecordHeader and memcpy it into dest, or MAXALIGN each record's start (which also requires reader changes).
📄 src/backend/access/undo/undorecord.c (L308-L320)
Same unaligned-write hazard as in UndoRecordAddPayload: header = (UndoRecordHeader *) dest at an unaligned offset followed by header->urec_prev = ... is an unaligned 8-byte store. Assemble the header in an aligned local and memcpy it (or MAXALIGN record starts).
📄 src/backend/access/undo/undorecord.c (L316-L322)
Silent uint32 truncation of a durable length field. record_size and payload_len are Size (64-bit) but stored as (uint32) into urec_len/urec_payload_len. If payload_len (or record_size) exceeds UINT32_MAX, the on-disk length is truncated, corrupting the record and every subsequent record in the batch when the reader steps by urec_len. Add an explicit bound check (e.g. ereport(ERROR) when record_size > PG_UINT32_MAX) before the cast, or document/enforce the max payload size at the API boundary.
📄 src/backend/access/undo/undorecord.c (L349-L349)
payload_len = part1_len + part2_len sums two Size values with no overflow check before being passed to UndoRecordGetPayloadSize and truncated to uint32 in urec_payload_len/urec_len. Validate the combined length against the on-disk uint32 limit before use.
📄 src/backend/access/undo/undorecord.c (L223-L227)
Unbounded capacity doubling can overflow Size. new_capacity *= 2 in the loop (and the initial buffer_capacity * 2) has no overflow guard; for a very large additional, new_capacity can wrap to a small value, satisfy the loop condition incorrectly, and lead to an undersized allocation followed by an out-of-bounds memcpy/write. Cap growth and error out (or use a checked size computation) when the required size exceeds MaxAllocSize / the uint32 record limit.
📄 src/backend/access/undo/undorecord.c (L243-L243)
Unsubstantiated micro-benchmark numbers in a comment ("~5 cycles vs ~300 cycles"). pgsql-hackers rejects performance claims without a reproducible benchmark, and cycle counts drift with compiler/arch. Describe why the reset path is cheaper (no context reset/realloc) without asserting specific cycle counts.
📄 src/backend/access/undo/undormgr.c (L42-L44)
RegisterUndoRmgr() dereferences rmgr (rmgr->rm_undo, rmgr->rm_name) before validating that rmgr itself is non-NULL. The function already guards rmid and rm_undo with elog(ERROR), so a NULL rmgr would crash with a raw SIGSEGV instead of a controlled error, which is inconsistent with the rest of the validation. Low practical risk today (all current callers pass the address of a static UndoRmgrData, e.g. &flux_undo_rmgr, so rmgr can never be NULL), but a cheap guard keeps the contract enforced. Confidence: moderate.
💡 Suggested change
Before:
if (rmgr->rm_undo == NULL)
elog(ERROR, "UNDO RM \"%s\" must provide an rm_undo callback",
rmgr->rm_name ? rmgr->rm_name : "(null)");
After:
if (rmgr == NULL)
elog(ERROR, "UNDO RM registration requires a non-NULL rmgr");
if (rmgr->rm_undo == NULL)
elog(ERROR, "UNDO RM \"%s\" must provide an rm_undo callback",
rmgr->rm_name ? rmgr->rm_name : "(null)");
📄 src/backend/access/undo/undostats.c (L13-L14)
This function is not registered in pg_proc.dat (verified: no matching entries exist there), so the header's claim that these functions are "registered in pg_proc.dat" is false and the function is unreachable from SQL. Add pg_stat_get_undo_logs, pg_stat_get_undo_buffers, and pg_undo_force_discard to pg_proc.dat with OIDs from the developer range (8000-9999); do not hand-edit the generated fmgroids.h/fmgrtab.c. Until then this is dead, uncallable code with no tests/docs (WIP). (high confidence)
📄 src/backend/access/undo/undostats.c (L293-L294)
This Phase 1/Phase 2 block is a near-verbatim copy of the discard worker's loop in undoworker.c (lines ~255-354), but it diverges in ways that corrupt shared accounting and behavior:
-
Phase 2 here frees a SEALED->DISCARDABLE->FREE log WITHOUT decrementing UndoWorkerShmem->sealed_log_count, whereas the worker does (undoworker.c:347: pg_atomic_fetch_sub_u32(&UndoWorkerShmem->sealed_log_count, 1)). Freeing a sealed log via this path leaves sealed_log_count permanently over-counted, drifting the worker's accounting.
-
Phase 1 here sets log->discard_ptr = insert_ptr directly, while the worker calls UndoLogDiscard() and updates UndoLogShared->total_discarded; this path skips the total_discarded counter, so pg_undo_force_discard produces inconsistent aggregate stats.
Duplicating concurrency-sensitive lifecycle code guarantees the two copies will drift. Factor the worker's Phase 1/Phase 2 into a shared routine and call it here instead of reimplementing it. (high confidence)
📄 src/backend/access/undo/undostats.c (L361-L365)
This branch drops out of the loop after freeing a DISCARDABLE slot but never decrements UndoWorkerShmem->sealed_log_count, unlike the equivalent worker code (undoworker.c:347). Any log that reaches here via SEALED->DISCARDABLE will leave sealed_log_count over-counted, corrupting the worker's pending-sealed accounting. (high confidence)
📄 src/backend/access/undo/undostats.c (L285-L287)
This ereport has no errcode(), so it defaults to ERRCODE_INTERNAL_ERROR for a user-reachable condition (calling the function when UNDO is not initialized). Use an appropriate code such as ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE. (low confidence)
💡 Suggested change
Before:
if (UndoLogShared == NULL)
ereport(ERROR,
(errmsg("UNDO subsystem is not initialized")));
After:
if (UndoLogShared == NULL)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("UNDO subsystem is not initialized")));
📄 src/backend/access/undo/undostats.c (L117-L121)
GetUndoBufferStats() unconditionally writes zeros, and pg_stat_get_undo_buffers() therefore exposes six columns that are always zero (including a hit_ratio branch that is dead because cache_hits/total are always 0). Per the minimalism/YAGNI discipline, shipping an SQL function plus helper that only ever return zeros is dead surface that wastes a catalog OID and misleads users. Either wire it to real data (the comment suggests pg_buffercache) or drop the function and helper entirely. (medium confidence)
📄 src/backend/commands/tablecmds.c (L997-L997)
This break; is unrelated churn. It is added to the last case of the switch, so it has no functional effect (there is no fall-through to guard against). This file (tablecmds.c) is unrelated to the stated purpose of this change (the flux/undo access method). Unrelated cosmetic edits to untouched code are a top rejection reason on pgsql-hackers and should be split out or dropped to keep the diff minimal. Confidence: high.
📄 src/backend/access/undo/undoworker.c (L347-L347)
sealed_log_count is only ever initialized to 0 and decremented here; it is never incremented anywhere in the codebase (undo_xlog.c sets UNDO_LOG_SEALED without touching this counter). Every DISCARDABLE log freed therefore performs an unsigned decrement from 0, underflowing to ~4 billion. After the first underflow, sealed_count > 0 in the main loop is permanently true, forcing the worker to spin at the 200ms fast naptime and ignore undo_worker_naptime. Either increment the counter when a log is sealed, or drop the counter and derive the pending-sealed condition from an actual scan.
📄 src/backend/access/undo/undoworker.c (L129-L131)
SIGTERM handler writes to UndoWorkerShmem->shutdown_requested, a plain bool in shared memory. Signal handlers must only touch volatile sig_atomic_t flags; writing a non-atomic shared field from signal context is not async-signal-safe and the write has no barrier, so the main loop (while (!UndoWorkerShmem->shutdown_requested)) is not guaranteed to observe it. Follow the standard pattern: use the core SignalHandlerForShutdownRequest/volatile sig_atomic_t ShutdownRequestPending (postmaster/interrupt.h), or a local volatile sig_atomic_t flag checked in the loop.
📄 src/backend/access/undo/undoworker.c (L395-L397)
retained_mb is uint64 but is printed with %lu after a cast to unsigned long, which truncates on LLP64 platforms (Windows 64-bit, where long is 32-bit). Use UINT64_FORMAT and pass the value as uint64.
💡 Suggested change
Before:
ereport(WARNING,
(errmsg("UNDO WAL retention (%lu MB) exceeds undo_max_wal_retention_size (%d MB)",
(unsigned long) retained_mb, undo_max_wal_retention_size),
After:
ereport(WARNING,
(errmsg("UNDO WAL retention (" UINT64_FORMAT " MB) exceeds undo_max_wal_retention_size (%d MB)",
retained_mb, undo_max_wal_retention_size),
📄 src/backend/access/undo/undoworker.c (L242-L245)
GetPGProcByNumber(i)->pid is read without holding ProcArrayLock while determining whether a backend is dead. This is a TOCTOU race: a slot can be reused by a new backend between the read of backend_undo_lsns[i] and the pid check (or between the check and the clear), which could clear a live backend's registered batch LSN and prematurely recycle WAL that is still needed for rollback. Consider acquiring ProcArrayLock (LW_SHARED) around the pid check, or otherwise coordinating with slot reuse.
📄 src/backend/access/undo/undoworker.c (L631-L633)
UndoWorkerRequestShutdown() sets shutdown_requested under UndoWorkerShmem->lock, but the SIGTERM handler and the initializer write the same field without the lock, and the main loop reads it without the lock. Mixing LWLock-protected and lock-free writes to the same field provides no real synchronization guarantee and is a data race; the lock here is misleading. Settle on one synchronization model for this flag (e.g. a volatile sig_atomic_t) and apply it consistently.
📄 src/backend/postmaster/bgworker.c (L15-L18)
Include ordering: PostgreSQL sorts #include directives alphabetically within a group, and pgindent/committer review will flag this. access/logical_revert_worker.h and access/relundo_worker.h sort before access/parallel.h ('l'/'r' vs 'p' at the path level: 'logical' < 'parallel', 'parallel' < 'relundo'). Reorder so the block reads logical_revert_worker.h, parallel.h, relundo_worker.h, undoworker.h.
💡 Suggested change
Before:
#include "access/parallel.h"
#include "access/logical_revert_worker.h"
#include "access/relundo_worker.h"
#include "access/undoworker.h"
After:
#include "access/logical_revert_worker.h"
#include "access/parallel.h"
#include "access/relundo_worker.h"
#include "access/undoworker.h"
📄 src/backend/executor/nodeModifyTable.c (L5839-L5844)
Partition tuple routing gap (moderate confidence): begin_bulk_insert is only called for the statically-known relations in mtstate->resultRelInfo[0..mt_nrels). For INSERT/UPDATE into partitioned tables, the actual leaf partitions that receive tuples are set up lazily via ExecInitPartitionInfo()/tuple routing and are NOT in this array, so they never get begin_bulk_insert (and correspondingly never get finish_bulk_insert in ExecEndModifyTable, which also iterates only mt_nrels). An AM relying on this hint (the flux AM) will silently fail to engage its batched-UNDO buffer for partitioned DML. Either wire the hint into the tuple-routing path or document this limitation explicitly.
📄 src/backend/executor/nodeModifyTable.c (L5843-L5843)
Undefined behavior on cast (moderate confidence): estimated_rows is a Cardinality (double). plan_rows can be an arbitrarily large planner estimate; converting a double whose value exceeds INT64_MAX to int64 is undefined behavior in C and can yield a garbage/negative row count that the AM then uses for buffer pre-sizing. Clamp before casting, e.g. pass (int64) Min(estimated_rows, (double) PG_INT64_MAX) (and guard against negatives).
📄 src/backend/executor/nodeModifyTable.c (L5825-L5827)
Comment describes another subsystem's internal implementation detail (low confidence): this executor comment hardcodes flux-AM specifics ("UNDO write buffer", "one palloc of ~512 bytes"). The executor is AM-agnostic; if the flux AM's buffer size changes, this comment silently drifts. Per the tableam.h wrapper doc, keep the rationale generic ("give the AM a bulk-mode hint") and leave AM-specific sizing rationale in the AM.
📄 src/backend/executor/nodeModifyTable.c (L5836-L5837)
begin/finish asymmetry (low confidence): begin_bulk_insert is skipped for CMD_MERGE, but ExecEndModifyTable now calls table_finish_bulk_insert unconditionally for every result relation regardless of operation. MERGE can perform INSERT/UPDATE/DELETE, so it both (a) misses the begin hint and (b) receives a finish without a matching begin. Confirm the AM's finish callback tolerates finish-without-begin, and consider including CMD_MERGE in the begin condition for consistency.
📄 src/backend/access/undo/xactundo.c (L950-L951)
Correctness bug: this goto inline_undo_done jumps past the MemoryContextSwitchTo(old_ctx) and MemoryContextDelete(undo_ctx) calls (below the PG_END_TRY()). On this path undo_ctx (a child of TopMemoryContext, created just above) is never switched away from and never deleted:
- It leaks one "Inline UNDO Apply" context per abort whenever
UndoValidateBatchLSN()fails -- accumulates for the life of the backend. - Worse, the current memory context is left as
undo_ctx, so everything that runs after the goto in AtAbort_XactUndo (ATMAddAborted, ApplyPerRelUndo, UndoRecordSetFree loop, ResetXactUndo) allocates into a context that is never reclaimed during normal operation.
Switch back and delete the context before jumping (or restructure so the invalid-LSN case does not create/enter the context at all).
💡 Suggested change
Before:
undo_applied = false;
goto inline_undo_done;
After:
undo_applied = false;
MemoryContextSwitchTo(old_ctx);
MemoryContextDelete(undo_ctx);
goto inline_undo_done;
📄 src/backend/access/undo/xactundo.c (L265-L268)
PrepareXactUndoData(), PrepareXactUndoDataParts(), InsertXactUndoData() and CleanupXactUndoInsertion() have no callers anywhere in the tree: the flux AM uses RegisterPerRelUndo(), nbtree/hash use UndoRecordSetCreate() directly, and undobuffer uses XActUndoUpdateLastBatchLSN(). This is ~230 lines of speculative scaffolding, including the fragile (UndoRecPtr) 1 magic-sentinel protocol below and its fix-up in InsertXactUndoData. Per YAGNI, either wire it in with a real caller and a test, or drop it -- unused public APIs are a maintenance and misuse hazard.
📄 src/backend/access/undo/xactundo.c (L339-L341)
This magic sentinel (UndoRecPtr) 1 is fragile: UndoRecPtrIsValid() treats any non-zero value as valid ((undoRecPtr) != InvalidUndoRecPtr), so between PrepareXactUndoData() setting the sentinel and InsertXactUndoData() fixing it up, start_location holds a bogus "valid" pointer. Any read of start_location in that window (subxact merge / rollback chain walk) would consume the sentinel as a real UndoRecPtr. The pair is currently uncalled so this is latent, but if these functions are ever wired in, use an explicit boolean "pending" flag rather than overloading the pointer value.
📄 src/backend/access/undo/xactundo.c (L1069-L1070)
Comment references a function that does not exist: there is no AtCleanup_XactUndo() in the tree. The retained per-backend UNDO LSN slot is actually cleared at process exit by AtProcExit_XactUndo() (registered via on_shmem_exit in undo.c). Fix the name so the comment matches reality.
💡 Suggested change
Before:
* containing our UNDO data. The slot will be cleared at backend exit via
* AtCleanup_XactUndo.
After:
* containing our UNDO data. The slot will be cleared at backend exit via
* AtProcExit_XactUndo.
📄 src/backend/access/undo/xactundo.c (L1002-L1007)
Stray blank lines (with trailing whitespace) between the else branch and the closing brace. git diff --check/pgindent will flag these. Remove them.
💡 Suggested change
Before:
elog(WARNING, "ATM full: could not record aborted transaction %u", GetCurrentTransactionId());
}
After:
elog(WARNING, "ATM full: could not record aborted transaction %u", GetCurrentTransactionId());
}
📄 src/backend/access/undo/xactundo.c (L185-L187)
Aspirational/future-tense comments describe behavior that does not exist yet ("an UndoRequestManager will be added later"). Per project comment discipline, comments should describe what the code does now and explain why, not speculate about future work. State plainly that no shared memory is currently required and drop the forward-looking prose.
📄 src/backend/postmaster/postmaster.c (L93-L93)
The access/undolog.h include is unused in postmaster.c. Nothing from undolog.h (no Undo*/undolog symbol) is referenced in this file; the only new call added is LogicalRevertLauncherRegister(), which is declared in logical_revert_worker.h. Per PostgreSQL's minimal-diff discipline, drop this unused include. (high confidence)
📄 src/backend/tcop/utility.c (L1194-L1194)
This is a whitespace-only change unrelated to any functional modification. PostgreSQL enforces a minimal-diff discipline: inserting a blank line into an untouched code path adds needless churn, risks merge conflicts, and would be flagged on pgsql-hackers. Please drop this hunk so the file has no unrelated diff. (high confidence)
📄 src/backend/utils/init/postinit.c (L24-L24)
Include ordering violation. Within a group PostgreSQL keeps includes in alphabetical order; access/undo.h was inserted between access/heapam.h and access/htup_details.h, which is out of order (it also breaks the pre-existing heapam.h -> htup_details.h adjacency). Move it to its alphabetical position, after access/tableam.h and before access/xact.h. (moderate confidence)
📄 src/backend/replication/logical/decode.c (L623-L627)
Leftover ad-hoc debugging. This elog(DEBUG1, "FLUX DecodeInsert: ...") exists only in this one helper and not in DecodeFluxMultiInsert/Update/Delete, so it is inconsistent instrumentation rather than intentional logging, and it adds noise to a hot logical-decoding path. Remove it before this goes to -hackers.
💡 Suggested change
Before:
memcpy(&heap_len, end - sizeof(uint32), sizeof(uint32));
elog(DEBUG1, "FLUX DecodeInsert: full_len=%zu heap_len=%u action=INSERT lsn=%X/%X",
full_len, heap_len,
(uint32) (buf->origptr >> 32), (uint32) buf->origptr);
heap_data = end - sizeof(uint32) - heap_len;
After:
memcpy(&heap_len, end - sizeof(uint32), sizeof(uint32));
heap_data = end - sizeof(uint32) - heap_len;
📄 src/backend/replication/logical/decode.c (L627-L631)
No bounds validation on heap_len read from the WAL tail. The write path guarantees heap_len == heaptup->t_len (>= SizeofHeapTupleHeader) and that the trailing region fits, but the decode path trusts these bytes blindly. On a corrupt/truncated record: (a) heap_len - SizeofHeapTupleHeader underflows to a huge Size when heap_len < SizeofHeapTupleHeader, and (b) end - sizeof(uint32) - heap_len can point before the record start, so the following memcpy reads out of bounds. Logical decoding runs against WAL that may be corrupt on disk; a sanity check (e.g. heap_len >= SizeofHeapTupleHeader && heap_data >= (char *) xlrec + sizeof(xl_flux_insert), else elog(ERROR)) would fail cleanly instead of crashing. The same pattern repeats in DecodeFluxMultiInsert, DecodeFluxUpdate and DecodeFluxDelete. (high confidence)
📄 src/backend/replication/logical/decode.c (L722-L726)
Same unvalidated-length hazard as the reverse walk in the single-insert helper: end -= heap_len after reading each per-tuple length can run end before the start of the record when a length is corrupt, and image_len[i] - SizeofHeapTupleHeader underflows for undersized lengths in the queue loop below. Validate each frame (length fits in remaining bytes; heap_len >= SizeofHeapTupleHeader) and elog(ERROR) on violation rather than dereferencing an out-of-range pointer.
📄 src/backend/replication/logical/decode.c (L715-L716)
image_data/image_len are palloc'd in the current (decode) memory context and only pfree'd at the tail of the function. ReorderBufferAllocChange / ReorderBufferAllocTupleBuf / ReorderBufferQueueChange can ereport(ERROR) (e.g. OOM or serialization failure), and on that path these two arrays leak until the surrounding context is reset. Consider a short-lived context or PG_TRY/PG_FINALLY, or free before the queue loop by copying the needed data. (moderate confidence)
📄 src/bin/pg_dump/pg_dump.c (L17478-L17478)
This added comment is unrelated to this PR (the flux TAM / undo subsystem) and is unnecessary churn in pg_dump.c — the fastest way to draw a rejection on -hackers is touching code not required by the change. Drop it to keep the diff minimal.
Beyond that, the comment is inaccurate: appendReloptionsArrayAH() here emits all of tbinfo->reloptions for whatever relkind this is (partitioned table, foreign table, matview, etc.), not specifically "heap" reloptions. It restates what the code does rather than why, which is exactly the kind of comment the project discourages. (high confidence)
📄 src/bin/pg_waldump/relundodesc.c (L1-L1)
This symlink should not be committed. Every other pg_waldump rmgr desc file (brindesc.c, xactdesc.c, xlogdesc.c, ...) is a build-time-generated symlink: the Makefile rule $(RMGRDESCSOURCES): % : $(top_srcdir)/src/backend/access/rmgrdesc/% auto-discovers *desc*.c via wildcard, creates the symlink, and make clean removes it. These files are deliberately untracked and listed in src/bin/pg_waldump/.gitignore. Committing a tracked symlink here (a) breaks the established pattern, (b) is redundant with the auto-generated symlink and will conflict with it, and (c) commits a git-tracked symlink, which is a portability hazard on Windows checkouts. Drop this file (and the sibling undodesc.c, and any atmdesc.c/fluxdesc.c) from the patch and instead add /relundodesc.c to src/bin/pg_waldump/.gitignore alongside the existing entries. Confidence: high.
📄 src/bin/pg_waldump/rmgrdesc.c (L20-L26)
These new includes break the alphabetical ordering of this include group, which is a PostgreSQL convention (and was consistently followed in this file). access/flux_xlog.h should precede access/heapam_xlog.h/access/multixact.h, and access/atm_xlog.h, access/relundo_xlog.h, access/undo_xlog.h should be sorted into their alphabetical positions rather than lumped after access/spgxlog.h. Reorder so the block reads: ... access/atm_xlog.h (near the top), access/flux_xlog.h (before access/generic_xlog.h/... as appropriate), access/relundo_xlog.h, access/spgxlog.h, access/undo_xlog.h, access/xact.h. Confidence: high.
📄 src/backend/storage/buffer/bufmgr.c (L3224-L3226)
The comment is materially inaccurate and creates a data-corruption footgun. It claims this is "callable while holding only a SHARED content lock" and that safety comes from CAS + (WAL insertion) or (full_page_writes + per-tuple lock). But that is not what actually protects against a torn/lost write. In this codebase the buffer flusher (FlushUnlockedBuffer -> BufferLockAcquire(..., BUFFER_LOCK_SHARE_EXCLUSIVE)) copies the page for I/O while holding SHARE_EXCLUSIVE. A plain SHARE lock does NOT conflict with SHARE_EXCLUSIVE, so a page modified/dirtied under only a plain SHARE lock can be flushed concurrently, producing a torn on-disk image or a lost update. The sole caller (flux_operations.c:2123) is actually safe only because it holds SHARE_EXCLUSIVE, not plain SHARE. The established convention (BufferIsDirty at line 3153, BufferFinishSetHintBits at 7257) requires SHARE_EXCLUSIVE or EXCLUSIVE for exactly this reason. Rewrite the comment to state that the caller must hold at least SHARE_EXCLUSIVE and that plain SHARE is insufficient; correct the safety rationale accordingly (the interlock is the SHARE_EXCLUSIVE lock mode vs the flusher, not FPW/per-tuple locks). The header comment in bufmgr.h:265-269 has the same error and must be fixed too.
📄 src/backend/storage/buffer/bufmgr.c (L3256-L3257)
This deliberately drops the lock-mode assertion, removing the only guard that would catch a caller who dirties a buffer under an insufficient (plain SHARE) lock -- the exact misuse the (incorrect) comment invites. Every sibling that touches BM_DIRTY under a non-exclusive lock asserts the mode: BufferIsDirty (line 3153) and BufferFinishSetHintBits (line 7257) both Assert SHARE_EXCLUSIVE || EXCLUSIVE. Add the same assertion here so a plain-SHARE caller is caught in assert builds, e.g.:
Assert(BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_SHARE_EXCLUSIVE) ||
BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_EXCLUSIVE));
💡 Suggested change
Before:
Assert(BufferIsPinned(buffer));
/* Caller holds at least BUFFER_LOCK_SHARE -- no exclusive assertion */
After:
Assert(BufferIsPinned(buffer));
Assert(BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_SHARE_EXCLUSIVE) ||
BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_EXCLUSIVE));
📄 src/backend/storage/buffer/bufmgr.c (L3194-L3208)
The entire body of MarkBufferDirtyShared is a verbatim copy of MarkBufferDirty (lines 3194-3218): same BM_LOCKED/WaitBufHdrUnlocked wait, same CAS loop, same VacuumCost/pgBufferUsage.shared_blks_dirtied accounting. The only intended difference is the relaxed lock-mode assertion. This copy-paste will draw pushback on -hackers and risks silent divergence if one path is later edited. Consider factoring the shared body into a static helper that both functions call (passing the required assertion in, or asserting in the wrappers), rather than duplicating.
📄 src/bin/pg_waldump/t/001_basic.pl (L83-L87)
This adds four new rmgrs (Undo, ATM, RelUndo, FLUX) to the expected list, but the comment above at lines 58-59 asks you to also add a test case exercising each new rmgr below. The SQL workload (lines ~112-166) is not extended to emit Undo/ATM/RelUndo/FLUX WAL records, so the corresponding desc routines (undo_desc/atm_desc/relundo_desc/flux_desc) get zero coverage from this test. Add SQL that produces records for these rmgrs so their desc/identify paths are exercised.
📄 src/common/relpath.c (L38-L38)
The comment directly above this array ("If you add a new entry, remember to update the errhint in forkname_to_number() below, and update the SGML documentation for pg_relation_size()") requires two follow-ups that are missing from this patch:
- The
errhintinforkname_to_number()still readsValid fork names are "main", "fsm", "vm", and "init".and does not list the new"relundo"fork. Update it to include the new name. - The SGML documentation for
pg_relation_size()must also be updated to mention the new fork.
Confidence: high (the requirement is explicitly documented in-file and verified against forkname_to_number() lines 63-64).
📄 src/bin/pg_waldump/undodesc.c (L1-L1)
Do not commit this file. In pg_waldump, every *desc.c is a build artifact, not source: the Makefile globs $(top_srcdir)/src/backend/access/rmgrdesc/*desc*.c and creates the symlink at build time (rm -f $@ && $(LN_S) $< .), and they are all gitignored (.gitignore lines 4-25). Committing this symlink breaks that convention and causes concrete problems:
make clean/distcleanrunsrm -f ... $(RMGRDESCSOURCES)(Makefile line 54); sinceRMGRDESCSOURCESglobs*desc*.cit now matchesundodesc.c, so clean deletes this tracked file and leaves the working tree dirty relative to HEAD.- Redundant on both build systems: the Makefile auto-creates the symlink, and meson already lists
undodesc.cinrmgr_desc_sources(src/backend/access/rmgrdesc/meson.build) and compiles it directly. Nothing consumes a committed file here. - Committed symlinks are not portable to checkouts/platforms (Windows) that don't preserve them.
Correct fix: delete this file and instead add /undodesc.c (and /relundodesc.c, /atmdesc.c, /fluxdesc.c) to src/bin/pg_waldump/.gitignore so git status stays clean. Confidence: high.
📄 src/include/access/atm_xlog.h (L32-L35)
The reloid field is dead state: the only WAL-write path (ATMAddAborted in atm.c) hardcodes xlrec.reloid = InvalidOid, and redo passes it straight through to ATMAddAbortedInternal. The comment "kept for struct layout" implies preserving a wire/on-disk layout, but this is a brand-new WAL record type that has never shipped, so there is no backward-compatibility or ABI constraint to preserve. Carrying it wastes 4 bytes in every ATM abort WAL record and threads a meaningless value through the redo path. Per YAGNI/minimalism, drop the field entirely and adjust SizeOfXlAtmAbort to end on dboid; update the writer/redo accordingly. Confidence: high.
💡 Suggested change
Before:
Oid reloid; /* InvalidOid (kept for struct layout) */
} xl_atm_abort;
#define SizeOfXlAtmAbort (offsetof(xl_atm_abort, reloid) + sizeof(Oid))
After:
} xl_atm_abort;
#define SizeOfXlAtmAbort (offsetof(xl_atm_abort, dboid) + sizeof(Oid))
📄 src/include/access/atm.h (L24-L27)
Unnecessary includes. This header uses only TransactionId (from access/transam.h), Oid, XLogRecPtr, Size, and bool in its prototypes. datatype/timestamp.h (TimestampTz) and storage/lwlock.h (LWLock) are not used anywhere in the declarations — I confirmed no TimestampTz/LWLock references exist in this header or in atm.c's API. Drop both to keep the include set minimal.
💡 Suggested change
Before:
#include "access/atm_xlog.h"
#include "access/transam.h"
#include "datatype/timestamp.h"
#include "storage/lwlock.h"
After:
#include "access/atm_xlog.h"
#include "access/transam.h"
📄 src/include/access/flux_dirtymap.h (L55-L58)
Redundant declaration. FluxDirtyMapShmemSize, FluxDirtyMapShmemInit, and FluxDirtyMapShmemCallbacks are already declared in src/include/access/flux.h (lines 808-810), and both flux_handler.c and flux_operations.c include access/flux.h and access/flux_dirtymap.h. The same public API is now declared in two headers, which will drift over time and leaves the canonical home ambiguous. Pick one: either move these three declarations out of flux.h (and let it include this header if needed), or drop this block. FluxDirtyMapMark/FluxDirtyMapCheck should live in exactly one header too.
📄 src/include/access/flux_dirtymap.h (L7-L8)
Comment/identity drift. This header (and the whole file comment) repeatedly uses bitmap terminology -- "set the page's bit", "a stale set bit", "a wrongly-cleared bit", "Reclaiming bits" -- but the implementation in flux_dirtymap.c is a partitioned open-addressed hash set of 64-bit keys; there are no per-page bits. This bit/set language will mislead maintainers about the data structure. Reword to "key"/"entry" to match the code (e.g. "mark the page's key", "a stale key only costs an unnecessary sLog probe").
💡 Suggested change
Before:
* modification (in-place UPDATE or in-place DELETE) whose before-image a
* scanner might still need from the sLog. If a page's bit is CLEAR, every
After:
* modification (in-place UPDATE or in-place DELETE) whose before-image a
* scanner might still need from the sLog. If a page's key is absent, every
📄 src/include/access/flux_undo.h (L6-L10)
This entire header declares a FLUX UNDO record path that has no writer. The apply side (flux_undo_apply, apply_flux_undo_insert, etc.) exists in flux_undo.c and the RM is registered via undormgrlist.h, but nothing in the tree ever populates a FluxUndoPayloadHeader or emits a record tagged UNDO_RMID_FLUX with these subtypes. Worse, this directly contradicts flux_handler.c:4427, which states "FLUX's own row DML does not emit UNDO_RMID_FLUX records" (FLUX table DML uses the RELUNDO fork). So this comment describes a mechanism FLUX does not use. Per the minimalism/YAGNI and comment-accuracy rules, either wire up the writer in this same commit or drop the speculative UNDO_RMID_FLUX scaffolding (header + flux_undo.c apply path + undormgrlist.h entry). A registered RM whose records are never produced is dead code. (high confidence)
📄 src/include/access/flux_undo.h (L56-L62)
FluxUndoPayloadHeader is a WAL-durable on-disk format: flux_undo.c memcpy's it as a raw byte image out of the UNDO payload (memcpy(&hdr, payload, SizeOfFluxUndoPayloadHeader)). ItemPointerData is only 2-byte aligned (BlockIdData is two int16), so a compiler is free to insert padding between tid and the uint32 tuple_len that follows. That padding can differ across compilers/ABIs, breaking cross-platform WAL replay and pg_upgrade. The trailing pad field only addresses tail padding, not the tid/tuple_len boundary. Reorder so the uint32 leads (or pack explicitly) and add a StaticAssertDecl on the struct size, following PostgreSQL on-disk struct conventions. (high confidence)
📄 src/include/access/flux_undo.h (L64-L64)
For a WAL/on-disk record-header size macro, PostgreSQL convention uses offsetof() to the end of the last meaningful field rather than sizeof(), precisely so compiler-inserted trailing padding is not baked into the persisted size and cross-platform format (cf. SizeOfUndoApply and other SizeOf* macros). Using sizeof() here couples the on-disk record size to struct padding. Consider offsetof(FluxUndoPayloadHeader, pad) (or the last real field) once the layout is fixed. (moderate confidence)
📄 src/include/access/flux_undo.h (L67-L68)
FLUX_UNDO_FLAG_PARTIAL_TUPLE is never referenced anywhere; only FLUX_UNDO_FLAG_HAS_TUPLE is read (flux_undo.c:371). Likewise the struct's flags comment says "future use: partial-tuple, index-flags," which is an aspirational/future-tense comment for behavior that has not shipped. Drop the unused bit and the speculative comment per the YAGNI/minimalism and comment-accuracy rules; add them back in the commit that actually uses them. (high confidence)
💡 Suggested change
Before:
#define FLUX_UNDO_FLAG_HAS_TUPLE 0x0001
+#define FLUX_UNDO_FLAG_PARTIAL_TUPLE 0x0002
After:
#define FLUX_UNDO_FLAG_HAS_TUPLE 0x0001
📄 src/include/access/flux.h (L109-L110)
Comment/code drift (misleading concurrency documentation). This says the per-tuple CAS writer lock is used "for same-size updates under BUFFER_LOCK_SHARE", but the only caller (flux_tuple_update in flux_operations.c) takes BUFFER_LOCK_SHARE_EXCLUSIVE before the CAS, not BUFFER_LOCK_SHARE. This distinction is correctness-relevant: BUFFER_LOCK_SHARE_EXCLUSIVE already admits only one writer per buffer, which is what makes the per-tuple CAS protocol sound; claiming plain SHARE describes a weaker (and unsafe) model that the code does not implement. Fix the comment to state BUFFER_LOCK_SHARE_EXCLUSIVE.
💡 Suggested change
Before:
* t_writer: Per-tuple CAS writer lock for same-size updates under
* BUFFER_LOCK_SHARE. 0 = unlocked; non-zero = (MyProcNumber + 1) of
After:
* t_writer: Per-tuple CAS writer lock for same-size updates under
* BUFFER_LOCK_SHARE_EXCLUSIVE. 0 = unlocked; non-zero = (MyProcNumber + 1) of
📄 src/include/access/flux.h (L16-L16)
A header must not include postgres.h. PostgreSQL convention is that each .c file includes postgres.h (or postgres_fe.h) as its very first include, and headers under src/include never pull it in. No core access-method header does this. It forces an inclusion-order assumption on every consumer and blocks any future frontend use of this header. Remove the include; the .c files already include postgres.h first.
📄 src/include/access/flux.h (L220-L222)
Stale comment: this describes the obsolete on-disk layout. The flag is still actively set/tested in flux_operations.c, but the version-chain head is now the fixed header field t_verptr, not a "trailing RelUndoRecPtr ... following column data". The comment contradicts the struct definition above (and the t_verptr field comment). Update it to describe what the flag means now.
📄 src/include/access/flux.h (L134-L134)
Dead reserved field (YAGNI). t_gen is only ever written (= 0) and never read anywhere in the FLUX backend; the comment itself admits it is unused and "a future slimming pass may remove it". For a brand-new on-disk format with no back-compat constraint, ship the layout you actually use rather than reserving speculative header space. Consider removing t_gen (and the accompanying t_pad, recomputing the header size) now.
📄 src/include/access/flux.h (L588-L589)
'%%' is only meaningful inside printf-style format strings; this is a plain comment, so it renders as a literal double percent. Use a single '%'.
💡 Suggested change
Before:
* packing pages 100%% full (heap's default) guarantees that any in-place
* growth on a full page aborts. The default reserves ~10%% of each page,
After:
* packing pages 100% full (heap's default) guarantees that any in-place
* growth on a full page aborts. The default reserves ~10% of each page,
📄 src/include/access/heapam.h (L548-L548)
This is a stray whitespace-only change: an extra blank line inserted before #endif with no functional purpose. PostgreSQL requires minimal diffs and clean git diff --check; unrelated whitespace churn is a top rejection reason on -hackers. Remove this line so the file is unchanged. (high confidence)
💡 Suggested change
Before:
+
#endif /* HEAPAM_H */
After:
#endif /* HEAPAM_H */
📄 src/include/access/heapam_xlog.h (L80-L81)
These three XLH_*_HAS_UNDO flags are dead scaffolding. A full-tree search shows they are only ever read (in undo_xlog.c's UndoValidateBatchLSN/apply path); no WAL-emitting code (heap_insert/heap_delete/heap_update in heapam.c, or anywhere) ever sets them in the record's flags field. As a result the "embedded UNDO payload" redo branch is unreachable, and the reader code that inspects these bits carries FIXME(reviewer-item-2) markers acknowledging it isn't properly wired. Per YAGNI/minimalism, don't add flag definitions until the producing side actually sets them; otherwise this is a footgun (reviewers will assume a working inline-UNDO path that does not exist). Confidence: high.
📄 src/include/access/heapam_xlog.h (L80-L81)
Comment style: confirmed by audit and bit N unused describe the review process, not the flag's semantics, and will rot. Follow the surrounding convention (a short semantic description). The parenthetical about which bit is free is also redundant with the (1<<N) value itself. Suggest trimming to just the meaning. Confidence: low (style).
💡 Suggested change
Before:
/* UNDO payload is embedded in this WAL record (bit 6 unused, confirmed by audit) */
#define XLH_INSERT_HAS_UNDO (1<<6)
After:
/* UNDO payload is embedded in this WAL record */
#define XLH_INSERT_HAS_UNDO (1<<6)
📄 src/include/access/heapam_xlog.h (L95-L96)
Comment style: replace the review-process phrasing (confirmed by audit, bit 7 unused) with a semantic-only comment. Note this consumes the last available bit of the 8-bit xl_heap_update.flags field; a comment noting the field is now full would be more useful than noting which bit was free. Confidence: low (style).
💡 Suggested change
Before:
/* UNDO payload is embedded in this WAL record (bit 7 unused, confirmed by audit) */
#define XLH_UPDATE_HAS_UNDO (1<<7)
After:
/* UNDO payload is embedded in this WAL record (last available bit) */
#define XLH_UPDATE_HAS_UNDO (1<<7)
📄 src/include/access/heapam_xlog.h (L114-L115)
Comment style: same review-process phrasing (confirmed by audit, bit 6 unused) as the other two flags; trim to a semantic-only description. Confidence: low (style).
💡 Suggested change
Before:
/* UNDO payload is embedded in this WAL record (bit 6 unused, confirmed by audit) */
#define XLH_DELETE_HAS_UNDO (1<<6)
After:
/* UNDO payload is embedded in this WAL record */
#define XLH_DELETE_HAS_UNDO (1<<6)
📄 src/include/access/logical_revert_worker.h (L25-L25)
Headers must never include "postgres.h". By PostgreSQL convention, postgres.h is included only as the very first line of a .c file; headers assume it has already been included and pull in only the specific headers they need. Remove this include. The types actually referenced here (Size, Datum, Oid) come transitively from the including .c file's postgres.h; if a header truly needs them standalone, include "c.h"/"postgres_ext.h" as appropriate, not postgres.h. (high confidence)
📄 src/include/access/logical_revert_worker.h (L40-L41)
extern variables exposed in a header and referenced across modules must be marked PGDLLIMPORT, otherwise they cannot be linked from other modules/extensions on Windows/MSVC. Compare src/include/postmaster/autovacuum.h, where the analogous autovacuum_naptime / autovacuum_max_workers are declared with PGDLLIMPORT. (high confidence)
💡 Suggested change
Before:
extern int logical_revert_naptime;
extern int max_logical_revert_workers;
After:
extern PGDLLIMPORT int logical_revert_naptime;
extern PGDLLIMPORT int max_logical_revert_workers;
📄 src/include/access/nbtree.h (L1338-L1347)
These nine NBTREE_UNDO_* subtype macros duplicate the identical definitions already present in nbtree_undo.c (lines 62-70). A search of the whole tree shows nothing outside nbtree_undo.c references them, so exposing them in the public header serves no caller. Two independent definitions of the same on-disk subtype values are a drift hazard (change one, forget the other). Drop them from the header and keep the single set of defines in nbtree_undo.c. Confidence: high.
📄 src/include/access/nbtree.h (L1357-L1365)
NbtreeUndoInsertLeafHeader and SizeOfNbtreeUndoInsertLeafHeader are unused: no code in the tree references them (verified by tree-wide search). The comment asserts the "UNDO discard worker" extracts (index_oid, blkno, offset) via this header, but no such caller exists -- this is speculative scaffolding (YAGNI). Additionally, the type merely mirrors the first three fields of NbtreeUndoInsertLeaf in nbtree_undo.c under a comment-only "must match" contract, which is fragile and padding-dependent for an on-disk UNDO payload. Remove this header struct and macro until a real consumer needs it; if a shared view is truly required, move the canonical NbtreeUndoInsertLeaf definition to a header rather than maintaining a parallel copy. Confidence: high.
📄 src/include/access/flux_xlog.h (L16-L16)
A public header must not #include "postgres.h". postgres.h is backend-only (it unconditionally pulls in utils/elog.h and utils/palloc.h and explicitly directs client code to postgres_fe.h) and is not FRONTEND-guarded. This header has a #ifdef FRONTEND branch and is included by frontend code (src/bin/pg_waldump/rmgrdesc.c). A frontend TU that includes this header without first arranging the right base include will drag in backend headers and break the build. Drop this include; a header should include only the specific headers it needs (the ones already listed below), and let the .c translation unit include postgres.h/postgres_fe.h first.
📄 src/include/access/flux_xlog.h (L108-L109)
This layout description contradicts the actual on-disk format and the other comments in this file. The emit path (FluxXLogRegisterLogicalImage in flux_xlog.c) writes the heap payload FIRST and the uint32 length LAST (trailing length: [bytes][uint32 len]), which matches the FluxLogicalImage header comment ("[heap bytes][uint32 heap_len]") and the xl_flux_multi_insert comment ("[body][uint32 len]"). This comment instead documents a LEADING length (uint32 logical_len then bytes), which is wrong and will mislead the decoder implementation. Fix the order to match the code.
💡 Suggested change
Before:
* uint32 logical_len -- bytes of the heap-tuple payload
* bytes[logical_len] HeapTuple t_data bytes
After:
* bytes[logical_len] HeapTuple t_data bytes
* uint32 logical_len -- bytes of the heap-tuple payload (trailing)
📄 src/include/access/flux_xlog.h (L53-L58)
Duplicated comment block: two consecutive "WAL record types for FLUX" headers. Remove the redundant terse block and keep only the descriptive one.
💡 Suggested change
Before:
/*
* WAL record types for FLUX
*/
/*
* WAL record types for FLUX.
*
After:
/*
* WAL record types for FLUX.
*
📄 src/include/access/flux_xlog.h (L82-L84)
These aliases are dead scaffolding and the justification is false. This is a brand-new file, so there is no "backward compatibility" to preserve. Neither XLOG_FLUX_VACUUM nor XLOG_FLUX_UPDATE is referenced anywhere in the backend flux code (fluxdesc.c defines its own local macros, not these). Remove the unused aliases (YAGNI).
📄 src/include/access/relundo_worker.h (L29-L30)
Header files must never include postgres.h. By PostgreSQL convention, postgres.h (or postgres_fe.h) is included only as the very first include in a .c file; a header that pulls it in breaks translation-unit ordering guarantees and is rejected on -hackers. No existing header under src/include/access/ includes it. Drop this line; the .c files that use this header already include postgres.h first, and the types used here (Oid, TransactionId, TimestampTz, LWLock) come from the other headers included below.
💡 Suggested change
Before:
+#include "postgres.h"
+#include "access/relundo.h"
After:
+#include "access/relundo.h"
📄 src/include/access/relundo_worker.h (L86-L87)
extern variables consumed cross-module need PGDLLIMPORT, otherwise linking against them from other modules/extensions fails on Windows/MSVC. Every other GUC extern in src/include/access/*.h (e.g. extern PGDLLIMPORT int max_prepared_xacts;) uses this. Add PGDLLIMPORT to both.
💡 Suggested change
Before:
+extern int max_relundo_workers;
+extern int relundo_worker_naptime;
After:
+extern PGDLLIMPORT int max_relundo_workers;
+extern PGDLLIMPORT int relundo_worker_naptime;
📄 src/include/access/relundo_worker.h (L34-L41)
Duplicate/stray comment stanza: there are two consecutive Shared memory structure for UNDO work queue header comments. Remove the redundant one. Also, Production workloads rarely exceed this with synchronous abort is an unverified assumption stated as fact; per comment discipline, comments should explain why the cap exists, not narrate unproven performance claims.
💡 Suggested change
Before:
+/*
+ * Shared memory structure for UNDO work queue
+ */
+/*
+ * MAX_UNDO_WORK_ITEMS limits the in-flight UNDO work queue.
+ * Keep moderate (64) so shmem stays small during bootstrap.
+ * Production workloads rarely exceed this with synchronous abort.
+ */
After:
+/*
+ * MAX_UNDO_WORK_ITEMS limits the in-flight UNDO work queue.
+ * Keep moderate (64) so shmem stays small during bootstrap.
+ */
📄 src/include/access/relundo.h (L247-L247)
Stale version in comment. This says "currently 3", but RELUNDO_METAPAGE_VERSION is defined as 4 below and every writer/validator (relundo.c, relundo_page.c, relundo_xlog.c) stamps and checks version 4. Update the comment to "currently 4" (or drop the parenthetical) to avoid misleading readers about the persisted on-disk version. [high confidence]
💡 Suggested change
Before:
uint16 version; /* Format version (currently 3); allows future
After:
uint16 version; /* Format version (currently 4); allows future
📄 src/include/access/relundo.h (L30-L30)
Stale documentation. The header claims the counter "enables fast age comparison" here and "O(1) age comparison" in the RelUndoMetaPageData.counter comment, but the counter is no longer used for age/discard comparison. Its only runtime use (relundo.c:1120,1198) is an equality (!=) check for ABA/stale-pointer defense, and discard eligibility is now driven by max_xid (as the RelUndoPageHeaderData.counter comment correctly states: "no longer used for discard eligibility"). These two comments contradict the actual behavior and the page-header comment; reword them to describe the counter's real purpose (generation stamp for stale-verptr rejection). [high confidence]
📄 src/include/access/relundo.h (L185-L185)
Speculative/YAGNI comment. This describes a feature (per-column bitmap for partial updates) that does not exist and has no caller. pgsql-hackers review flags speculative scaffolding in comments; drop it and add such a field only when a consumer needs it. [low confidence]
📄 src/include/access/rmgrlist.h (L54-L56)
Remove the #ifdef USE_FLUX guard around RM_FLUX_ID. Both build systems define USE_FLUX unconditionally, and their own comments state a build with it unset "fails to link" (meson.build: cdata.set('USE_FLUX', 1); src/Makefile.global.in: USE_FLUX = 1). So this guard is dead conditional compilation that can never be false, and it directly violates the PostgreSQL convention against #ifdef-fencing a feature.
Beyond being dead code, it is a WAL-format footgun. This list defines each rmgr's numeric ID (stored in every WAL record) and drives the RmgrIds enum via #include "access/rmgrlist.h" in rmgr.h, as well as RM_NEXT_ID/RM_MAX_BUILTIN_ID. Making RM_FLUX_ID conditional means the same source can produce two incompatible rmgr-ID layouts, and it clashes with the rest of the tree which references FLUX unconditionally (e.g. TWOPHASE_RM_FLUX_ID/TWOPHASE_RM_MAX_ID in twophase_rmgr.h, flux_decode in decode.c). Drop the #ifdef/#endif and register the entry unconditionally, matching the actual always-built reality. (high confidence)
💡 Suggested change
Before:
#ifdef USE_FLUX
PG_RMGR(RM_FLUX_ID, "FLUX", flux_redo, flux_desc, flux_identify, NULL, NULL, flux_mask, flux_decode)
#endif
After:
PG_RMGR(RM_FLUX_ID, "FLUX", flux_redo, flux_desc, flux_identify, NULL, NULL, flux_mask, flux_decode)
📄 src/include/access/relundo_xlog.h (L36-L37)
Duplicate typedef of RelUndoRecPtr. relundo.h (line 80) also defines typedef uint64 RelUndoRecPtr;. Multiple translation units include BOTH headers (e.g. relundo.c, relundo_apply.c, relundo_discard.c, relundo_xlog.c, flux_xlog.c), so they get the typedef twice. Redefinition of a typedef with the same underlying type is a constraint violation under C99 (the tree's baseline) and is rejected by MSVC and strict/older compilers, breaking the portability gate. Do not forward-declare the type here; include the canonical header or extract RelUndoRecPtr into a small shared header (e.g. undodefs.h) included by both.
📄 src/include/access/relundo_xlog.h (L29-L31)
Headers must not include "postgres.h" (nor postgres_fe.h). PostgreSQL convention is that each .c file includes postgres.h/postgres_fe.h first, then headers include only what they specifically need. This header is a frontend consumer via pg_waldump (src/bin/pg_waldump/rmgrdesc.c and the relundodesc.c symlink build with FRONTEND) where pulling in backend postgres.h assumptions is a portability hazard; it will also be flagged by review/pgindent. Drop this include; the types used here (uint16/uint32/uint64, TransactionId, BlockNumber, StringInfo, XLogReaderState, RelFileLocator) come from the already-included c.h-dependent headers.
💡 Suggested change
Before:
#include "postgres.h"
+
+#include "access/xlogreader.h"
After:
#include "access/xlogreader.h"
📄 src/include/access/relundo_xlog.h (L11-L13)
Stale header comment: this "Record types" list enumerates only INIT/INSERT/DISCARD, but the file now defines five ops (XLOG_RELUNDO_TRUNCATE and XLOG_RELUNDO_APPLY are also declared below). Update the comment to describe what the code does now.
| @@ -0,0 +1,41 @@ | |||
| # LiteLLM proxy config — bridges Open Code Review (OpenAI protocol) to AWS Bedrock. | |||
There was a problem hiding this comment.
Non-ASCII em-dash character (U+2014) used in a comment. The contribution standard requires ASCII only in source and diffs (no smart quotes, em-dashes, or ellipsis). Replace the em-dash with an ASCII hyphen/dash.
| # LiteLLM proxy config — bridges Open Code Review (OpenAI protocol) to AWS Bedrock. | |
| # LiteLLM proxy config - bridges Open Code Review (OpenAI protocol) to AWS Bedrock. |
| aws_region_name: os.environ/AWS_REGION | ||
|
|
||
| # "High effort" review. Claude Opus 4.8 on Bedrock uses *adaptive* thinking | ||
| # controlled by output_config.effort. Set it DIRECTLY here — NOT via |
There was a problem hiding this comment.
Non-ASCII em-dash character (U+2014) used in a comment. ASCII-only is required in source and diffs. Replace the em-dash with an ASCII hyphen/dash.
| # controlled by output_config.effort. Set it DIRECTLY here — NOT via | |
| # controlled by output_config.effort. Set it DIRECTLY here - NOT via |
| PG_HISTORY_OUT output markdown path (default /tmp/pg-history.md) | ||
| Writes the markdown to PG_HISTORY_OUT; exits 0 even on soft failures (writes a note). | ||
| """ | ||
| import json, os, subprocess, sys, urllib.request |
There was a problem hiding this comment.
sys is imported but never used anywhere in the file (no sys. reference exists). Drop it to keep the import to what is actually used.
| import json, os, subprocess, sys, urllib.request | |
| import json, os, subprocess, urllib.request |
| capture_output=True, text=True) | ||
| ids = json.loads(out.stdout or "[]") |
There was a problem hiding this comment.
The AWS CLI subprocess result is used without checking its return code. subprocess.run(...) is called without check=True, and out.stdout or "[]" silently falls back to an empty list on any failure (expired/misconfigured OIDC role, missing bedrock:ListInferenceProfiles permission, throttling, wrong region). On failure ids becomes [], opus is empty, and the script prints newer=false and exits 0 — a silent false negative that defeats the workflow's purpose and hides a broken setup. Check the return code (or use check=True) and fail loudly when the CLI errors, e.g.:
out = subprocess.run([...], capture_output=True, text=True)
if out.returncode != 0:
print(f"::error::aws bedrock list-inference-profiles failed: {out.stderr}", file=sys.stderr)
raise SystemExit(1)
ids = json.loads(out.stdout or "[]")
| check-model: | ||
| runs-on: ubuntu-latest |
There was a problem hiding this comment.
The check-model job has no timeout-minutes. It performs OIDC negotiation plus external AWS API calls that can stall (network hang, throttling, slow STS/Bedrock endpoint); without a bound the scheduled job can run until the GitHub default (6h) and burn runner minutes. The repo's pg-ci.yml sets timeout-minutes on every job. Add a small bound (e.g. timeout-minutes: 10) to this job.
| check-model: | |
| runs-on: ubuntu-latest | |
| check-model: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 |
| -- STEP 5: Commit the transaction | ||
| COMMIT; |
There was a problem hiding this comment.
The comment says "Commit the transaction" but no transaction block was ever started -- there is no BEGIN; earlier in the script. Under autocommit, the CREATE TABLE/INSERT/UPDATE/DELETE each commit individually, so this bare COMMIT; runs outside any transaction and emits WARNING: there is no transaction in progress while doing nothing. The comment is also misleading since there is no single "the transaction" to commit. Either add a matching BEGIN; before STEP 2 (as example 02 does with BEGIN/ROLLBACK) or drop STEP 5 and its comment. Confidence: high.
| -- STEP 5: Commit the transaction | |
| COMMIT; | |
| -- STEP 5: Commit the transaction (implicitly committed under autocommit) |
| ifdef USE_FLUX | ||
| SUBDIRS += flux | ||
| endif |
There was a problem hiding this comment.
Out of sync with meson.build and inconsistent with the stated design. In src/backend/access/meson.build subdir('flux') is built unconditionally, and both src/Makefile.global.in (USE_FLUX = 1) and meson (cdata.set('USE_FLUX', 1)) always define USE_FLUX because catalog/rmgr code references flux symbols unconditionally (a build with USE_FLUX unset fails to link). This ifdef USE_FLUX guard is therefore dead scaffolding that can never be false; add flux directly to the unconditional SUBDIRS list to match meson and avoid an impossible-to-disable knob.
| flux_relundo.o \ | ||
| flux_pvs.o | ||
|
|
||
| include $(top_srcdir)/src/backend/common.mk No newline at end of file |
There was a problem hiding this comment.
Missing newline at end of file (the diff shows \ No newline at end of file). PostgreSQL source files, including Makefiles, must end with a newline; the sibling src/backend/access/undo/Makefile added in this same change correctly terminates with one. This will trip git diff --check/pgindent conventions and is a common -hackers rejection reason. Add a trailing newline after the final include line.
Confidence: high.
| include $(top_srcdir)/src/backend/common.mk | |
| include $(top_srcdir)/src/backend/common.mk |
| * Cluster-wide UNDO resource manager: UNDO_RMID_FLUX (=5), FluxUndoRmgrInit | ||
| (undormgrlist.h), used so index inserts on a FLUX table can piggyback their | ||
| index-UNDO onto the table's active UNDO context. | ||
| * Two-phase commit: TWOPHASE_RM_FLUX_ID (=6), flux_twophase_* callbacks. |
There was a problem hiding this comment.
Factual error: TWOPHASE_RM_FLUX_ID is 5, not 6. See src/include/access/twophase_rmgr.h:31 (#define TWOPHASE_RM_FLUX_ID 5). The value 5 is shared with UNDO_RMID_FLUX (which really is 5) on the line above, which likely caused the off-by-one. This drift will mislead maintainers about the two-phase RM identity.
| * Two-phase commit: TWOPHASE_RM_FLUX_ID (=6), flux_twophase_* callbacks. | |
| +* Two-phase commit: TWOPHASE_RM_FLUX_ID (=5), flux_twophase_* callbacks. |
| This README describes FLUX's design and how it differs from heap. For the | ||
| UNDO substrate FLUX relies on, see src/backend/access/undo/README (the | ||
| cluster-wide UNDO engine) and src/include/access/relundo.h (the per-relation | ||
| UNDO fork). The zheap origin design notes are in DESIGN. |
There was a problem hiding this comment.
Dangling cross-reference: no DESIGN file exists in the FLUX access-method directory (or elsewhere in the tree). Either add the referenced design document or drop/fix this reference so the README does not point maintainers at a non-existent file.
There was a problem hiding this comment.
🔍 OCR found 244 issue(s).
- 25 inline, 219 in summary (inline capped at 25)
⚠️ 2 warning(s) during review
📄 src/backend/access/flux/flux_lock.c (L76-L78)
FluxTrackSerializeLock does not record the lock mode, and FluxReleaseSerializeLocks hardcodes ExclusiveLock. This is correct only because the single current call site (flux_operations.c ~2815) locks at LockTupleNoKeyExclusive, which maps to ExclusiveLock. It is a latent footgun: if a future caller tracks a lock taken at LockTupleExclusive (AccessExclusiveLock, as the delete path at flux_operations.c:1594 uses) the release would call LockHeldByMe/LockRelease with the wrong mode, silently leaking the lock. Store the LockTupleMode (or the resolved LOCKMODE) in FluxSerializeLockEnt and release with it.
📄 src/backend/access/flux/flux_fsm.c (L92-L95)
FluxGetCommitTimestamp() is called inside the critical section (after START_CRIT_SECTION()). This function begins with if (FluxMvccShmem == NULL) elog(ERROR, "FLUX MVCC not initialized") (flux_mvcc.c:359-360); an ERROR raised inside a critical section is promoted to PANIC. Every other call site in this feature deliberately fetches the timestamp before the critical section -- see flux_handler.c:2701-2704, whose comment explicitly states "Get timestamp BEFORE entering critical section, as this may allocate memory." Move FluxGetCommitTimestamp() above START_CRIT_SECTION() to keep the crit-section allocation- and ERROR-free. (moderate confidence)
📄 src/backend/access/flux/flux_handler.c (L633-L635)
direction is accepted but never used: this function always scans forward regardless of the requested ScanDirection. heap_getnextslot honors the direction (via heapgettup) so that FETCH BACKWARD on a scrollable cursor and es_direction == BackwardScanDirection return tuples in reverse. As written, a backward fetch over a FLUX table keeps advancing forward and returns wrong results. Either implement backward iteration over rs_vistuples/blocks, or (if backward scans are genuinely unsupported) reject !ScanDirectionIsForward(direction) explicitly rather than silently mis-scanning.
📄 src/backend/access/flux/flux_handler.c (L1443-L1445)
flux_index_entry_key_mismatch calls BuildIndexInfo() + CreateExecutorState() + FreeExecutorState() on every returned tuple. Because every FLUX table is a delete-marking table (am_index_delete_marking = true) and xs_want_itup_recheck is set for all of them, this recheck runs once per index tuple in the hot index-scan loop, adding a fresh catalog-driven IndexInfo build and full EState setup/teardown per row. That is an O(rows) executor-state churn no other AM incurs on the index-scan hot path. Build the IndexInfo and EState once (e.g. cache them in IndexScanFluxData at flux_index_scan_begin, reset the per-tuple context between rows) and free them at scan end.
📄 src/backend/access/flux/flux_handler.c (L507-L509)
rs_vistuples is allocated in the backend-lifetime TopMemoryContext but only freed in flux_scan_end (pfree(scan->rs_vistuples)). The FluxScanDescData itself lives in the caller's context, so if the scan is torn down by an error/ResourceOwner unwind before flux_scan_end runs, the scan struct is reclaimed with its context while this MaxOffsetNumber-sized buffer is orphaned in TopMemoryContext -- a cumulative leak across aborted scans. heap uses a fixed inline rs_vistuples[MaxHeapTuplesPerPage] in the scan descriptor precisely to avoid this. Either make it an inline array in FluxScanDescData or allocate it in the same context as the scan descriptor. (Same pattern in the bitmap path below.)
📄 src/backend/access/flux/flux_handler.c (L2586-L2588)
buffer was ReadBuffer'd and locked at the top of this function and is never invalidated before here, so BufferIsValid(buffer) is always true: the else branch that does a raw, non-atomic tuple_hdr->t_flags &= ~FLUX_TUPLE_UNCOMMITTED under only a SHARE lock is dead code. Besides being unreachable, the raw write it performs would be unsafe (it bypasses BufferSetHintBits16, racing concurrent hint-bit writers). Drop the dead else branches and keep only the BufferSetHintBits16 path. (Two occurrences in this SNAPSHOT_DIRTY block.)
📄 src/backend/access/flux/flux_handler.c (L627-L627)
Non-ASCII punctuation in a comment (arrow character). Project style requires ASCII-only source; replace with an ASCII arrow such as "->".
📄 src/backend/access/flux/flux_handler.c (L4464-L4464)
Stray doubled percent in prose (not a printf format string): "~9%%" should read "~9%".
📄 src/backend/access/flux/flux_handler.c (L1922-L1922)
Non-ASCII em-dash (U+2014) in comments. This file uses em-dashes pervasively (also lines 1992, 2017, 2034, 2038, 2314, 3284, 3840, etc.); the project's ASCII-only source rule requires converting all of them to ASCII ("--" or " - "). pgindent/CI comment-style gates will reject the file otherwise.
📄 src/backend/access/flux/flux_lock.c (L117-L121)
The backend-local tracking set (flux_serlocks) has no lifecycle handling for the 2PC PREPARE path. FluxReleaseSerializeLocks runs only at XACT_EVENT_COMMIT and FluxForgetSerializeLocks only at XACT_EVENT_ABORT (see FluxXactCallback in flux_mvcc.c); the XACT_EVENT_PREPARE case clears MyFluxXactState but never touches this set. However, AtPrepare_Locks transfers these xact-level LOCKTAG_TUPLE(ExclusiveLock) locks to the prepared xact's dummy proc and RemoveLocalLock()s them from this backend, so after PREPARE the flux_serlocks entries are stale but persist across transaction boundaries. Consequences in the same backend: (1) the next transaction's FluxTrackSerializeLock dedup check can spuriously match a stale (dbid,relnumber,blkno,offnum) and skip tracking a genuinely-new lock, which then never gets released at that transaction's commit -- leaking a held LOCKTAG_TUPLE into the held-at-commit assertion this whole mechanism exists to avoid; (2) unbounded growth of the set until a later non-2PC commit/abort. The tracking set must be cleared (forget-without-release, since the lock now belongs to the prepared xact) at PREPARE. Fix the callback to call FluxForgetSerializeLocks() on XACT_EVENT_PREPARE, and update this comment which currently documents only COMMIT/ABORT as the complete lifecycle.
📄 src/backend/access/flux/flux_lock.c (L82-L87)
FluxTrackSerializeLock does a linear O(n) scan of flux_serlocks on every call to dedup, and it is invoked once per updated row (flux_operations.c:2815). A single transaction that UPDATEs N distinct FLUX rows therefore costs O(N^2). For large write transactions this is a measurable regression versus heap's per-tuple lock bookkeeping. Consider a dynahash/simplehash keyed on (dbid,relnumber,blkno,offnum), consistent with the project's DRY preference for existing hash infrastructure over open-coded linear scans.
📄 src/backend/access/flux/flux_mvcc.c (L760-L767)
Dead scaffolding: serializable_horizon and the mvcc_lock that guards it are never read anywhere in the tree (verified: the field is only written here and in the two init paths; mvcc_lock is acquired only around this write). It is a monotonically-decreasing value initialized to 1, so even if a reader existed it would pin at the smallest commit ts. Per the project's YAGNI/minimalism rule, remove serializable_horizon, the mvcc_lock LWLock, and this entire block; SSI is delegated to predicate.c which the header comment already states.
📄 src/backend/access/flux/flux_mvcc.c (L278-L281)
Dead code / duplicate init path. FluxMvccShmemInit() has no caller anywhere in the tree; shared memory is actually set up via FluxMvccShmemInit_cb, which is registered through PG_SHMEM_SUBSYSTEM(FluxMvccShmemCallbacks) in subsystemlist.h. Maintaining two divergent copies of the same field-initialization logic (this function vs. FluxMvccShmemInit_cb) is a drift hazard: adding a field to one and not the other silently leaves it uninitialized on one path. Remove the unused FluxMvccShmemInit() (and its declaration in flux.h), keeping only the subsystem-callback path.
📄 src/backend/access/flux/flux_mvcc.c (L148-L149)
Non-ASCII em-dash (U+2014) in source; the project mandates ASCII-only source (also at lines 525 and 725). Additionally, the comment claims "volatile access", but xact_start_ts_slots is declared as plain uint64 (no volatile) and accessed via pg_read_barrier/pg_write_barrier, so the comment is inaccurate. Replace the em-dash with -- and drop the "volatile access" wording. Note also the single-writer claim is weakened by FluxPrepareReassignSlot/FluxResolvePreparedSlot which write another proc's slot.
📄 src/backend/access/flux/flux_mvcc.c (L525-L525)
Non-ASCII em-dash (U+2014); replace with ASCII -- per the ASCII-only source rule.
📄 src/backend/access/flux/flux_mvcc.c (L725-L725)
Non-ASCII em-dash (U+2014); replace with ASCII -- per the ASCII-only source rule.
📄 src/backend/access/flux/flux_mvcc.c (L886-L889)
Portability (low confidence, low impact): xact_start_ts_slots[i] and oldest_active_ts are 64-bit values shared across backends and read/written concurrently with only memory barriers, not atomics. On 32-bit platforms a plain uint64 load/store is not guaranteed atomic, so a reader can observe a torn value. I verified the consumer (FluxGetOldestActiveTimestamp) only feeds the cosmetic page commit-ts word in the DEFRAG WAL record (the actual reclamation and VM all-visible gates use the XID horizon FluxGetOldestXminHorizon), so this is not a correctness/data-loss hazard -- but a torn 64-bit read is still non-portable. Prefer pg_atomic_uint64 (pg_atomic_read_u64/write_u64) for these shared 64-bit fields to satisfy the portability gate.
📄 src/backend/access/flux/flux_handler.c (L3613-L3618)
The bottom-up free-space accumulator is reset to 0 on every iteration, so actualfreespace only ever holds a single entry's freespace and never accumulates across all deletable entries. This defeats the bottomupfreespace early-stop target: heap_index_delete_tuples() declares actualfreespace once outside the loop and adds each deletable entry's freespace to it (heapam.c:8364). As written, FLUX either stops far too late (only when one entry alone meets the whole target, which is rare) or effectively never stops early, so bottom-up index deletion does less useful work than costed. Declare the accumulator outside the loop.
📄 src/backend/access/flux/flux_handler.c (L3096-L3098)
Stale comment: it describes the pre-pivot HLC design. This file's own header and the extensive comments elsewhere (e.g. flux_index_fetch_tuple, flux_tuple_fetch_row_version) state that FLUX now uses heap-shaped xmin/xmax + CLOG for visibility, and this very function calls FluxTupleVisibleToSnapshotDual (xmin/xmax based). The claim "timestamp-based MVCC and ... concurrent modifications" no longer matches the code. Same stale wording recurs at lines 3509, 3711, 3774, and 4050 ("FLUX uses timestamp-based MVCC rather than xmin/xmax", "visibility is determined by the tuple's timestamp-based MVCC"). Update to reflect the current heap-shaped xmin/xmax + CLOG model.
📄 src/backend/access/flux/flux_mvcc.c (L717-L728)
SSI conflict-out is missed for committed-but-concurrent writers. flux_header_dirty_xid() only returns a valid XID when the writer is still TransactionIdIsInProgress(); it returns InvalidTransactionId for an inserter/deleter that has already committed. That is exactly the common conflict-out case that heap's HeapCheckForSerializableConflictOut handles: for a not-visible-to-us tuple it takes t_xmin/update-xid regardless of in-progress state and only bails via TransactionIdPrecedes(xid, TransactionXmin). Here a transaction that committed after our snapshot was taken (xid >= TransactionXmin) yields Invalid -> early return -> no CheckForSerializableConflictOut() call, so the rw-antidependency is never recorded. This silently weakens the SERIALIZABLE guarantee the file's ISOLATION LEVEL header claims to provide ("full SSI"). The rationalizing comment ("committed so long ago that it's no longer tracked") is incorrect: a millisecond-old committed concurrent xid is still within the snapshot window and is precisely what must be caught. The writer XID must be derived from the header xmin/xmax and gated on TransactionIdPrecedes(xid, TransactionXmin) rather than on in-progress status. (moderate-high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L182-L185)
Dead fields. rc_read_point_cid and rc_read_point_xcc are only zeroed in FluxInitTransactionState() and never read anywhere in the tree, yet the 5-line comment above describes an elaborate "READ COMMITTED per-command read point" mechanism that is not implemented. is_read_only is likewise only ever set to true at init and never read or set to false. Remove the unused fields and the aspirational comment, or wire them up.
📄 src/backend/access/flux/flux_mvcc.c (L212-L213)
epq_reconcile_verptr is write-only dead scaffolding: it is assigned in FluxEpqReconcileMark() but never read. FluxEpqReconcileMatches() deliberately ignores verptr/xid identity (it matches on (relid, tid, cid) only, as its own comment states), so this field has no consumer. Moreover this comment claims dedup keys on the "(verptr, xid) marker" identity and that "a strictly-newer committer stamps a NEW record with a fresh verptr, so dedup only suppresses re-firing on the identical marker" -- which directly contradicts the actual matching logic. Remove the unused field and fix the stale comment to describe the (relid, tid, cid) match that is actually performed.
📄 src/backend/access/flux/flux_operations.c (L502-L505)
Return type/contract mismatch. FluxPagePruneOpt is declared int and the doc comment says "Returns the number of tuples pruned", but the success path returns true (line 504) while cleanup paths return 0. Callers (flux_tuple_insert) only test truthiness, but a true for a count is misleading and any future consumer relying on the documented count will be wrong. Either change the return type to bool and fix the doc comment, or actually return ndead.
Confidence: high.
📄 src/backend/access/flux/flux_operations.c (L2968-L2969)
t_writer CAS lock can leak permanently on an ereport. FluxTupleWriterTryLock stamps MyProcNumber+1 directly into the on-page tuple header (an in-page value, NOT tracked by any resource owner). Between this trylock and the matching FluxTupleWriterUnlock, several operations can longjmp: palloc (cas_old_copy/cas_full_image/cas_delta_buf), CheckForSerializableConflictIn (line ~3318, ereports on serialization failure), and FluxPbuPrepareInsert -> PrepareUndoInsert (its own comment: "may palloc / extend the undo log / ereport"). On such a throw the buffer content lock is released by LWLock cleanup, but t_writer stays set to the aborting backend's value. The tuple then appears permanently write-locked: every future CAS fast-path attempt on that row fails the trylock and falls to the slow path -- a permanent hot-row fast-path denial with no cleanup. Wrap the CAS section that can throw in PG_TRY/PG_CATCH and FluxTupleWriterUnlock(old_tuple_hdr) in the catch before re-throwing (as the exclusive path already does for its overflow buffers).
Confidence: high.
📄 src/backend/access/flux/flux_operations.c (L66-L66)
Dead macro. MAX_OVERFLOW_PTRS_PER_TUPLE is defined here but has no use anywhere in the tree (its only occurrence is this definition), and the VACUUM code in this same file explicitly states FLUX has no on-page overflow (wide values use TOAST). Remove the macro and its comment.
Confidence: high.
📄 src/backend/access/flux/flux_operations.c (L5538-L5538)
100%% is only meaningful inside a printf format string; this is a plain C comment, so the doubled percent is a typo. Use 100%.
Confidence: high.
📄 src/backend/access/flux/flux_operations.c (L3495-L3495)
Development-history comments must not ship. Throughout this file, comments reference internal development phases and bug-tracking labels that describe how the code got here rather than what it does now: "Phase 8b/8c", "BUG 2 fallback", "BUG 5 fix", "BUG 5:", "FIXME(flux hot-row scaling)", and "Future enhancement:". These are exactly the aspirational/history comments the project rejects -- a reader has no access to the phase/bug numbering. Rewrite them to explain the current behavior and rationale, or delete them. (This particular line and its twin at line ~5310 add nothing beyond a dangling label; the code below is self-documenting.)
Confidence: high.
📄 src/backend/access/flux/flux_operations.c (L3398-L3398)
Torn-read hazard: this in-place overwrite runs a non-atomic multi-word memcpy of the whole tuple header (t_flags, t_xmin, t_xmax/t_commit_ts, t_ctid, ...) while holding only BUFFER_LOCK_SHARE_EXCLUSIVE, which (per bufmgr.c) is compatible with concurrent plain-SHARE readers. The MVCC reader path (FluxTupleSatisfiesMVCC) dereferences tuple->t_flags / FluxTupleGetXmin / FluxTupleGetXmax directly off the buffer with no seqlock or barrier. A concurrent SHARE reader can therefore observe a half-written header -- e.g. the new t_flags (UNCOMMITTED set) paired with a stale t_xmin, or a partially-written t_xmin -- producing a wrong visibility verdict. The rationale comment above relies on "marked UNCOMMITTED and xmin is the in-progress updater", but those very fields are part of the same non-atomic memcpy, so their mutual consistency is not guaranteed mid-copy, and this is worse on weakly-ordered architectures (ARM64/PPC64). The readers need a seqlock/version-counter or the write needs to publish the header atomically (e.g. write body first, then flags with a release barrier).
Confidence: medium (hazard is real; whether a barrier/seqlock exists elsewhere in the reader path could not be confirmed in this file).
📄 src/backend/access/flux/flux_pvs.c (L179-L184)
Per-step updater visibility is inconsistent with the rest of FLUX MVCC and can expose an aborted (rolled-back) update's image. Here a candidate is served whenever !XidInMVCCSnapshot(urec_xid, snapshot) is true, but that is also true for an aborted xid that has left the running list / is below the snapshot xmax. The authoritative path FluxTupleSatisfiesMVCC (flux_mvcc.c:1226-1233) and the lost-update probe FluxTupleHasCommittedUpdateAfter (flux_mvcc.c:1048) both additionally require TransactionIdDidCommit() before treating an out-of-snapshot xid as "visible". FLUX rollback undo is applied asynchronously by undo workers, so during that window the on-page/chain image can still carry an aborted update whose xid is no longer in-progress; this path would then serve that phantom before-image. The INSERT-origin branch below correctly guards with TransactionIdDidCommit(ins_xid) && !XidInMVCCSnapshot(...); the per-step branch must be symmetric. (high confidence)
💡 Suggested change
Before:
if (!XidInMVCCSnapshot(urec_xid, snapshot))
{
if (payload != NULL)
pfree(payload);
break;
}
After:
if (!XidInMVCCSnapshot(urec_xid, snapshot) &&
(TransactionIdIsCurrentTransactionId(urec_xid) ||
TransactionIdDidCommit(urec_xid)))
{
if (payload != NULL)
pfree(payload);
break;
}
📄 src/backend/access/flux/flux_pvs.c (L103-L103)
The chain walk reads header fields (FluxTupleGetVersionPtr->hdr->t_verptr at offset 8, FluxTupleGetXmin->hdr->t_xmin) from candidate without ever verifying candidate_len >= sizeof(FluxTupleHeader). For a reconstructed image, candidate_len is phdr.tuple_len or delta->old_len, both taken from a potentially corrupted undo record. A record whose tuple_len/old_len is smaller than the 40-byte header would over-read the palloc'd buffer on the next iteration. Since this function's stated purpose is to defend against corrupted records (see the depth cap), add a header-size guard when computing hdr/reading verptr. (medium confidence)
📄 src/backend/access/flux/flux_pvs.c (L6-L7)
The file-header comment describes the version pointer as a "trailing UndoRecPtr (verptr)" stamped "into the new on-page image" and refers repeatedly to a tuple's "own trailing verptr". That design was abandoned: per flux.h (t_verptr comment, lines 152-160 and FluxTupleGetVersionPtr, lines 609-622) the verptr is now a fixed, aligned header field t_verptr at offset 8 -- the trailing/item_len - 8 scheme no longer exists. Update the prose to describe the header field so the comment matches current behavior. (high confidence)
📄 src/backend/access/flux/flux_pvs.c (L96-L96)
The tid parameter is documented "currently unused but reserved for future diagnostics" and discarded via (void) tid;, yet it is dereferenced in the depth-cap WARNING (ItemPointerGetBlockNumber(tid) / ItemPointerGetOffsetNumber(tid)). Two issues: (1) the "reserved for future" wording is an aspirational comment for a param that is in fact used -- drop the (void) tid; and the "unused" claim; (2) all current callers pass a non-NULL tid, so this is not a live crash, but the contradictory contract invites a future NULL caller that would then crash only on a corrupted deep chain. Reconcile the comment with actual usage. (high confidence)
📄 src/backend/access/flux/flux_pvs.c (L203-L204)
Casting an UndoRecPtr (uint64) to unsigned long long and printing with %llu deviates from the PostgreSQL convention of UINT64_FORMAT used elsewhere in the undo subsystem (e.g. flux_pbu.c:453, pbu_undorequest.c:1164). Note the sibling file flux_undo.c already uses the same %llu+ULL pattern, so this is a subsystem-wide convention drift rather than a portability bug; prefer UINT64_FORMAT for new code. (low confidence)
📄 src/backend/access/flux/flux_pvs.c (L113-L113)
This loop can iterate up to FLUX_PVS_MAX_CHAIN_DEPTH (10000) times, each doing a FluxPbuFetchVersion undo fetch, with no CHECK_FOR_INTERRUPTS. Sibling FLUX scan loops (flux_handler.c, flux_operations.c) include CHECK_FOR_INTERRUPTS; a long chain walk here is not cancelable. Consider adding CHECK_FOR_INTERRUPTS at the top of the loop. (low confidence)
📄 src/backend/access/flux/flux_pbu.c (L166-L169)
Static prepared-state is not crash/abort-safe. flux_pbu_prepared (plus the pinned undo buffers and the uur_payload StringInfo) is only reset by FluxPbuFinishUndo/FluxPbuCancelUndo, and there is no xact/subxact abort callback that clears it. The UPDATE caller runs FluxPbuPrepareInsert() inside a PG_TRY whose PG_CATCH (flux_operations.c ~5148) releases overflow buffers and re-throws WITHOUT calling FluxPbuCancelUndo(); the DELETE caller then does GetCurrentTransactionId() before the crit section. If any of these throw after prepare (e.g. OOM in FluxXLogPrepareLogicalImage, which the caller comment explicitly says can ereport), flux_pbu_prepared stays true, the pinned undo buffers leak, and the next FLUX DML in the same backend trips Assert(!flux_pbu_prepared) (assert builds) or silently reuses stale state and calls UnlockReleaseUndoBuffers()/UndoLogBuffersSetLSN() on already-released buffers (production). Ensure every path between prepare and finish is either error-free or wrapped so FluxPbuCancelUndo() runs on error, and reset this state on (sub)transaction abort.
📄 src/backend/access/flux/flux_pbu.c (L289-L293)
FluxPbuCancelUndo() has no callers anywhere in the tree. The doc comment says it is used for a "same-size CAS update that finds no byte actually changed", but that path (flux_operations.c ~3291-3315) returns before preparing any undo record, so nothing needs cancelling there. As written this is dead code, and its absence from the DML error paths is the root of the state-poisoning issue flagged in FluxPbuPrepareInsert. Either wire it into the abort/error paths (prepare-without-commit) or remove it and fix the stale comment.
📄 src/backend/access/flux/flux_pbu.c (L228-L228)
uur_payload.len (an int) is silently truncated to uint16 here, and FluxPbuRedoUndo then hard-PANICs on datalen != SizeOfFluxUndo + payload_len. Today FLUX before-images are bounded by FLUX_MAX_TUPLE_SIZE (< 64KB even at BLCKSZ=32768), so this is not reachable, but the truncation is a silent data-loss footgun if that bound ever changes or a delta payload grows. Add an explicit guard (e.g. an elog(ERROR)/Assert that uur_payload.len <= PG_UINT16_MAX) instead of relying on the invariant holding forever.
📄 src/backend/access/flux/flux_pbu.c (L735-L739)
If rm_undo (flux_undo_apply) throws mid-loop, the PG_FINALLY only resets flux_pbu_in_subxact_apply; the just-fetched urec (palloc'd by UndoFetchRecord) and any buffer it pinned are not released here, and the in-loop UndoRecordRelease/UnlockReleaseBuffer are skipped. flux_undo_apply does take a buffer lock and palloc, so it can ereport(ERROR). The leak is bounded by the enclosing subtransaction-abort resource-owner/memory-context cleanup, so it is not a hard corruption, but the FINALLY should also release the in-flight urec for cleanliness (and to avoid relying on outer cleanup for the fetched record's buffers).
📄 src/backend/access/flux/flux_operations.c (L4540-L4542)
Memory leak: flux_tuple_update_inplace toasts wide varlena columns here (setting upd_toasted true and populating upd_ttc), but flux_toast_cleanup(&upd_ttc) is never called on any exit path. flux_toast_tuple -> toast_tuple_init/toast_tuple_externalize palloc working buffers (and, when oldvalues are supplied, schedule superseded-datum deletion); the insert path correctly balances this with flux_toast_cleanup(&ttc) (line 1051), but the update path does not. Every in-place UPDATE of a row with a toastable column leaks the staging buffers into the transaction memory context, which accumulates on UPDATE-heavy workloads. Add a flux_toast_cleanup(&upd_ttc) call guarded by upd_toasted on the successful in-place path (and the out-of-place fallback re-drives via flux_tuple_update_inplace so its own toasting is separate). Confidence: high.
📄 src/backend/access/flux/flux_pvs.c (L270-L272)
The delta path uses delta->old_len (read straight from the untrusted undo payload) to size the allocation before any validation, unlike the FLUX_UNDO_UPDATE case above which first checks payload_size >= SizeOfFluxUndoPayloadHeader + phdr.tuple_len. FluxUndoDeltaReconstruct only bounds-checks old_len against delta_len after this palloc. On a corrupt record with a bogus old_len, palloc(next_len) can either request up to ~1GB or elog(ERROR) on an oversized request, aborting the reader query mid-read -- defeating the best-effort/WARNING-based degradation the rest of this function is built around. Validate old_len (e.g. SizeOfFluxUndoPayloadHeader + delta->prefixlen + delta->suffixlen <= payload_size and a sane upper bound) and break with a WARNING before allocating, mirroring the full-tuple path.
📄 src/backend/access/flux/flux_operations.c (L5851-L5852)
Dead code: FluxVacuumCrossPageDefrag returns early whenever nindexes > 0, so every if (nindexes > 0) branch below this point is unreachable -- the FluxDefragMove struct, the estate/slot/indexInfoArray setup, the per-source-page deferred_moves allocation, and the entire deferred index-insert replay loop can never execute. That is a large block of unreachable machinery (including index_insert calls that the top-of-function comment explains can never run). Either drop the nindexes > 0 support code entirely and simplify the signature (it only ever runs with no indexes), or remove the early return if index-aware relocation is actually intended. Confidence: high.
📄 src/backend/access/flux/flux_pbu.c (L378-L386)
Silent loss of undo replay when the forward-op record has many block references. The redo scan window is first_block_id .. first_block_id + FLUX_PBU_MAX_UNDO_BLOCKS (i.e. 4 blocks), and every UPDATE/DELETE redo caller passes first_block_id = 1 (flux_xlog.c:1971,2038,2551). But at DO-time the undo pages are folded in AFTER all forward-op blocks: flux_undo_block_id = next_block_id after registering block 0, up to MAX_OVERFLOW_BUFFERS (=32) unique overflow blocks, and the cross-page destination block (flux_xlog.c:596-610). So an UPDATE with more than ~3 unique overflow/cross-page block references lands its undo region at block_id >= 5. This loop then continues past the overflow blocks 1..4 and exits at the block_id < first_block_id + FLUX_PBU_MAX_UNDO_BLOCKS bound WITHOUT ever inspecting the undo block, so FluxPbuRedoUndo returns as a no-op. The before-image is never rebuilt on the standby / after crash, silently destroying the ability to roll the transaction back -- a crash-safety/corruption hazard. The scan must cover up to XLR_MAX_BLOCK_ID (the undo pages are already tagged and skippable via FluxPbuBlockIsUndo), not a fixed 4-block window anchored at block 1.
📄 src/backend/access/Makefile (L25-L28)
Minor consistency nit: the three new subdirs are appended in an ad-hoc order that neither follows the near-alphabetical convention of the surrounding list nor matches the order in src/backend/access/meson.build (which lists flux, recno before nbtree and undo last). Ordering has no build effect here (subdirs build independently, linking is top-level), but keeping the two build systems in the same order eases review and future maintenance. Consider sorting these (e.g. flux, recno, undo) to match meson.build.
💡 Suggested change
Before:
transam \
+ undo \
+ flux \
+ recno
After:
transam \
+ flux \
+ recno \
+ undo
📄 src/backend/access/flux/flux_xlog.c (L1065-L1065)
Two forward declarations share one line, which will not pgindent cleanly and is inconsistent with the surrounding one-declaration-per-line block. Split them.
💡 Suggested change
Before:
+static void flux_xlog_delete_redo(XLogReaderState *record);static void flux_xlog_defrag_redo(XLogReaderState *record);
After:
+static void flux_xlog_delete_redo(XLogReaderState *record);
+static void flux_xlog_defrag_redo(XLogReaderState *record);
📄 src/backend/access/flux/flux_tuple.c (L354-L357)
flux_form_tuple_internal() carries dead scaffolding. Per the comment two lines below, FLUX never compresses attributes and has no on-page overflow, so has_compressed/has_overflow stay false, the is_compressed/is_overflowed arrays are never written, and the later pfree loop over them is unreachable. work_values is only ever a verbatim memcpy of values. The force_shrink, old_ovptrs and old_ovpresent parameters threaded through this function are likewise unused in the body. Recommend collapsing this to a single form path (no work_values duplication, no tracking arrays, no unused params) to match YAGNI and avoid misleading future readers.
📄 src/backend/access/flux/flux_tuple.c (L291-L295)
FluxFormTupleUpdate() has no callers anywhere in the tree (only this definition and its declaration in flux.h). It exists solely to pass old_ovptrs/old_ovpresent into flux_form_tuple_internal(), which ignores them. This is dead API surface; remove it (and the unused parameters it feeds) unless a caller is added in this same patch.
📄 src/backend/access/flux/flux_stats.c (L147-L150)
This FLUX_TUPLE_COMPRESSED branch (and the FluxCompressionHeader read below) is dead: the tuple form path in flux_tuple.c never sets FLUX_TUPLE_COMPRESSED (has_compressed is hard-wired false, and the file repeatedly states "FLUX does not compress attributes"). Reading comp_hdr->orig_size/comp_size from a struct that is never written yields meaningless stats if the flag were ever set by accident. Either drop the compression-stats path or gate the whole feature behind code that actually writes it.
📄 src/backend/access/flux/flux_slot.c (L368-L376)
For cmin/cmax on a tuple not written by the current transaction, FluxLocalCidGet returns InvalidCommandId and this falls through to SLogTupleLookupFiltered with GetTopTransactionIdIfAny(). When called outside a transaction that xid is InvalidTransactionId, and SLogTupleLookupFiltered treats an invalid filter as "return ALL active ops for this TID" (see slog_tuple.c). The first matching op can belong to an unrelated in-progress transaction, so cmin/cmax could surface another backend's command id rather than InvalidCommandId. Filter must be pinned to the current xact (skip the lookup entirely when GetTopTransactionIdIfAny() is invalid).
📄 src/backend/access/flux/flux_vm.c (L547-L547)
Aspirational / future-tense comments describe behavior that has not shipped ("A future optimization could...", "reserved for future timestamp checking"). Per project convention, comments must describe what the code does now, not speculative future work. Remove or rewrite to explain the current unconditional-clear behavior.
📄 src/include/access/flux.h (L16-L18)
Same convention violation: a header must not include postgres.h. .c files include it first; a header that does breaks the FRONTEND include path and is contrary to the tree's convention. Remove it and rely on the includer having pulled in postgres.h.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/heapam.h"
After:
#include "access/heapam.h"
📄 src/include/access/flux.h (L815-L817)
These three declarations (FluxDirtyMapShmemSize/Init/Callbacks) are duplicated verbatim in flux_dirtymap.h. Duplicate extern declarations across headers can silently drift if one signature changes. The DirtyMap interface belongs solely in flux_dirtymap.h; drop these here and #include "access/flux_dirtymap.h" if flux.h needs them.
📄 src/include/access/flux.h (L204-L206)
The FluxTupleWriterTryLock/Unlock/IsLocked macros have no callers anywhere in src/backend/access/flux/*.c: the code manipulates t_writer directly as a plain uint32 (e.g. tuple->t_writer = 0), never through these atomics macros or pg_atomic_compare_exchange_u32. This is dead scaffolding; per YAGNI it should be removed. Separately, the comment's portability claim ("safe on all platforms because pg_atomic_uint32 is {volatile uint32}") is coincidentally true only because atomics.h #errors out when there is no native U32 CAS (the spinlock fallback simulates u64 only), but there is no need to assert this for unused code.
📄 src/include/access/flux.h (L316-L318)
The column-level overflow-pointer machinery is dead code: FluxIsOverflowPtr, FluxGetOverflowPtr, the FluxOverflowPtr struct, and FLUX_OVERFLOW_PTR_MAGIC have zero references in src/backend/access/flux/*.c. flux_tuple.c's flux_form_tuple_internal explicitly states "FLUX has no on-page overflow mechanism; wide varlena values are TOASTed by the caller ... FLUX does not compress attributes." This inline (and the related FluxStoreOverflowColumn/FluxFetchOverflowColumn/compression declarations) is speculative scaffolding for a path that is not wired up. Additionally, as written this inline reads a 4-byte ov_magic via VARDATA_ANY(ptr) with no alignment guarantee and would misclassify any user varlena that happens to begin with 0x52564F50. Remove the unused overflow-pointer/compression API rather than shipping it.
📄 src/include/access/flux_undo.h (L172-L173)
FluxUndoDeltaCompute can underflow when force_offset is close to old_len. In the force branch, suffixlen = old_len - 1 - force_offset wraps to a huge uint32 if force_offset >= old_len - 1, and there is no subsequent re-check that prefixlen + suffixlen <= old_len, so middle_len = old_len - prefixlen - suffixlen is computed from wrapped operands and feeds the trailing memcpy(out + SizeOfFluxUndoDelta, old_image + prefixlen, middle_len) -> OOB. Current callers pass force_offset = offsetof(t_verptr) = 8 with old_len >= FLUX_TUPLE_OVERHEAD (40), so it is not reachable today, but this inline is a shared, easy-to-misuse primitive on the DML write path. Add a defensive guard (e.g. require force_offset + 1 < old_len, or clamp and bail to a full before-image) so a future caller cannot wrap it.
📄 src/include/access/flux.h (L136-L136)
On-disk tuple struct carries t_gen documented as "RESERVED, unused in FLUX ... a future slimming pass may remove it", and multiple comment blocks describe removed/future behavior ("Formerly t_xid_hint", "t_cid removed", "MultiXact support has been removed"). Comments must describe current behavior, not history or aspirational intent, and a reserved dead field baked into an on-disk header is expensive to remove later (YAGNI). Either drop t_gen now while the format is not yet in production, or document precisely why the geometry must stay 40 bytes; trim the historical narration.
📄 src/backend/access/hash/hash_undo.c (L250-L257)
Index-corruption hazard (high confidence): this marks whatever ItemId currently sits at the stored blkno/offset LP_DEAD, with no verification that it is still the tuple this transaction inserted. Hash pages are compacted (PageIndexMultiDelete in _hash_vacuum_one_page) and tuples are relocated across pages by _hash_squeezebucket() (hashovfl.c) and bucket splits (hashpage.c), all of which shift/renumber offsets. Between insert and abort the stored offset can therefore point at a live tuple belonging to an unrelated transaction, silently killing it and producing wrong results.
Hash's own kill-prior-tuple path (_hash_kill_items, hashutil.c:594) matches the target by heap TID (ItemPointerEquals(&ituple->t_tid, &currItem->heapTid)) precisely to avoid killing a reused TID. This UNDO payload stores no heap TID, so it cannot perform the same guard. Store the heap TID in HashUndoInsert and verify it before ItemIdMarkDead, mirroring _hash_kill_items.
📄 src/backend/access/hash/hash_undo.c (L256-L259)
Marking the ItemId LP_DEAD without setting LH_PAGE_HAS_DEAD_TUPLES on the page opaque and without decrementing metapage hashm_ntuples leaves the marking ineffective for space reclamation and the tuple count inconsistent (high confidence). _hash_doinsert only invokes _hash_vacuum_one_page when H_HAS_DEAD_TUPLES(pageopaque) is true (hashinsert.c:140), and the native kill path sets LH_PAGE_HAS_DEAD_TUPLES after marking (hashutil.c:623). Because this UNDO path never sets that hint, the space is never reclaimed by the insert-time cleanup, defeating the stated "zero-VACUUM" goal. Set opaque->hasho_flag |= LH_PAGE_HAS_DEAD_TUPLES here (and correspondingly in the WAL redo handler).
📄 src/backend/access/hash/hash_undo.c (L146-L151)
Every reachable call lands here and hits an unconditional error (high confidence). RelationUndoEngine() (tableam.c:807) asserts and returns UNDO_ENGINE_PERBACKEND for any AM with am_supports_undo set (the fork engine was removed). Since HashUndoLogInsert is only called when RelationAmSupportsUndo(heapRel) is true, this condition is always true, hash_undo_write_perbackend() is always taken, and it unconditionally elog(ERROR)s ("per-backend index UNDO is not wired until Phase 9/10"). Consequently a hash index on a non-delete-marking UNDO table AM would fail every insert. The UndoBufferIsActive/UndoRecordSetCreate "design (a)" branch below is unreachable dead code for the same reason.
Also note the only two UNDO-supporting AMs in this series (flux, recno) both set am_index_delete_marking = true, so the hashinsert.c gate !RelationSupportsDeleteMarking(heapRel) never lets them reach this function either -- making the whole module dead scaffolding today. Per YAGNI this should not be committed until an AM actually drives it.
📄 src/backend/access/hash/hash_undo.c (L320-L324)
Inconsistent length guard (low confidence): the apply path rejects records shorter than SizeOfHashUndoInsert, but this desc guard only checks payload_len >= sizeof(Oid). For a truncated record whose length falls between sizeof(Oid) and SizeOfHashUndoInsert this still reads the leading Oid. Use SizeOfHashUndoInsert here to match the apply-side check.
💡 Suggested change
Before:
if (payload_len >= sizeof(Oid) && info == HASH_UNDO_INSERT)
{
Oid index_oid;
memcpy(&index_oid, payload, sizeof(Oid));
After:
if (payload_len >= SizeOfHashUndoInsert && info == HASH_UNDO_INSERT)
{
Oid index_oid;
memcpy(&index_oid, payload, sizeof(Oid));
📄 src/backend/access/hash/hashinsert.c (L243-L249)
Two adjacent block comments both explain this UNDO write; the first is generic and immediately superseded by the second. Collapse to a single comment (low confidence, style/minimal-diff).
📄 src/backend/access/flux/flux_slot.c (L751-L760)
The same-buffer fast path frees rslot->tuple but not rslot->values_block. If the slot previously held a values-only materialized state (tuple == NULL, values_block != NULL, SHOULDFREE set) via tts_flux_materialize_values(), that state has buffer == InvalidBuffer; calling FluxSlotStoreTuple(slot, ..., InvalidBuffer) then takes this same-buffer branch (InvalidBuffer == InvalidBuffer), clears SHOULDFREE and leaks values_block. tts_flux_clear() correctly frees both; this fast path should too.
💡 Suggested change
Before:
if (rslot->buffer == buffer)
{
/* Same buffer — just free any materialized data */
if (unlikely(TTS_SHOULDFREE(slot)))
{
if (rslot->tuple)
pfree(rslot->tuple);
slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
}
}
After:
if (rslot->buffer == buffer)
{
/* Same buffer — just free any materialized data */
if (unlikely(TTS_SHOULDFREE(slot)))
{
if (rslot->tuple)
pfree(rslot->tuple);
if (rslot->values_block)
{
pfree(rslot->values_block);
rslot->values_block = NULL;
}
slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
}
}
📄 src/backend/access/flux/flux_stats.c (L50-L52)
The entire flux_stats.c module is dead code: FluxCollectRelationStats() and FluxLogRelationStats() (and the FluxRelationStats struct) have no callers anywhere in the tree. The header comment claims they are "designed to be called during ANALYZE" and "made available through the FluxCollectRelationStats() interface so that the planner can incorporate FLUX-specific cost adjustments", but nothing is wired up. Per the project's YAGNI/minimalism discipline, drop this file (and its declarations in flux.h) until a caller exists. Note also that the compression/overflow accounting here is doubly dead, since FLUX never sets FLUX_TUPLE_COMPRESSED and uses TOAST rather than on-page overflow.
📄 src/backend/access/flux/flux_tuple.c (L768-L771)
FluxPageUpdateTuple() has no callers anywhere in the tree (only this definition plus its declaration in flux.h). It is dead API surface; remove it and its declaration unless a caller is added.
📄 src/backend/access/flux/flux_tuple.c (L839-L841)
FluxPageGetLiveTuples() is dead code: its own comment states "This helper is only an estimate (no current callers)", and code_search confirms no caller exists. The snapshot_ts parameter is unused and discarded. Remove the function and its declaration in flux.h per YAGNI.
📄 src/backend/access/flux/flux_undo.c (L434-L436)
On delta reverse-diff failure this emits the "reverse-diff failed" WARNING and then sets info = 0. The subsequent switch(info) hits the default case, emitting a second, misleading WARNING ("FLUX UNDO: unknown subtype 0x0"). The record is not an unknown subtype; it is a known UPDATE_DELTA whose reconstruction failed. Break out of the record processing after the first WARNING (or use a distinct sentinel handled without the "unknown subtype" message) so recovery logs a single accurate message.
📄 src/include/access/flux_xlog.h (L16-L18)
A header must not #include "postgres.h". This is worse here than in flux.h: flux_xlog.h is compiled under FRONTEND (pg_waldump's rmgrdesc.c includes it for the flux_desc/flux_identify prototypes). FRONTEND translation units include postgres_fe.h, never postgres.h, so pulling in postgres.h from this header breaks the FRONTEND build. Drop the include and rely on the includer having pulled in the correct top-level header.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/xlogreader.h"
After:
#include "access/xlogreader.h"
📄 src/include/access/flux_xlog.h (L401-L408)
These FRONTEND struct definitions are dead code and dangerously wrong. (1) The only FRONTEND consumer, rmgrdesc/fluxdesc.c, deliberately defines its own local xl_flux_*_fe copies and never uses these. (2) They do not match the backend on-disk WAL layout: backend xl_flux_update carries dst_block_id + pad[3] after new_tuple_len (sizeof 32 with 8-byte alignment), while this FRONTEND version omits them (sizeof 24). Any future code that trusts these to parse a backend-written record would misparse it. Remove the entire #else /* FRONTEND */ block; the backend structs plus fluxdesc.c's own _fe copies are the source of truth.
📄 src/include/access/flux_pbu.h (L88-L88)
Documentation drift: the prototype and implementation name the second parameter stop_inclusive, and flux_pbu.c applies undo "back to AND INCLUDING" it. This comment says stop_exclusive, which contradicts the actual (inclusive) semantics and could lead a caller to pass the wrong bound. Fix the comment to stop_inclusive.
💡 Suggested change
Before:
* FluxPbuApplySubxactUndo(from, stop_exclusive) is run at
After:
* FluxPbuApplySubxactUndo(from, stop_inclusive) is run at
📄 src/backend/access/hash/hashinsert.c (L258-L262)
Dead sub-expression / misleading gate (high confidence). RelationAmSupportsUndo(heapRel) is already required by the first conjunct, and RelationUndoEngine() (tableam.c) unconditionally returns UNDO_ENGINE_PERBACKEND for any AM whose am_supports_undo is set (it even Asserts this). Therefore RelationUndoEngine(heapRel) == UNDO_ENGINE_PERBACKEND is always true here and || UndoBufferIsActive(heapRel) is unreachable. The engine-routing this comment describes does not exist; simplify the gate to the two conditions that actually vary and drop the dead disjunct.
📄 src/backend/access/hash/hashinsert.c (L260-L262)
Reachability/footgun (high confidence): because the gate always resolves to the per-backend arm, this call reaches HashUndoLogInsert() -> hash_undo_write_perbackend(), which unconditionally elog(ERROR)s ("per-backend index UNDO is not wired until Phase 9/10"). Consequently, once any am_supports_undo (non-delete-marking) table AM has a hash index, every _hash_doinsert() -- including CREATE INDEX build inserts via _h_indexbuild() -- fails with an ERROR. This is untested, unreachable-by-design scaffolding wired into a hot insert path; it should not be merged in a state where enabling the feature bricks hash inserts.
📄 src/backend/access/hash/hash_undo.c (L158-L166)
Dead scaffolding (high confidence): this entire UNDO-in-WAL arm (both the UndoBufferIsActive branch and the UndoRecordSetCreate/UndoRecordAddPayloadParts/UndoRecordSetInsert else branch) is unreachable. The function returns early whenever RelationUndoEngine(heapRel) == UNDO_ENGINE_PERBACKEND, and RelationUndoEngine() returns UNDO_ENGINE_PERBACKEND for every undo-supporting AM (the fork/UNDO-in-WAL engine was removed -- see tableam.c). HashUndoLogInsert is only ever called for undo-supporting AMs, so control never falls past the early return. Per YAGNI, drop this speculative path (and the associated xid, UndoRecordSet*/UndoBuffer* includes) rather than carry a dead second engine.
📄 src/backend/access/recno/recno_operations.c (L3056-L3060)
Dangling if (update_indexes) with no braces guards only the following pfree(new_tuple) statement. When update_indexes is NULL this leaks new_tuple; when non-NULL the if is misleading dead logic (the actual index-update strategy was already computed and assigned in the hot_changed/sum_changed block above). The intent is clearly to always free new_tuple. Remove the stray if so the tuple is freed unconditionally.
(high confidence)
💡 Suggested change
Before:
if (update_indexes)
pfree(new_tuple);
return TM_Ok;
}
After:
pfree(new_tuple);
return TM_Ok;
}
📄 src/backend/access/recno/recno_clock.c (L367-L369)
%lu is not portable for a uint64 value: unsigned long is 32-bit on LLP64 (Windows) and 32-bit platforms, so this misprints error_ms and reads a garbage vararg. PostgreSQL requires UINT64_FORMAT for uint64. Also recno_max_clock_offset_ms * 0.8 mixes an int GUC with a double literal, doing float comparison for what is integer-threshold logic. Applies to all three ereport sites here (the 80% FATAL, the 50% WARNING, and the NTP messages).
(high confidence)
💡 Suggested change
Before:
ereport(FATAL,
(errmsg("recno clock error bound %lu ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
After:
ereport(FATAL,
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
📄 src/backend/access/recno/recno_clock.c (L377-L379)
Second %lu-for-uint64 site (50% WARNING). Same portability defect: use UINT64_FORMAT.
(high confidence)
💡 Suggested change
Before:
ereport(WARNING,
(errmsg("recno clock error bound %lu ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
After:
ereport(WARNING,
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
📄 src/backend/access/recno/recno_clock.c (L303-L308)
Data race: this holds only LW_SHARED yet writes shared state (RecnoClockShmem->last_bounds and last_bounds_read). Concurrent backends running under the same shared lock can produce torn/interleaved writes to last_bounds. Either take LW_EXCLUSIVE for the update, or drop the shared-memory caching of last_bounds here. Separately, RecnoReadClockBound() memcpy's sizeof(ClockBoundData) straight out of an mmap of the external /dev/shm/clockbound file and trusts foreign-process fields (segment_id, tv_sec) as a trust boundary with only minimal sanity checks.
(high confidence)
📄 src/backend/access/recno/recno_clock.c (L572-L576)
The mmap fd and mapped pointer are stored in a SHARED-memory struct but are process-local: a file descriptor and an mmap address are only valid in the process that created them. After the postmaster forks backends, clockbound_fd/clockbound_map in shared memory refer to the postmaster's address space and fd table, not the backend's — so either every backend dereferences an invalid pointer, or the mapping is silently only valid in one process. Per-process mmap state must live in backend-local variables, established at backend startup, not in ShmemInitStruct memory.
(high confidence)
📄 src/backend/access/recno/recno_clock.c (L204-L206)
This bgworker cannot be shut down cleanly. pqsignal(SIGTERM, PG_SIG_DFL) makes SIGTERM hard-kill the process (default action = terminate) instead of requesting graceful exit, and the main loop only reacts to WL_EXIT_ON_PM_DEATH, never to a die/shutdown request. PostgreSQL bgworkers must install the standard async-safe handler (e.g. pqsignal(SIGTERM, die) or a SIGTERM handler that sets a volatile sig_atomic_t flag and SetLatch(MyLatch)), then check it in the loop and proc_exit(0).
(medium confidence)
💡 Suggested change
Before:
/* Establish signal handlers */
pqsignal(SIGTERM, PG_SIG_DFL);
BackgroundWorkerUnblockSignals();
After:
/* Establish signal handlers */
pqsignal(SIGTERM, die);
BackgroundWorkerUnblockSignals();
📄 src/backend/access/recno/recno_compress.c (L616-L618)
Endianness-dependent on-disk format. memcpy(out_ptr + 2, &abs_val, bytes_needed) copies the low bytes_needed bytes of a host-order uint32/uint64; RecnoDecompressDelta reads them back the same way. On a big-endian host (s390x, PPC64BE) the low-order bytes are at the high addresses, so a value written little-endian is misdecoded — silent data corruption, and cross-endian breakage on physical replication of WAL-logged delta-compressed attributes. Serialize with a fixed byte order (e.g. store via explicit shifts, or pg_hton32/64 into a full-width field), not a raw truncated memcpy of the host integer.
(high confidence)
📄 src/backend/access/recno/recno_compress.c (L975-L980)
RecnoDecompressDictionary reads dict_id from stored data and only checks the upper bound (dict_id >= num_entries), not dict_id < 0 — a negative id from corrupt/foreign data indexes entries[] out of bounds. More seriously, the dictionary is per-backend, process-local, and not persisted (as the header comment admits): a value dictionary-compressed by one backend cannot be decompressed by another (different dict_id -> different entry, or entry absent), yielding wrong data or ERRCODE_DATA_CORRUPTED on read. If DICTIONARY compression can ever reach on-page storage this is silent data corruption; it must not be selectable for persisted attributes until the dictionary is WAL-logged/catalog-persisted.
(high confidence)
💡 Suggested change
Before:
if (compression_dict == NULL || dict_id >= compression_dict->num_entries)
{
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("invalid dictionary ID in compressed data")));
}
After:
if (compression_dict == NULL || dict_id < 0 ||
dict_id >= compression_dict->num_entries)
{
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("invalid dictionary ID in compressed data")));
}
📄 src/backend/access/recno/recno_compress.c (L132-L133)
Dead code / YAGNI: RecnoCompressZSTDDict, RecnoDecompressZSTDDict, RecnoCompressLZ4Dict, RecnoDecompressLZ4Dict are all marked pg_attribute_unused() and have no callers anywhere in the tree. They also allocate 64KB dict_buf[65536] on the stack, which is a stack-pressure hazard if ever wired. PostgreSQL patches must not carry speculative, uncalled scaffolding — remove these until an actual caller exists.
(medium confidence)
📄 src/backend/access/recno/recno_compress.c (L1329-L1332)
RecnoGetCompressionStats returns hardcoded placeholder values (*avg_compression_ratio = 0.7, *total_compressed = 0) with a comment admitting "These would need to be tracked in a real implementation." Reporting fabricated statistics is misleading to any monitoring caller. Either track the real values or drop this stats accessor until it can report accurate data.
(low confidence)
📄 src/backend/access/recno/recno_fsm.c (L292-L295)
Memory leak: RecnoGetFSMState() palloc's a fresh RecnoFSMState plus a dedicated memory context and defrag_queue on every call (the comment even says "For simplicity, create a new state each time"). RecnoMarkPageForDefrag() calls it but never frees the returned state or deletes its context — only RecnoOpportunisticDefrag() frees. Every delete/update that triggers RecnoRecordFreeSpace -> RecnoMarkPageForDefrag leaks an FSMState and its context. Cache the state per-relation, or free it in RecnoMarkPageForDefrag after scheduling.
(medium confidence)
📄 src/backend/access/recno/recno_fsm.c (L360-L366)
Lock-order/self-deadlock hazard: RecnoScheduleDefrag() can synchronously call RecnoDefragmentPage(), which does ReadBufferExtended + LockBuffer(EXCLUSIVE) + a crit section, when the defrag queue is full. RecnoScheduleDefrag is reached from RecnoRecordFreeSpace -> RecnoMarkPageForDefrag, and RecnoRecordFreeSpace is called from insert/update/delete paths that may already hold an exclusive buffer lock on the same or another page. Taking a second buffer lock there risks self-deadlock or lock-order inversion. Defrag scheduling must never acquire buffer locks synchronously from within a free-space update; defer it.
(medium confidence)
📄 src/backend/access/recno/recno_diff.c (L92-L94)
Silent truncation risk: RecnoComputeTupleDiff casts start/end (Size) to uint16 for segments[].offset/.length with no check that they fit in 16 bits. For a tuple larger than 65535 bytes the offset/length wrap, producing a corrupt diff that RecnoApplyDiffReverse will then misapply (its bounds check uses the already-truncated values). Guard the cast: if compare_len > UINT16_MAX (or any segment offset/end exceeds UINT16_MAX), return NULL to fall back to full-tuple storage.
(medium confidence)
📄 src/backend/access/recno/recno_mvcc.c (L1329-L1334)
Unlocked shared-page mutation: when BufferIsValid(buffer) is false this clears RECNO_TUPLE_UNCOMMITTED directly on the page (tuple->t_flags &= ~...) with no buffer lock, no MarkBufferDirty, and no WAL. If this else branch is ever reachable with a real on-page tuple, it is a torn write to a shared page that others may be reading, and the hint is not persisted. Confirm this path is only reached for materialized/off-page copies; if an on-page tuple can reach it, the mutation is unsafe. Same pattern exists in the HLC variant (hlc_clear_uncommitted).
(medium confidence)
📄 src/include/access/recno.h (L24-L24)
Backend headers under src/include must not include "postgres.h"; the .c file is required to include it first, and the header should only include what it directly needs. No standard in-tree AM header (heapam.h, nbtree.h, hash.h) does this. Including postgres.h here breaks frontend/extension inclusion and violates project convention -- this will draw an immediate rejection on -hackers. (Confidence: high.) Same issue in recno_diff.h, recno_slog.h and recno_xlog.h.
📄 src/include/access/recno_diff.h (L18-L18)
Do not include "postgres.h" from an installed header; it is included first by the .c file. Remove it (this header only needs the base types, which come transitively). Same convention violation as recno.h. (Confidence: high.)
📄 src/include/access/recno_slog.h (L19-L19)
Remove "#include "postgres.h"" -- backend headers must not include postgres.h (the .c includes it first). Convention violation, same as recno.h. (Confidence: high.)
📄 src/include/access/recno_xlog.h (L18-L18)
Remove "#include "postgres.h"" -- backend headers must not include postgres.h; the .c file includes it first. Convention violation, same as recno.h. (Confidence: high.)
📄 src/include/access/recno_xlog.h (L139-L139)
SizeOf* macros for WAL/serialized records should use offsetof(last_field)+sizeof(last_field), not bare sizeof(struct). The tree's convention (SizeOfHeapInsert, SizeOfBtreeInsert, etc.) deliberately excludes trailing struct padding from the WAL-registered size; sizeof(xl_recno_undo) here includes any trailing padding, which is registered/compared as WAL bytes and is not portable. Note recno_xlog.c registers these structs whole via XLogRegisterData(&xlrec, sizeof(...)). Also SizeOfRecnoUndo lacks the parenthesization the others have (SizeOfXlRecnoHlcInfo). (Confidence: high.)
💡 Suggested change
Before:
#define SizeOfRecnoUndo sizeof(xl_recno_undo)
After:
#define SizeOfRecnoUndo (offsetof(xl_recno_undo, payload_len) + sizeof(uint16))
📄 src/include/access/recno_xlog.h (L135-L137)
payload_len is uint16 but the producer casts uur_payload.len (int/Size) to uint16 without a bounds check (recno_pbu.c:176 recno_pbu_walhdr.payload_len = (uint16) recno_pbu_uur.uur_payload.len;). Today the before-image is bounded by RECNO_MAX_TUPLE_SIZE (BLCKSZ/3, ~10.9KB at BLCKSZ=32KB) so it cannot overflow, but nothing enforces that invariant at the truncation site; a future path that stashes a larger image would silently truncate and corrupt rollback/version reconstruction. Add an assertion/elog guard at the cast, or document the bound on this field. (Confidence: moderate.)
📄 src/include/access/recno_undo.h (L27-L28)
Comment is inaccurate about the UNDO rmid space: in this same changeset ID 2 is UNDO_RMID_FILEOPS (in-core, defined in storage/fileops.h), not "reserved out-of-core", and there is no ZHEAP (6) consumer in this tree. Comments must describe what the code does now. Fix to reflect the actual assignments (1=NBTREE, 2=FILEOPS, 3=HASH, 5=FLUX, 7=RECNO). The value 7 itself is unique and correct. (Confidence: high.)
📄 src/include/access/recno_undo.h (L54-L54)
For consistency with the tree's WAL/serialized-size convention and the other SizeOf* macros here, prefer offsetof-based sizing over bare sizeof(struct) so trailing padding is not counted as payload. This header's RecnoUndoPayloadHeader has an explicit pad last field so sizeof and offsetof(pad)+sizeof(pad) happen to coincide, but the sizeof form is fragile if the layout changes. (Confidence: low.)
📄 src/backend/access/recno/recno_overflow.c (L951-L952)
WAL-before-data violation. RecnoDeleteOverflowChain() MarkBufferDirty()s the page and updates the FSM but emits no WAL record (there is no XLOG_RECNO_OVERFLOW_DELETE opcode in recno_xlog.h). After a checkpoint the physical removal becomes durable with no redo record, so a standby/PITR replica never removes the overflow record: the primary and replica diverge, and the surviving chain pointers on the replica still reference records the primary deleted. The "similar to TOAST cleanup" justification is incorrect -- TOAST deletions are WAL-logged as ordinary heap deletes. This shared on-disk mutation must be WAL-logged and replayed.
📄 src/backend/access/recno/recno_overflow.c (L437-L440)
Buffer double-release / use-after-release in the FSM-stale retry path. When buffer was found in overflow_buffers at lines 356-365, it is already locked EXCLUSIVE and is still tracked in overflow_buffers->buffers[] for the caller to WAL-log and release. If PageGetFreeSpace() < MAXALIGN(record_size) here, UnlockReleaseBuffer(buffer) unlocks and drops the pin on that tracked buffer; the caller will later WAL-register and release it again, causing a double-release and use-after-release. Only release/unlock the buffer if it was freshly ReadBuffer()'d in this iteration, not when it came from the overflow_buffers cache.
📄 src/backend/access/recno/recno_overflow.c (L383-L384)
Self-described "WORKAROUND" heuristic: treating target_block == 0 as "probably the main tuple's page" and unconditionally rejecting it will misbehave whenever overflow data legitimately belongs on block 0 (e.g. a small relation where block 0 has free space and is not the caller's pinned page). This silently wastes space by extending the relation and duplicates the read/lock logic below. A heuristic that keys off a magic block number rather than the actual pinned-buffer set is a footgun; track the caller's pinned data buffer explicitly instead.
📄 src/backend/access/recno/recno_overflow.c (L1552-L1554)
Dead transitional scaffolding. RecnoStoreOverflow/RecnoFetchOverflow/RecnoDeleteOverflow are labeled "kept for compatibility during transition", RecnoFetchOverflow always ereport(ERROR)s, and code_search confirms no caller exists outside this file. For a brand-new AM there is no prior on-disk format to be compatible with. Per YAGNI/minimal-diff discipline, delete the legacy trio (and the RecnoOverflowRef type if it is otherwise unused).
📄 src/backend/access/recno/recno_slot.c (L437-L441)
GetCurrentTransactionId() assigns a real XID as a side effect if the backend has none. Calling it to answer an xmin/xmax system-column request during a read-only query forces XID assignment -- a POLA violation that spuriously consumes XIDs (wraparound/pg_upgrade pressure) and is semantically wrong (returning the reader's XID as the tuple's xmin/xmax is not MVCC-correct anyway). Use GetCurrentTransactionIdIfAny() and return InvalidTransactionId when none is assigned, as the read-only path should never allocate an XID.
📄 src/backend/access/recno/recno_slot.c (L257-L265)
Overflow fetch during deform is a hot-path and lock-context hazard. tts_recno_deform() (and RecnoTupleToSlotWithOverflow) opens the relation and takes a SHARE lock on overflow pages while deforming. Deform can be invoked with the tuple's own buffer already locked, in short-lived contexts, and during executor teardown; if an overflow record sits on the same page as the tuple being deformed, this SHARE re-lock can self-conflict/deadlock. The fetched Datum is palloc'd via RecnoFetchOverflowColumn in whatever context is current, risking per-row leaks on large scans. Confirm deform is never reached while the same page is exclusively locked, and pin the overflow result to the per-tuple context.
📄 src/backend/access/recno/recno_undo.c (L450-L453)
The ABORTED sLog marker is removed unconditionally on the UNDO_APPLY_SUCCESS return, even when the switch branch skipped the physical restore. apply_recno_undo_restore_tuple() returns false (DEBUG-only) when the on-page slot is smaller than the before-image, and several branches break early (missing before-image, non-normal ItemId, reverse-diff failure) without restoring. In all those cases RecnoSLogRemove() below still drops the RECNO_SLOG_ABORTED entry that was the only thing hiding the un-restored row from readers, so the aborted row becomes visible again with no valid version behind it. Gate the RecnoSLogRemove() call on the restore having actually succeeded (and return UNDO_APPLY_ERROR/retry when it did not), rather than running it on every success path.
📄 src/backend/access/recno/recno_undo.c (L240-L244)
Skipping the restore when the current slot is smaller than the before-image (returning false with only a DEBUG1) silently abandons physical undo for DELETE and shrinking-UPDATE aborts. Combined with the unconditional RecnoSLogRemove() below, the row is left in its post-op physical state forever (or until VACUUM), while the marker that reconciled visibility is gone. For DELETE undo the row must become visible again; leaving RECNO_TUPLE_DELETED set on the page defeats that. This "skip and rely on sLog+VACUUM" strategy is not safe -- the correct behavior must not lose the ability to undo.
📄 src/backend/access/recno/recno_slog.c (L1563-L1567)
RECNO_TUPLE_UNCOMMITTED is authoritative for MVCC visibility (recno_mvcc.c consults it to decide whether to hit the sLog/CLOG at all), so clearing it via MarkBufferDirtyHint() -- which is not WAL-logged -- is unsafe. If this hint-dirtied page reaches disk during PRE_COMMIT and the backend then crashes before the commit WAL record is durable, recovery aborts the transaction but the on-disk page shows the flag cleared; visibility then treats the aborted row as committed-visible (clear flag == visible without an sLog check). On a standby that never receives the hint, the flag stays set and readers fall back to sLog/CLOG, so primary and replica disagree. Clearing a visibility-bearing bit must be WAL-logged, not treated as a hint bit.
📄 src/backend/access/recno/recno_slog.c (L514-L516)
Reclaiming another xid's committed op under HASH_ENTER holding the partition LWLock uses TransactionIdIsInProgress + TransactionIdDidCommit per slot in a hot INSERT/UPDATE/DELETE path. That is real work under a contended shared-memory lock, and the subsequent retry loop spins pg_usleep(1000) up to 3 times when the entry stays full. A sleep-and-retry cliff in the DML fast path (default sizing MaxBackends * 2048) will convert transient sLog pressure into multi-millisecond stalls across all backends contending the same TID/partition. This design needs a non-blocking overflow strategy (e.g. spill-to-SLRU as the comment itself contemplates) rather than sleeping in the DML path.
📄 src/backend/access/recno/recno_xlog.c (L2377-L2381)
Over-broad masking defeats wal_consistency_checking. Zeroing t_infomask, t_flags, t_commit_ts and t_ctid on every normal item means genuine primary/standby divergence in these fields (an actual redo bug) is masked and never caught -- the exact class of bug this tool exists to detect. If redo truly cannot reproduce t_ctid or the flags, that is a redo-completeness defect to fix in the redo handlers, not to paper over here. At minimum t_ctid should be reproduced by redo and left unmasked; masking should be limited to fields that are legitimately hint-like and provably non-authoritative.
📄 src/backend/access/recno/recno_xlog.c (L476-L476)
Aspirational/past-debugging comments violate the comment-accuracy and ASCII-only conventions for pgsql-hackers patches. Comments must describe present behavior and explain WHY, not narrate the debugging history ("caveat-3", "Fix B", "the ... path made this collision frequent enough to crash") and must not use non-ASCII punctuation (em-dashes appear throughout). Rewrite these as present-tense rationale in plain ASCII.
📄 src/backend/access/recno/recno_vm.c (L486-L487)
Future-tense / aspirational comment describing behavior that does not exist. Per comment-accuracy discipline, drop the "TODO: In the future, we could check ..." narrative and the unused (void) tuple; placeholder, or implement the check. A tuple parameter that exists only "for future timestamp checking" is dead flexibility (YAGNI) -- remove it from the signature until a caller needs it.
📄 src/backend/access/recno/recno_stats.c (L250-L252)
Use INT64_FORMAT for int64 rather than casting to (long long) with %lld (portability convention). Also, emitting three-to-four separate ereport() statements per relation at ANALYZE time is log spam; combine into a single message or lower to DEBUG2.
📄 src/backend/access/recno/recno_overflow.c (L1538-L1540)
Use INT64_FORMAT for these int64 counters instead of the (long long)/%lld cast pattern, per the portability convention for int64 formatting.
📄 src/backend/access/recno/recno_operations.c (L3222-L3232)
Crash-recovery data loss in the batch WAL path. This loop adds batch_count tuples to the page, but the WAL record below logs only formed_tuples[batch_start] (a single tuple) via RecnoXLogInsert. The main buffer is registered with plain REGBUF_STANDARD (recno_xlog.c:297), not REGBUF_FORCE_IMAGE, so when no full-page image is emitted this checkpoint cycle, redo takes the BLK_NEEDS_REDO path and re-adds exactly one tuple at xlrec->offnum (recno_xlog_insert_redo). The remaining batch_count - 1 tuples are silently lost on crash recovery. Either emit one WAL record per tuple, register the buffer with REGBUF_FORCE_IMAGE so a full page image always covers the whole batch, or add a batch-aware insert record + redo handler.
📄 src/include/access/recno.h (L140-L140)
The comment is inaccurate. With the preceding fields, t_attrs_bitmap lands at offset 68 (t_commit_ts..t_infomask = 65, + t_pad[3] = 68), and RECNO_TUPLE_OVERHEAD = offsetof(t_attrs_bitmap) = 68 is NOT MAXALIGN'd (MAXALIGN(68) = 72). Comments must describe what the code does now. The 3 pad bytes only round the header to a 4-byte multiple. Datum access still works because the data area is placed at RECNO_TUPLE_OVERHEAD + MAXALIGN(bitmap_len) and each attribute is aligned via att_align_nominal(), but the layout is not self-verified. Unlike the sibling FluxTupleHeader (flux.h has a StaticAssertDecl and an exact 40-byte MAXALIGN'd header), RECNO has no StaticAssertDecl guarding this layout, so a future field change could silently break it. Fix the comment to describe the actual 4-byte rounding, and add a StaticAssertDecl for offsetof(RecnoTupleHeader, t_attrs_bitmap) as FLUX does. (Confidence: high for the comment inaccuracy; medium for the missing assert.)
💡 Suggested change
Before:
uint8 t_pad[3]; /* pad so bitmap starts MAXALIGN'd */
After:
uint8 t_pad[3]; /* pad header to a 4-byte multiple; data area is
* anchored at RECNO_TUPLE_OVERHEAD +
* MAXALIGN(bitmap_len) and each attribute is
* aligned via att_align_nominal() */
📄 src/include/access/recno.h (L52-L52)
HLC_PHYSICAL_MASK is defined but has no user anywhere in the tree (only the HLC_GET_PHYSICAL accessor and HLC_LOGICAL_MASK are used). Per the minimalism/YAGNI discipline, drop unused macros rather than shipping dead scaffolding.
📄 src/backend/access/recno/recno_overflow.c (L1522-L1524)
Critical data-loss / replica-divergence bug. Pass 2 deletes only the orphaned overflow records from the page via RecnoPageIndexTupleDelete (compacting but keeping live tuples and referenced overflow records), then WAL-logs the change with RecnoXLogInitPage(). That opcode registers the buffer with REGBUF_WILL_INIT (implies NO_IMAGE) and its redo handler (recno_xlog_init_page_redo) reads the page RBM_ZERO_AND_LOCK and calls RecnoInitPage(), which re-initializes the page to EMPTY. On a standby / PITR replica, replay therefore wipes every live tuple and every non-orphan record still on that page -- xl_recno_init_page carries no page contents to rebuild them. Use a WAL record that logs the actual post-delete page state (e.g. a per-item delete/defrag opcode whose redo replays the same RecnoPageIndexTupleDelete calls, or force a full-page image), not a page re-init.
📄 src/backend/access/recno/recno_tuple.c (L1153-L1157)
Inconsistent attlen handling: this reader errors out for fixed-length by-reference / cstring (attlen == -2) columns, whereas RecnoComputeDataSize(), RecnoFormTuple(), RecnoDeformTuple() and tts_recno_deform() all support attlen == -2. A table with a cstring-typed column can be written by RecnoFormTuple but will fail with "unsupported attribute length: -2" the moment RecnoTupleToSlot(WithOverflow) reads it back. Handle attlen == -2 here the same way the other codecs do.
📄 src/backend/access/recno/recno_pbu.c (L302-L307)
Undo-region probe window is too small when overflow / cross-page blocks are present. The forward path (RecnoXLogInsert/Update) registers the per-backend undo buffers at recno_undo_block_id, which is 1 + (number of distinct overflow blocks, up to MAX_OVERFLOW_BUFFERS=32) + (1 for cross-page dst). But recno_redo always calls RecnoPbuRedoUndo(record, 1), and this loop only scans block ids [1, 1 + RECNO_PBU_MAX_UNDO_BLOCKS) = [1,5). With more than ~3 distinct overflow pages (or cross-page + overflow), the undo block lands at block_id >= 5, outside the window. The loop then finds no undo region, data stays NULL, and the function silently returns WITHOUT re-inserting the undo record during redo. Per this file's own PANIC contract, that misplaces the urec_ptr for this and every later record in the log and loses the ability to roll back the transaction after crash recovery -- a critical WAL-redo correctness bug. Either propagate the real first undo block id into recno_redo's RecnoPbuRedoUndo() calls, or size the probe window to XLR_MAX_BLOCK_ID rather than a fixed 4.
📄 src/backend/access/undo/slog_flathash.c (L4-L4)
Stale documentation: the file header still describes a "Seqlock-protected" hash with a "per-partition seqlock ... retry-based consistent reads". Per slog_flathash.h the seqlock scheme was replaced by an LWLock-only scheme: SLOG_SEQ_READ_BEGIN/END expand to LWLockAcquire(LW_SHARED)/Release (no retry) and SLogSeqWriteBegin/End are no-ops. The inline comments at the num_entries fast path ("caught by the seqlock retry") and "under the seqlock writer" are likewise wrong. This misleads maintainers into believing wait-free/retry guarantees that do not exist (POLA). Reconcile all seqlock/retry wording with the shipped LWLock scheme.
📄 src/backend/access/undo/slog_flathash.c (L178-L179)
This comment claims a concurrent writer's insert is "caught by the seqlock retry", but under the LWLock-only scheme the reader holds writer_lock LW_SHARED for the whole read section and never retries. The reasoning is now false; the correct justification is simply that the shared lock excludes the exclusive writer. Update this to match the actual locking.
📄 src/backend/access/undo/slog_tuple.c (L8-L8)
The file header and the many "WAIT-FREE: uses the seqlock read-side retry loop" / "body may re-run on retry" comments throughout this file are stale. SLOG_SEQ_READ_BEGIN takes writer_lock LW_SHARED and the body runs exactly once (never retries); reads are not wait-free. These comments contradict the shipped LWLock-only implementation and must be reconciled to avoid misleading future maintainers about lock-free/retry semantics.
📄 src/backend/access/undo/slog_tuple.c (L1473-L1473)
Dead complexity introduced for the removed seqlock. SLogSeqWriteBegin/End are no-ops under the LWLock-only scheme (the writer holds writer_lock LW_EXCLUSIVE for the whole function), so the SLOG_ABORT_PUBLISH_BATCH "end and re-begin the seqlock every N ops" logic no longer bounds any reader-visible window and never lets readers make progress mid-batch. This batching (here and in SLogTupleCleanupRetained) is now pure dead code plus misleading comments; either restore a real seqlock or drop the batching machinery.
📄 src/backend/access/undo/slog_tuple.c (L1437-L1438)
Performance: has_entries is set true whenever ANY local-only INSERT tracked keys exist, regardless of partition, and the final guard also short-circuits to entering the partition when insert_tids is non-empty. The result is that every one of SLogNumPartitions partitions is locked LW_EXCLUSIVE and the entire backend-local INSERT hash is re-scanned per partition -- O(num_partitions * insert_tids). For a large INSERT rollback this is a needless all-partition sweep. Collect touched partitions once (as SLogCollectTrackedPartitions already does for the linked list) rather than conservatively locking all of them.
📄 src/backend/access/undo/slog.c (L233-L233)
LWTRANCHE_SLOG is used here (and in slog_tuple.c) but is not defined anywhere in the tree -- it is absent from lwlocklist.h/lwlock.h and there is no lwlocknames.txt entry. As written this subsystem does not compile. The tranche must be registered (add to lwlocknames.txt or the built-in tranche list) before this can build.
📄 src/include/access/slog.h (L400-L401)
These are declared and commented as GUCs ("Set at postmaster startup") but neither slog_num_partitions nor slog_dsa_max_size_mb is registered in guc_tables.c (the only slog.h reference there is the #include). As shipped they are plain globals that can never be set by a user, so the GUC comments are inaccurate. Either register the config_int entries or drop the GUC framing.
📄 src/include/access/slog_flathash.h (L124-L124)
Non-ASCII characters in comments violate the project's ASCII-only source rule: the multiplication sign in "4x the number of CPUs" and the em-dash in "set during SLogShmemInit()". Replace with ASCII ("4x" / "4 times" and "--").
📄 src/include/access/slog_flathash.h (L234-L234)
Non-ASCII em-dash in comment (ASCII-only source rule). Use "--".
📄 src/include/access/slog.h (L253-L254)
Minimalism / YAGNI: none of the tuple-sLog public API declared here (SLogTupleInsert/Lookup/RemoveByXid, SLogRegisterAmDescriptor, SLogShmemInit, SLogTupleMaybeCleanupRetained, SLogTupleCollectTrackedKeys, etc.) has any caller anywhere in this change -- the FLUX/RECNO AMs do not reference it, guc_tables.c only #includes the header, and there is no PG_SHMEM_SUBSYSTEM registration. A ~4700-line subsystem with zero call sites is not commit-ready: it cannot be exercised or tested and will not link its shmem/tranche wiring. Either wire it to a real consumer with tests, or split it out until a consumer lands.
📄 src/backend/access/undo/slog_tuple.c (L1503-L1508)
Same stale-seqlock issue as SLogTupleMarkAborted: with SLogSeqWriteBegin/End being no-ops under the LWLock-only scheme, the SLOG_ABORT_PUBLISH_BATCH end/re-begin here bounds no reader-visible window and lets no reader make progress. The "wait-free readers of this partition make progress" comment is inaccurate. Remove the dead batching or restore a real seqlock.
📄 src/backend/access/undo/slog_tuple.c (L905-L911)
Correctness: the function contract (and the header comment: "Returns false silently if the hash is full") is violated. The INSERT via SLogFlatHashApply can silently drop the op when the partition is full (flat_hash_apply_insert() logs SLOG_LOST_OP and returns without storing), but this function never probes for success and always returns true. On a standby with a full partition, the UNCOMMITTED tuple is not tracked yet the redo caller is told it was, so the AM's visibility check cannot see the in-flight writer -- a visibility-correctness hazard during recovery. Probe with SLogFlatHashHasOpForXid() under the writer lock and return false when the op was not stored, matching the primary path.
📄 src/include/access/slog_flathash.h (L237-L237)
Portability (MSVC hard gate): SLogNumPartitions is referenced from the static inline SLogFlatHashPartitionIndex() defined in this public header, which is compiled into cross-module callers (e.g. the FLUX AM includes access/slog.h). An extern global read from a different module than the one that defines it must be declared PGDLLIMPORT, or the Windows/MSVC build fails to link. Add PGDLLIMPORT here.
📄 src/include/access/slog.h (L239-L240)
Portability (MSVC): slog_dsa_max_size_mb and slog_num_partitions are exported globals; if any cross-module caller (FLUX AM / an extension including this header) reads them, MSVC requires PGDLLIMPORT on the declaration. Add PGDLLIMPORT to these externs to keep the Windows build linkable.
📄 src/backend/access/undo/slog_tuple.c (L485-L486)
Robustness / EXEC_BACKEND consistency: the actual partition count is stored in shmem as SLogState->num_partitions but is never read anywhere -- all routing uses the backend-local global SLogNumPartitions. Every backend must independently arrive at the same value, otherwise SLogFlatHashPartitionIndex() routes to a different partition than the one an entry was written to (silent lookup misses / cross-partition corruption). Since the value is already authoritative in shmem, backends should read SLogState->num_partitions (or assert it equals the local global) rather than relying on each process recomputing SLogComputeNumPartitions() identically; the stored field is currently dead.
📄 src/backend/access/undo/slog_flathash.c (L265-L270)
Tombstone accumulation with no compaction/rehash. num_tombstones is incremented on every REMOVE/COMMIT/ABORT that empties an entry and only decremented when an insert happens to reuse that exact slot, but nothing ever triggers a rehash or compaction (verified: no rehash routine exists in the tree). On a hot partition with churn (insert/remove of scattered keys), tombstones steadily replace EMPTY slots on probe chains. Since SLogFlatHashProbe/ProbeForInsert only terminate a chain at SLOG_FLAT_EMPTY, probe chains lengthen without bound and eventually hit SLOG_FLAT_MAX_PROBE (128), so inserts start failing spuriously (op silently dropped -> lost update / visibility failure) even when num_entries is tiny. The field is documented '(for load factor)' but is never consulted. Add a tombstone-density-triggered in-place rehash, or make ProbeForInsert reclaim the probed chain, before this can be shipped.
📄 src/include/access/nbtree.h (L1472-L1477)
Dead code / speculative scaffolding. NbtreeUndoInsertLeafHeader and SizeOfNbtreeUndoInsertLeafHeader are defined here but referenced nowhere in the tree (I grepped the whole codebase). The comment claims they are "Exposed here so the UNDO discard worker can extract (index_oid, blkno, offset) ... without depending on the full struct", but the discard worker (pbu_discardworker.c / pbu_undodiscard.c) never uses them; the actual UNDO paths in nbtree_undo.c use the full NbtreeUndoInsertLeaf struct. Drop this prefix-struct until an actual caller needs it (YAGNI). Confidence: high.
📄 src/include/access/nbtree.h (L1453-L1455)
These NBTREE_UNDO_* subtype macros are defined identically a second time in nbtree_undo.c (lines 63-71), which #includes this header. That is a duplicated-layout contract: the values must be kept byte-for-byte in sync by hand, and the day someone renumbers one set the compiler emits -Wmacro-redefined (or, worse, the two copies silently diverge if the guards differ). Define them in exactly one place (here) and delete the copy in nbtree_undo.c. Confidence: high.
📄 src/include/access/nbtree.h (L772-L774)
Missing upper-bound guard before OR'ing newsize into the 16-bit t_info. INDEX_SIZE_MASK is only 0x1FFF (8191); if newsize exceeds that, the high bits silently corrupt INDEX_ALT_TID_MASK (0x2000) / INDEX_VAR_MASK (0x4000) / INDEX_NULL_MASK (0x8000). The existing assertion only checks the lower bound (newsize >= IndexTupleSize(itup) + sizeof(ItemPointerData)). A near-max-size plain leaf tuple (~1/3 page) plus the MAXALIGN'd 8-byte trailer can approach the limit; add Assert(newsize <= INDEX_SIZE_MASK) (mirroring itup.h's IndexTupleSetSize contract) so an over-size promotion is caught rather than producing a mis-classified tuple. Confidence: moderate.
📄 src/include/access/nbtree.h (L478-L479)
Comment discipline: shipped source comments should explain why the code behaves as it does, not track an out-of-tree phased rollout. The "Phase 5" label and the pointer to access/nbtree/PHASE5_DELETE_MARKING_DESIGN.md are development-process artifacts; a hackers reviewer will object to both the phase labels sprinkled through nbtree.h/nbtxlog.h and to committing a .md design doc under src/backend/access/nbtree/ (design goes to the -hackers thread / README, not a Markdown file in the source tree). Rewrite these comments to describe the tuple format itself and drop the external-doc reference. Confidence: moderate.
📄 src/backend/access/undo/slog_flathash.c (L261-L261)
ASCII-only source rule: these comments contain non-ASCII em-dash (U+2014) characters (also on lines 358, 416, 713 here, and lines 137/157/796/2441/2466 of slog_tuple.c, plus '~4%%' phrasing). PostgreSQL sources must be ASCII only. Replace each em-dash with ' -- '.
📄 src/backend/access/nbtree/nbtree_undo.c (L149-L153)
This entire file is dead scaffolding in this patch. NbtreeUndoLogInsert (its only external producer) is called from _bt_insertonpg only when RelationAmSupportsUndo(heaprel) && !RelationSupportsDeleteMarking(heaprel). But both shipped UNDO-supporting table AMs (FLUX in flux_handler.c, RECNO in recno_handler.c) declare am_supports_undo = true AND am_index_delete_marking = true, so RelationSupportsDeleteMarking() is always true for any relation that reaches this gate -- the call site is never taken at runtime. NbtreeUndoLogDedup has no caller at all, and nbtree_undo_apply() is therefore never dispatched a real record. Per the project's YAGNI/minimalism rule, ~690 lines of unreachable UNDO machinery (plus the RM registration via NbtreeUndoRmgrInit) should not ship until a consumer exists. Recommend dropping nbtree_undo.c and the NbtreeUndoLogInsert call site from this patch, or gating the feature behind a real AM that exercises it.
📄 src/backend/access/nbtree/nbtinsert.c (L1441-L1447)
Duplicated comment block: the two consecutive paragraphs both open with "Only write UNDO if the parent table AM supports UNDO. The heaprel parameter is NULL during index builds and recovery." The first (lines under END_CRIT_SECTION) is fully subsumed by the second. Trim to one block.
📄 src/backend/access/nbtree/nbtree_undo.c (L221-L222)
nbtree_undo_write_perbackend() unconditionally elog(ERROR)s and the surrounding block comments are heavily aspirational/future-tense ("not wired until Phase 9/10", "Two pieces remain to be wired", "a real per-backend AM is needed to exercise"). This is speculative future scaffolding describing an unreachable path -- flagged by the comment-accuracy and minimalism rules. If the file is kept at all, this function and its multi-paragraph roadmap comment must go.
📄 src/backend/access/nbtree/nbtree_undo.c (L559-L562)
The DEDUP undo path memcpy's hdr.page_len bytes (read from the payload) over the live shared buffer page with no validation that hdr.page_len == PageGetPageSize(page) (or <= BLCKSZ). A mismatched/corrupt length is an unbounded overwrite of shared-buffer memory. It is currently unreachable (NbtreeUndoLogDedup has no caller), but if this code is retained the length must be bounds-checked before the memcpy.
📄 src/backend/access/nbtree/nbtree_undo.c (L88-L88)
These SizeOf* macros are unparenthesized offsetof(...) + sizeof(...), unlike every other SizeOf* macro in the tree (e.g. SizeOfBtreeDeleteMark in nbtxlog.h wraps the whole expression). Current use sites happen to be precedence-safe (all + or <), but this is a footgun the moment one is used in a */- context. Wrap the definitions in parentheses.
💡 Suggested change
Before:
#define SizeOfNbtreeUndoInsertLeaf offsetof(NbtreeUndoInsertLeaf, itup_sz) + sizeof(Size)
After:
#define SizeOfNbtreeUndoInsertLeaf (offsetof(NbtreeUndoInsertLeaf, itup_sz) + sizeof(Size))
📄 src/backend/access/nbtree/nbtree_undo.c (L241-L244)
On-disk/serialized format hazard: the NbtreeUndoInsertLeaf/Upper/Dedup/Delete structs are memcpy'd whole into the UNDO payload and memcpy'd back on apply, so intra-struct padding and native field widths become part of the persisted UNDO format. That violates the portability gate (no dependence on struct padding for on-disk formats) and would break replay across compilers/arches if these payloads are ever persisted or shipped to a standby. Serialize field-by-field with fixed-width types if this path is kept.
📄 src/backend/access/nbtree/nbtinsert.c (L261-L263)
Stray blank line added after the _bt_findinsertloc() call, unrelated to this change -- minimal-diff violation. Remove it.
📄 src/backend/access/nbtree/nbtree.c (L1967-L1971)
Return-value/contract mismatch: the header comment says the already-marked case is "idempotent success" and the function "Returns true if the entry was found and marked", but this branch sets marked = false and the function then return marked; (false). So an already-marked entry reports the same "not found / not marked" result as a genuinely-missing entry. The sole caller (flux_operations.c / recno_operations.c index_delete_mark) ignores the return via (void), so this is not a live bug today, but the doc contradicts the code and is a footgun for any future caller that distinguishes idempotent-success from not-found. Either return true here or fix the comment to say already-marked returns false.
📄 src/include/access/nbtree.h (L951-L953)
Footgun: _bt_binsrch_insert() reads insertstate->nopostingsplit unconditionally (nbtsearch.c:558, result == 0 && key->scantid != NULL && !insertstate->nopostingsplit), and this struct is explicitly not private to nbtinsert.c ("also used by _bt_binsrch_insert", see comment above). Adding a field that the shared search path reads means every caller that builds a stack BTInsertStateData must now set it. contrib/amcheck/verify_nbtree.c:3047 builds one (with scantid set for rootdescend) and does NOT initialize nopostingsplit, so that path now reads an uninitialized value -> nondeterministic posting-split suppression / spurious ERROR. Either update all existing initializers (including verify_nbtree.c) or, better, have _bt_binsrch_insert gate the field so an unset caller is safe. Confidence: high.
📄 contrib/amcheck/verify_nbtree.c (L1493-L1496)
This only excludes delete-marked tombstones from the heapallindexed fingerprint path, but the checkunique path (bt_entry_unique_check, reached at lines ~1663-1670 and the postponed calls at ~1703/1798/1826) is not updated. bt_entry_unique_check() calls BTreeTupleGetHeapTID(itup) -> heap_entry_is_visible(tid), which fetches the row version by TID and checks snapshot visibility WITHOUT verifying the current heap key still matches the index key. A Phase 5 tombstone (k_old, T) whose heap tuple at T is still visible (its live version now under k_new) will therefore be treated as a live holder of key k_old: it populates lVis, and a same-key live entry (or another such tombstone) on the page triggers a spurious "index uniqueness is violated" ERROR. This directly contradicts design invariant I3 (_bt_check_unique must not treat a delete-marked entry as a live duplicate). The comment here even states the live version is reachable under a different key -- exactly the condition that misfires. bt_entry_unique_check must skip BTreeTupleIsDeleteMarked() entries too (and such entries must not update lVis). Confidence: high.
📄 src/include/access/nbtxlog.h (L203-L206)
This comment misdescribes the on-disk WAL layout. The redo path (btree_xlog_delete_mark) retrieves the tuple image via XLogRecGetBlockData(record, 0, &newsize), i.e. it is registered as block-0 buffered data (XLogRegisterBufData(0, ...) in nbtree.c), NOT appended after the fixed record data. The claim that the image "follows the fixed portion of the record" is wrong and will mislead anyone maintaining the format into reading (char *)xlrec + SizeOfBtreeDeleteMark, which points at unrelated bytes. State that the image is stored as block-0 data.
💡 Suggested change
Before:
* Backup Blk 0: the leaf page. The full replacement tuple image (already in
* its final delete-marked or plain form) follows the fixed portion of the
* record; redo overwrites the item at 'offnum' with it. Logging the exact
* final bytes keeps the redo deterministic for wal_consistency_checking.
After:
* Backup Blk 0: the leaf page. The full replacement tuple image (already in
* its final delete-marked or plain form) is stored as block-0 data (retrieved
* with XLogRecGetBlockData); redo overwrites the item at 'offnum' with it.
* Logging the exact final bytes keeps the redo deterministic for
* wal_consistency_checking.
📄 src/include/access/nbtxlog.h (L217-L217)
Same layout error as the header comment: the tuple image is NOT stored at the end of this struct. It is registered as block-0 buffered data (XLogRegisterBufData) and read back with XLogRecGetBlockData in btree_xlog_delete_mark(). A reader trusting this comment would dereference the wrong bytes. Fix the comment to say the image is carried as block-0 data.
💡 Suggested change
Before:
/* NEW TUPLE IMAGE FOLLOWS AT END OF STRUCT (absent when deleted) */
After:
/* NEW TUPLE IMAGE CARRIED AS BLOCK-0 DATA (absent when deleted) */
📄 src/backend/access/rmgrdesc/nbtdesc.c (L139-L141)
The descriptor omits the deleted flag, so pg_waldump cannot distinguish an in-place rewrite (deleted=false, tuple image present) from the full-page fallback that removes the entry (deleted=true, no image). These are materially different physical operations; printing deleted aids WAL forensics. Suggest adding it to the output.
💡 Suggested change
Before:
appendStringInfo(buf, "offnum: %u, setmark: %c",
xlrec->offnum,
xlrec->setmark ? 'T' : 'F');
After:
appendStringInfo(buf, "offnum: %u, setmark: %c, deleted: %c",
xlrec->offnum,
xlrec->setmark ? 'T' : 'F',
xlrec->deleted ? 'T' : 'F');
📄 src/backend/access/index/indexam.c (L251-L252)
This comment is factually wrong for this change set: FLUX (flux_handler.c) and RECNO (recno_handler.c) both set .am_index_delete_marking = true and call index_delete_mark/index_undo_mark in this same series, so the path is reached at runtime. Comments must describe current behavior, not a stale roadmap. Also drop the project-internal "Phase 5 / Phase 8c / Phase 8+ / DORMANT" jargon throughout these wrappers (and the amapi.h/relscan.h/plancat.c comments) -- it will not mean anything to a committist reading committed source and will read as stale as soon as phases move. Explain why (the I1 invariant), not internal phase labels. [high confidence]
📄 src/backend/access/index/indexam.c (L266-L270)
The comment says the gate checks "table AM enables delete-marking", but RelationUndoEngine(heapRelation) == UNDO_ENGINE_NONE only checks that the table AM supports UNDO, not that it enables delete-marking. The precise predicate is RelationSupportsDeleteMarking(heapRelation) (which additionally checks am_index_delete_marking), and that is exactly what the planner and nbtree call sites use. As written, an AM that supports UNDO but not delete-marking would still invoke amdeletemark. Use RelationSupportsDeleteMarking() here (and in index_undo_mark) to match the stated contract. [moderate confidence]
📄 src/backend/optimizer/util/plancat.c (L299-L300)
RelationSupportsDeleteMarking(relation) is invariant across index columns, so it is evaluated ncolumns times inside the loop. It is cheap (two pointer dereferences), so this is minor, but hoist it into a single bool computed before the loop and AND it in -- clearer and avoids the repeated table-AM reach per column in this planner hot path. [low confidence]
📄 src/include/access/tableam.h (L366-L368)
Stale future-tense comment. This same change already introduces two in-tree table AMs (FLUX in flux_handler.c and RECNO in recno_handler.c) that set am_supports_undo = true and register their own UNDO RMs. The wording "A future in-place-update AM will set am_supports_undo = true and register its own UNDO RM" therefore describes behavior that has already shipped. Per comment-accuracy discipline, describe current behavior (e.g. "The FLUX and RECNO AMs set am_supports_undo = true and register their own UNDO RMs").
📄 src/include/access/tableam.h (L692-L693)
The new callback is named begin_bulk_insert but its sole caller (nodeModifyTable.c) invokes it for CMD_INSERT, CMD_UPDATE and CMD_DELETE alike -- it is not insert-specific. The name is a POLA violation and will mislead AM authors into thinking it fires only for INSERT. Consider a name that reflects the actual contract (e.g. begin_bulk_modify / begin_dml). Also, the options parameter is always passed as literal 0 by the only caller, so it is currently unused flexibility (YAGNI) unless a concrete consumer is added.
📄 src/include/access/heapam.h (L543-L547)
Stray extra blank line before the closing #endif in an otherwise-untouched file. This is whitespace-only churn that violates the minimal-diff discipline and will draw a rejection on -hackers; drop it.
💡 Suggested change
Before:
tuple->t_infomask2 = frz->t_infomask2;
}
+
#endif /* HEAPAM_H */
After:
tuple->t_infomask2 = frz->t_infomask2;
}
#endif /* HEAPAM_H */
📄 src/backend/access/table/tableam.c (L826-L831)
Verify this contract is actually reachable. The only in-tree AMs that set am_supports_undo=true (FLUX, RECNO) also set am_index_delete_marking=true, so callers gated on RelationAmSupportsUndo(heaprel) && !RelationSupportsDeleteMarking(heaprel) (nbtinsert.c, hashinsert.c) never write index UNDO for any real AM. Consequently the separate index-UNDO path this function is meant to gate is dead in-tree, and its per-backend arm (nbtree_undo.c nbtree_undo_write_perbackend) elog(ERROR)s "not wired until Phase 9/10". This is speculative scaffolding for an AM that does not exist; consider trimming it or documenting that no in-tree AM exercises the non-delete-marking UNDO path.
📄 src/backend/access/nbtree/nbtree.c (L2004-L2015)
Crash-recovery atomicity gap in the in-place-overwrite fallback. When PageIndexTupleOverwrite() fails, the old plain entry is deleted under one WAL record (XLOG_BTREE_DELETE_MARK, deleted=true) and the delete-marked tuple is then re-inserted by a separate _bt_doinsert() (its own XLOG_BTREE_INSERT_LEAF), after the buffer lock is dropped. These are two independent physical-redo records with no atomicity between them. If the server crashes after the delete record is flushed but before the insert record is, redo replays the delete and the tombstone is permanently lost: the old key ends up with neither a live entry nor a tombstone, leaving the index inconsistent with the heap (old-snapshot readers can no longer reach the before-image via this index, violating invariant I1). btdeletemark() is reachable at runtime via FLUX/RECNO (index_delete_mark -> flux_operations.c / recno_operations.c), so this is not merely theoretical. The delete+reinsert must be made crash-atomic (e.g. a single WAL record that both removes the old entry and records the replacement, or ensuring the two records replay atomically).
📄 src/backend/access/index/indexam.c (L296-L299)
Gate predicate mismatch (same defect as index_delete_mark, but this is a separate, un-flagged call site). RelationUndoEngine(heapRelation) == UNDO_ENGINE_NONE is equivalent to !am_supports_undo; it does NOT check am_index_delete_marking. An AM that supports UNDO but does not enable delete-marking would wrongly pass this gate and reach amundomark. The precise predicate is RelationSupportsDeleteMarking(heapRelation) (requires both am_supports_undo && am_index_delete_marking), which is already available via the included tableam.h. Confidence: high.
💡 Suggested change
Before:
if (indexRelation->rd_indam->amundomark == NULL)
return false;
if (RelationUndoEngine(heapRelation) == UNDO_ENGINE_NONE)
return false;
After:
if (indexRelation->rd_indam->amundomark == NULL)
return false;
if (!RelationSupportsDeleteMarking(heapRelation))
return false;
📄 src/include/access/amapi.h (L348-L349)
New IndexAmRoutine members are inserted in the middle of the interface-function list (between amgetbitmap and amendscan). PostgreSQL convention is to append new members at the end of the struct so out-of-tree index AMs that use designated/positional initializers or rely on field offsets are not silently perturbed by the layout change. Consider moving amdeletemark/amundomark to the end of the struct. Confidence: moderate (style/convention).
📄 src/include/access/tableam.h (L322-L338)
The UndoEngine enum and its comment were inserted between the TableAmRoutine doc comment ("API struct for a table AM...") and the typedef struct TableAmRoutine it documents. This orphans the struct's own doc comment: the UndoEngine block now sits directly above the struct, and the struct comment reads as if it describes the enum. Move the enum definition (and its comment) to before the /* API struct for a table AM... */ comment so the struct comment stays attached to the struct. (moderate confidence)
📄 src/include/access/tableam.h (L330-L332)
Comment references ZHEAP as an in-tree consumer of UNDO_ENGINE_PERBACKEND, but ZHEAP is not part of this change -- it appears only in design docs (README/PHASE*.md) as an unshipped future phase (Phase 9). The in-tree table AMs that set am_supports_undo=true are FLUX and RECNO (flux_handler.c / recno_handler.c). Drop ZHEAP to keep the comment describing what actually exists now. Same over-claim recurs in the am_undo_engine comment below ("FLUX/ZHEAP/RECNO"). (high confidence)
💡 Suggested change
Before:
* UNDO_ENGINE_PERBACKEND -- per-backend UNDO engine (access/undo/perbackend);
* FLUX/ZHEAP/RECNO use this. This is the only
* engine a table AM may declare.
After:
* UNDO_ENGINE_PERBACKEND -- per-backend UNDO engine (access/undo/perbackend);
* FLUX/RECNO use this. This is the only
* engine a table AM may declare.
📄 src/include/access/tableam.h (L379-L381)
Same ZHEAP over-claim as the enum comment: ZHEAP is not an in-tree AM in this patch. List only the AMs actually present (FLUX/RECNO). (high confidence)
💡 Suggested change
Before:
* An AM with am_supports_undo = true MUST set this to
* UNDO_ENGINE_PERBACKEND (FLUX/ZHEAP/RECNO); the per-relation fork engine
* has been removed, so it is the only supported value.
After:
* An AM with am_supports_undo = true MUST set this to
* UNDO_ENGINE_PERBACKEND (FLUX/RECNO); the per-relation fork engine
* has been removed, so it is the only supported value.
📄 src/backend/access/undo/README (L67-L68)
This entire 'Per-relation RelUndo fork' facility is documented in the present tense as a shipped, co-equal alternative to the cluster-wide UndoLog, but it does not exist in this change set. None of relundo.c, relundo_page.c, relundo_apply.c exist; RELUNDO_FORKNUM is not defined anywhere in the tree (only referenced in this README and flux/README prose); and RelUndoVacuum/RelUndoMaybeVacuum/RelUndoFinish have no implementation. Documenting a non-existent subsystem as present, with concrete file and API names, will mislead every reviewer and maintainer. Either delete the RelUndo material or move it under an explicitly-labeled 'Future Work / not yet implemented' section. [high confidence]
📄 src/backend/access/undo/README (L642-L643)
GUC default is wrong. The code default is undo_worker_naptime = 10000 (10 seconds) in undolog.c, not 60000/1 minute. (postgresql.conf.sample also disagrees with this README, showing 60s.) Documenting the wrong default is a footgun for operators tuning the discard cycle. Fix the README to match the actual code default (or reconcile code + conf sample + README to a single value). [high confidence]
💡 Suggested change
Before:
undo_worker_naptime Sleep interval between discard cycles (ms)
+ Default: 60000 (1 minute)
After:
undo_worker_naptime Sleep interval between discard cycles (ms)
Default: 10000 (10 seconds)
📄 src/backend/access/undo/README (L645-L646)
GUC default is wrong. The code default is undo_retention_time = 60000 (60 seconds) in undolog.c, not 3600000/1 hour. (postgresql.conf.sample shows yet a third value, 300s.) Reconcile README, code, and postgresql.conf.sample to a single documented default. [high confidence]
💡 Suggested change
Before:
undo_retention_time Minimum retention time for UNDO records (ms)
+ Default: 3600000 (1 hour)
After:
undo_retention_time Minimum retention time for UNDO records (ms)
Default: 60000 (60 seconds)
📄 src/backend/access/undo/README (L1135-L1135)
The lock-ordering table cites undo_flush.c:61 as the holder of the flush lock, but undo_flush.c does not exist anywhere in the tree. A stale file:line reference in a normative lock-ordering table (the one place a reader trusts to reason about deadlock safety) is worse than no reference. Point at the real file/line or drop the citation. [high confidence]
📄 src/backend/access/undo/README (L498-L501)
This RM table is inaccurate against rmgrlist.h. RM_RELUNDO_ID ("RelUndo") is not registered anywhere in rmgrlist.h -- it does not exist. Conversely, the two UNDO RMs that ARE added, RM_UNDOLOG_ID ("UndoLog") and RM_UNDOACTION_ID ("UndoAction"), are not mentioned here at all. Since WAL RM IDs are position-defined and WAL-durable, an RM table that lists a nonexistent RM and omits two real ones actively misleads anyone reasoning about on-disk/WAL compatibility. Reconcile this table with the actual PG_RMGR entries. [high confidence]
📄 src/backend/access/undo/README (L755-L757)
Section 5's 'Modified Core Files' list and the RelationHasUndo() reference are stale. heapam.c, heapam_handler.c, and pruneheap.c contain no UNDO code in this change set (no UndoBuffer / UndoRecordSet / RelationHasUndo usage), and RelationHasUndo is not defined anywhere. The UNDO-enabled AMs actually shipped here are flux_* and recno_*, not heap. Update this list (and the matching lines in Section 14 'Modified Core Files') to reference the AMs that actually use UNDO. [high confidence]
📄 src/backend/access/undo/README (L341-L342)
This Initialization Flow section does not match undo.c. (1) The shmem is not wired via direct ipci.c calls; undo.c uses the PG_SHMEM_SUBSYSTEM pattern (ShmemCallbacks registered in storage/subsystemlist.h), and its own header comment says so. (2) UndoShmemSize() also adds PbuUndoLogShmemSize() and PendingUndoShmemSize(), and UndoShmemInit() also calls PbuUndoLogShmemInit()/PendingUndoShmemInit() -- neither is reflected here. Update the flow to describe the subsystem-callback registration and the pbu/pending contributions. [high confidence]
📄 src/backend/access/undo/README (L822-L822)
Per PostgreSQL doc discipline, a source-tree README should describe current behavior, not carry machine-specific benchmark dumps and roadmap material. The 'b8 benchmark results -- nuc' block (dated FreeBSD runs, TPS tables, CV percentages) and the pgbench TPS table are reproducible-benchmark artifacts that belong in a -hackers thread, not committed here; likewise the 'Planned Future Work' / 'In Progress' / 'Future:' passages (and line 638 '4. Future: physically truncates...'). Reviewers will ask to trim these. [moderate confidence]
📄 src/backend/access/undo/undoworker.c (L271-L277)
Phase 1 discards the entire log up to insert_ptr whenever the log's single oldest_xid precedes the global oldest_xid. A log can contain records from many XIDs; XIDs in the range [log->oldest_xid+1, oldest_xid) may still be needed by transactions that are older than oldest_xid is meant to protect (or by aborted xids whose UNDO has not been applied). Discarding to insert_ptr therefore risks dropping still-needed UNDO, breaking rollback / crash recovery. The discard bound must be computed per-XID (the oldest record whose XID >= oldest_xid), not the full insert pointer. [high confidence]
📄 src/backend/access/undo/undoworker.c (L394-L396)
Non-portable format: retained_mb is uint64, but it is cast to unsigned long and printed with %lu. On LLP64 platforms (Windows/MSVC) unsigned long is 32-bit, truncating the value. Use UINT64_FORMAT and pass the uint64 directly (no cast), per the tree's 64-bit formatting rule. [medium confidence]
💡 Suggested change
Before:
ereport(WARNING,
(errmsg("UNDO WAL retention (%lu MB) exceeds undo_max_wal_retention_size (%d MB)",
(unsigned long) retained_mb, undo_max_wal_retention_size),
After:
ereport(WARNING,
(errmsg("UNDO WAL retention (" UINT64_FORMAT " MB) exceeds undo_max_wal_retention_size (%d MB)",
retained_mb, undo_max_wal_retention_size),
📄 src/backend/access/undo/undoworker.c (L622-L624)
Inconsistent synchronization: this path acquires UndoWorkerShmem->lock to set shutdown_requested, but undo_worker_sigterm sets the same field with no lock (from signal context) and the main loop reads it unlocked. The LWLock here provides no protection against those accesses and is misleading. Standardize on a single mechanism (the ShutdownRequestPending volatile sig_atomic_t + latch pattern) and drop the lock here. [low confidence]
📄 src/backend/access/undo/xactundo.c (L715-L721)
total_undo_bytes is computed from UndoRecordSetGetSize(), which returns uset->buffer_size. But CleanupXactUndoInsertion() calls UndoRecordSetReset() after each insertion cycle, and that resets buffer_size = 0 (undorecord.c:256). By the time abort runs, the record sets have been reset (records were emitted to WAL), so total_undo_bytes is ~0 and the threshold test total_undo_bytes < undo_instant_abort_threshold is essentially always true. Result: every aborting transaction takes the inline apply path in its own backend regardless of actual UNDO size, defeating the large-transaction/ATM deferral and risking multi-second stalls applying huge chains inline. The size used for the threshold must reflect the UNDO actually emitted (e.g. track cumulative bytes, or use the WAL span perm_lsn..XactLastRecEnd). [high confidence]
📄 src/backend/access/undo/xactundo.c (L773-L775)
This goto inline_undo_done; is taken after old_ctx = MemoryContextSwitchTo(undo_ctx) and jumps to the inline_undo_done: label which sits AFTER MemoryContextSwitchTo(old_ctx) and MemoryContextDelete(undo_ctx). So on the invalid-batch path: (1) undo_ctx (allocated in TopMemoryContext) is leaked, and (2) CurrentMemoryContext is left as undo_ctx, so the ATM fallback and the subsequent ResetXactUndo() run in a context that is neither the abort context nor ever deleted. This is a leak and mis-attributed allocation on a post-abort path that must not fail. Perform the switch-back/delete before the goto (or move the label above them). [high confidence]
📄 src/backend/access/undo/xactundo.c (L315-L317)
The (UndoRecPtr) 1 sentinel relies on UndoRecPtrIsValid(1) being true and on 1 never being a real UndoRecPtr, and it is checked/reset by open-coded == (UndoRecPtr) 1 comparisons in multiple places. This is fragile against any future change to UndoRecPtr encoding. Prefer a named constant or an explicit boolean 'pending start location' flag per persistence level. [low confidence]
📄 src/backend/access/undo/undobuffer.c (L302-L307)
xl_undo_batch has a 4-byte padding hole: TransactionId xid (uint32 at offset 0) is followed by XLogRecPtr chain_prev (8-byte aligned at offset 8), leaving bytes 4-7 uninitialized. This struct is written verbatim into WAL via XLogRegisterData((char *) &xlrec, SizeOfUndoBatch) without a prior memset, so the padding bytes carry stack garbage. That makes WAL content non-deterministic and breaks wal_consistency_checking / replica byte-comparison. Zero the struct before assigning fields (the same defect exists in undoinsert.c UndoRecordSetInsert).
💡 Suggested change
Before:
xl_undo_batch xlrec;
XLogRecPtr batch_lsn;
Oid primary_reloid = InvalidOid;
if (!undo_t2buf.active || undo_t2buf.nrecords == 0)
return;
After:
xl_undo_batch xlrec;
XLogRecPtr batch_lsn;
Oid primary_reloid = InvalidOid;
if (!undo_t2buf.active || undo_t2buf.nrecords == 0)
return;
/* Zero padding so no uninitialized bytes are written to WAL */
memset(&xlrec, 0, sizeof(xlrec));
📄 src/backend/access/undo/undoinsert.c (L86-L91)
Same WAL-nondeterminism bug as undobuffer.c: xl_undo_batch has a 4-byte padding hole between xid (uint32) and chain_prev (8-byte-aligned XLogRecPtr). The struct is written into WAL via XLogRegisterData((char *) &xlrec, SizeOfUndoBatch) with all fields set individually and no memset, so the padding carries uninitialized stack bytes into the WAL stream. This breaks WAL consistency checking and replica byte-comparison. memset the struct to zero before populating fields.
💡 Suggested change
Before:
xl_undo_batch xlrec;
XLogRecPtr batch_lsn;
Oid primary_reloid = InvalidOid;
if (uset == NULL || uset->nrecords == 0)
return InvalidUndoRecPtr;
After:
xl_undo_batch xlrec;
XLogRecPtr batch_lsn;
Oid primary_reloid = InvalidOid;
if (uset == NULL || uset->nrecords == 0)
return InvalidUndoRecPtr;
/* Zero padding so no uninitialized bytes are written to WAL */
memset(&xlrec, 0, sizeof(xlrec));
📄 src/backend/access/undo/undo_xlog.c (L1325-L1331)
The RM_HEAP_ID branch here is dead code operating on a WAL format that no producer emits. A tree-wide search shows XLH_INSERT_HAS_UNDO / XLH_DELETE_HAS_UNDO / XLH_UPDATE_HAS_UNDO are only ever read (here and in UndoValidateBatchLSN); no heap code path sets these flags or appends an xl_undo_batch after the fixed heap record header. heap_insert() etc. register the tuple as block data (XLogRegisterBufData), not as main record data, so even if a producer existed, the payload_offset = SizeOfHeapInsert arithmetic would be wrong. Either wire up the heap producer or remove this branch and the heapam_xlog.h include entirely (the FIXME already acknowledges the agnosticism breach). As written it is unreachable speculative scaffolding.
📄 src/backend/access/undo/undo_xlog.c (L398-L401)
Assert-only length validation is unsafe in production redo. Unlike the delta-CLR path (which uses a runtime ereport(ERROR) on short block data), the visibility and HOT branches guard XLogRecGetBlockData reads with Assert(datalen >= SizeOf...) only. In non-assert builds these compile out, so a short or corrupt block causes the following memcpy(&vis_rec, vis_data, SizeOfUndoApplyVisibility) (and the HOT equivalent) to read past the buffer during recovery. Replace the Asserts with runtime checks that ereport(ERROR) like the delta path.
📄 src/backend/access/undo/undo_xlog.c (L433-L436)
Same Assert-only hazard in the HOT branch: Assert(datalen >= SizeOfUndoApplyHot) compiles out in production builds, so a short/corrupt block lets memcpy(&hot_data, data, SizeOfUndoApplyHot) read past the XLogReader buffer during redo. Use a runtime length check that ereport(ERROR)s, matching the delta-CLR path.
📄 src/backend/access/undo/undo_xlog.c (L630-L630)
ASCII-only source rule violation: this comment contains the non-ASCII multiplication sign and the approximately-equal sign. Replace with ASCII (e.g. "1 million entries * ~40 bytes each is about 40 MB").
💡 Suggested change
Before:
* 1 million entries × ~40 bytes each ≈ 40 MB, which is reasonable for
After:
* 1 million entries * ~40 bytes each is about 40 MB, which is reasonable for
📄 src/backend/access/undo/undo_xlog.c (L1213-L1213)
ASCII-only source rule violation: this comment uses a non-ASCII em-dash. Replace with an ASCII hyphen/double-hyphen.
💡 Suggested change
Before:
* Compare against GetRedoRecPtr() — if our target is well behind the
After:
* Compare against GetRedoRecPtr() -- if our target is well behind the
📄 src/backend/access/undo/undo_xlog.c (L1470-L1478)
This reset is only invoked from the logical revert worker (logical_revert_worker.c). The inline-abort path in xactundo.c wraps ApplyUndoChainFromWAL() in PG_TRY/PG_CATCH but its PG_CATCH does not call UndoResetBatchReader(). If a read throws mid-way (ereport ERROR inside an rm_undo callback, or a torn read), the cached static undo_batch_reader is left with a stale segment FD / partial decode buffer, and the next abort reusing it can misbehave or crash. Wire UndoResetBatchReader() into every PG_CATCH that can interrupt a batch read, not just the worker path. (Also verify reentrancy: an rm_undo callback that itself triggers a WAL read via this same static reader would corrupt the outer walk's XLogBeginRead position.)
📄 src/backend/access/undo/undo_bufmgr.c (L59-L61)
This entire module appears to be unreachable dead code. A tree-wide search finds no callers of ReadUndoBuffer, ReadUndoBufferExtended, InvalidateUndoBuffers, InvalidateUndoBufferRange, MarkUndoBufferDirty or UndoMakeBufferTag outside this file and its header. The header (undo_bufmgr.h) even documents that UNDO now lives entirely in WAL (pwrite/pread, bypassing shared_buffers), and the segment-discard path that would use these invalidation APIs is itself a no-op stub (UndoLogDeleteSegmentFile). Per YAGNI, drop this module and header, or explain which live path is expected to use it.
📄 src/backend/access/undo/undo_bufmgr.c (L235-L236)
Footgun: the last_block parameter is accepted (and Assert'd against first_block) but never used -- DropRelationBuffers drops every buffer with blockNum >= first_block. So a caller expecting a bounded [first_block, last_block] invalidation will silently over-drop every buffer past last_block, potentially discarding not-yet-persisted undo pages. Either honor the range (loop/DropRelationBuffersInRange semantics) or remove the misleading last_block parameter so the API can't be misused. (Note: this whole module currently has no callers; if it is being kept, fix the contract before it grows one.)
📄 src/backend/access/undo/undostats.c (L39-L41)
These three SQL-callable functions are declared via PG_FUNCTION_INFO_V1 but have no pg_proc.dat entries anywhere in the tree. Without a catalog registration (with a developer-range OID 8000-9999) they are not callable from SQL. The shipped example scripts (examples/01-basic-undo-setup.sql, examples/05-undo-monitoring.sql) call SELECT * FROM pg_stat_get_undo_logs(), which will fail with "function does not exist". Add the pg_proc.dat entries (and corresponding SQL wrappers/system_views if intended as views).
📄 src/backend/access/undo/undostats.c (L114-L122)
This function always reports zeros for every counter (the comment states UNDO pages are now in shared_buffers and dedicated stats are no longer tracked). A monitoring function that unconditionally returns zero cache stats is a misleading footgun: users cannot tell "no activity" from "not measured". If the data genuinely lives in pg_buffercache now, this function should not exist -- remove it (and its SQL wrapper) rather than ship an always-zero stub.
📄 src/backend/access/undo/undostats.c (L271-L275)
Dead/speculative scaffolding (YAGNI). undolog.c states UNDO now lives entirely in WAL and the segment-file lifecycle operations are no-ops: UndoLogSealAndRotate() and UndoLogDeleteSegmentFile() are no-op stubs, and the only producers of UNDO_LOG_SEALED/DISCARDABLE states are this (unregistered, hence unreachable) function and the legacy XLOG_UNDO_ROTATE redo which nothing emits. So pg_undo_force_discard drives a segment-file state machine that no longer has any real files or SEALED/DISCARDABLE logs to act on. Remove this function and the associated lifecycle machinery unless a live producer of these states is added.
📄 src/backend/access/undo/README (L699-L703)
None of these "Key Log Messages" strings exist in the code. Searching the tree, "created UNDO log file: ...", "applying UNDO chain starting at ...", "UNDO log %u: discard pointer updated to offset %llu", and "UNDO rollback: relation %u no longer exists, skipping" are found only in this README (and a parallel copy in doc/src/sgml/undo.sgml), not in any C source under src/backend/access/undo/. Documenting log messages that are never emitted misleads operators grepping logs for these strings. Additionally, the %llu templates would violate the tree's 64-bit format rules (UINT64_FORMAT), but the deeper problem is the strings are fabricated. Remove or replace with the messages the code actually emits. [high confidence]
📄 src/backend/access/undo/README (L711-L715)
This describes active segment rotation that no longer exists. undolog.c defines UndoLogSealAndRotate() as a no-op stub (/* No-op: no segment rotation with UNDO-in-WAL */), and undolog.h states "the ACTIVE->SEALED->DISCARDABLE rotation no longer occurs." This also contradicts Section 4 of this same README ("There are NO separate UNDO segment files or directories"). The error string "UNDO log %u would exceed segment size" is not emitted anywhere in the tree. Remove this Common Issue or rewrite it to match the UNDO-in-WAL model. [high confidence]
📄 src/backend/access/undo/README (L782-L783)
"Log rotation and segment lifecycle management" is listed as Implemented, but UndoLogSealAndRotate() in undolog.c is a no-op stub (segment rotation "no longer occurs" per undolog.h). Under the UNDO-in-WAL model there are no segment files to rotate. This claim is stale and contradicts Section 4. Remove it from the Implemented list. [high confidence]
📄 src/backend/access/undo/README (L1115-L1115)
heapam_begin_bulk_insert / heapam_finish_bulk_insert do not exist anywhere in the tree. The begin_bulk_insert table AM callback is implemented by flux_begin_bulk_insert (flux_handler.c) and recno_begin_bulk_insert (recno_handler.c); the heap AM does not implement it. This is consistent with UndoBufferBegin() being called only from flux_handler.c, not heapam/nbtree. Correct these to name the AMs that actually implement the callback. [high confidence]
📄 src/backend/access/undo/README (L328-L328)
"Used by heapam and nbtree" is inaccurate. UndoBufferBegin (the entry point for this Tier 2 buffer) is called only from src/backend/access/flux/flux_handler.c in this change set; there is no UndoBuffer usage in heapam or nbtree. The undobuffer consumers are the flux/recno in-place AMs, not heap/nbtree. [high confidence]
📄 src/backend/access/undo/undorecord.c (L30-L32)
This comment is inaccurate. UndoRecordSetResetCache() is the only release point for UndoRecordReusableContext, but it is called solely from AtAbort_XactUndo(); AtCommit_XactUndo() never calls it (neither the !has_undo early return nor the main path). Because UndoRecordSetFree() re-parents the recycled context to TopMemoryContext, on a committed transaction that context is left living in TopMemoryContext across transaction boundaries -- not "cleaned up at transaction end". Either add the call to the commit path (both branches) or fix this comment to describe the actual behavior.
📄 src/backend/access/undo/undoinsert.c (L28-L29)
Transitional no-op stubs with aspirational comments ("kept for callers that haven't been updated yet") are the kind of migration-in-progress code pgsql-hackers rejects: comments must describe what the code does now, not a migration. These stubs still have live callers in xactundo.c (AtCommit/AtAbort/AtPrepare_XactUndo), so removing them requires removing the calls too. Either finish the migration by deleting both the stubs and their call sites, or drop the "haven't been updated yet" framing and document them as permanent no-ops with a concrete reason.
📄 src/backend/access/undo/undo_xlog.c (L652-L660)
This is an O(n) linear scan per tracked batch, and UndoRecoveryRemoveXid (called from every XLOG_XACT_COMMIT/ABORT redo) does the same. With up to UNDO_RECOVERY_MAX_ENTRIES (1,048,576) distinct in-flight XIDs, the aggregate cost of building/removing entries during the redo phase is O(n^2) -- potentially ~10^12 comparisons in the pathological case, stalling crash recovery. Use a hash table (dynahash/simplehash keyed on xid) instead of a linear array so track/remove are O(1).
📄 src/backend/access/undo/undo.c (L60-L63)
These entry points already have proper prototypes in headers (PbuUndoLogShmemSize in pbu_undolog.h, PendingUndoShmemSize in pbu_undorequest.h, UndoLauncher*/DiscardWorkerRegister in their pbu_*.h). Re-declaring them as bare local externs here duplicates declarations that can silently drift out of sync with the definitions (nothing cross-checks these against the real prototypes at compile time). Prefer including the owning headers. If a genuine type-name collision forces this, isolate the colliding declarations in a small shared internal header both TUs include, rather than hand-copying signatures. Note also PendingUndoShmemSize returns int while its siblings return Size -- inconsistent and easy to get wrong when copied by hand.
📄 src/backend/access/undo/undo.c (L72-L72)
The header that defines pbu_undo_workers_enabled (src/include/access/undolog.h:150) declares it as a plain extern bool without PGDLLIMPORT. On Windows/MSVC a cross-module extern variable must be marked PGDLLIMPORT in its header or it will fail to link from other modules/extensions. This local extern PGDLLIMPORT here is a workaround that papers over the missing marker on the real declaration; fix undolog.h to add PGDLLIMPORT so all consumers link correctly rather than each re-declaring it.
📄 src/backend/access/undo/undo_xlog.c (L161-L167)
Inconsistent locking discipline: normal running code updates discard_ptr/oldest_xid under log->lock (see UndoLogDiscard / UndoLogGetDiscardPtr / GetUndoLogStats in undolog.c and undostats.c), but this redo path writes them with no lock held. On a hot standby serving reads, pg_stat_get_undo_logs() reads these fields under LW_SHARED concurrently with redo, so a redo write here is a torn-read / TOCTOU hazard against those readers. Acquire log->lock (LW_EXCLUSIVE) around the field updates in the discard/rotate redo handlers to match the reader side.
📄 src/backend/access/undo/undoworker.c (L106-L106)
sealed_log_count is initialized to 0 here and only ever decremented (line 346 pg_atomic_fetch_sub_u32) -- it is never incremented anywhere. The seal path in undo_xlog.c sets state to UNDO_LOG_SEALED but does not bump this counter. Consequences: (1) the adaptive fast-naptime branch (if (sealed_count > 0)) is dead code and never fires as intended; worse, (2) the first time a DISCARDABLE log is freed, fetch_sub_u32(...,1) underflows the unsigned counter to UINT32_MAX. From then on sealed_count > 0 is permanently true, so the worker busy-spins at UNDO_WORKER_FAST_NAPTIME_MS (200ms) forever instead of the configured undo_worker_naptime (default 10s). Either increment the counter when a log is sealed, or drop the counter and derive the adaptive interval another way.
📄 src/backend/access/undo/undoworker.c (L280-L281)
total_discarded is a cumulative counter (initialized to 0 in undolog.c and never reset). Here it is incremented every discard cycle by the absolute offset of the current insert_ptr, so the same log's offset is re-added on every worker pass, wildly over-counting. Additionally this shmem field is write-only -- it is never read anywhere (the stats path in undolog.c computes its own total from discard_ptr offsets). Either accumulate the delta discarded this cycle, or remove this dead, incorrect accounting.
📄 src/backend/access/undo/undoworker.c (L251-L253)
These comments state the discard logic respects undo_retention_time ("We need to respect the retention_time setting" / "2. The retention time setting") for point-in-time recovery, but the code below never references undo_retention_time -- the discard decision is based solely on log->oldest_xid vs oldest_xid. Aspirational comments describing behavior that does not exist should be removed or the feature implemented.
📄 src/backend/access/undo/undoworker.c (L606-L607)
Use snprintf with BGW_MAXLEN like the two lines below (and like ApplyLauncherRegister/autovacuum in-tree), not sprintf, per the codebase's bounded-string-op convention. Although the literals are safe today, mixing sprintf and snprintf for adjacent BackgroundWorker fields is inconsistent and a footgun if these strings ever become non-constant.
💡 Suggested change
Before:
sprintf(worker.bgw_library_name, "postgres");
sprintf(worker.bgw_function_name, "UndoWorkerMain");
After:
snprintf(worker.bgw_library_name, BGW_MAXLEN, "postgres");
snprintf(worker.bgw_function_name, BGW_MAXLEN, "UndoWorkerMain");
📄 src/backend/access/undo/xactundo.c (L885-L886)
Stale comment: AtCleanup_XactUndo does not exist anywhere in the tree. The per-backend slot is actually cleared at backend exit by AtProcExit_XactUndo (which calls UndoClearBatchLSN). Fix the referenced function name so the comment matches the code.
💡 Suggested change
Before:
* containing our UNDO data. The slot will be cleared at backend exit via
* AtCleanup_XactUndo.
After:
* containing our UNDO data. The slot will be cleared at backend exit via
* AtProcExit_XactUndo.
📄 src/include/access/undo_xlog.h (L267-L271)
High-confidence portability defect: UndoPersistenceLevel is a bare enum whose width is implementation-defined by the C standard (GCC/Clang may narrow it; MSVC uses int). Embedding it directly in an on-disk WAL struct makes the xl_undo_batch layout compiler/ABI-dependent, so WAL replay can diverge between a primary and a standby built with different toolchains/architectures. The producers (UndoRecordSetInsert in undoinsert.c, UndoBufferFlush in undobuffer.c) serialize the raw struct via XLogRegisterData((char *) &xlrec, SizeOfUndoBatch) and the redo path (undo_xlog.c:489) reads it back with a direct (xl_undo_batch *) cast, so the layout must be fixed-width and deterministic. Additionally, the struct has interior padding (4 bytes between xid and the 8-byte-aligned chain_prev) that is never zeroed by the producers, so indeterminate bytes are written to WAL. Store persistence as a fixed-width uint8/uint16 field and either serialize field-by-field or memset the struct before use.
💡 Suggested change
Before:
UndoPersistenceLevel persistence; /* Persistence level of this batch */
/* Followed by total_len bytes of serialized UndoRecordHeader+payload */
} xl_undo_batch;
#define SizeOfUndoBatch (offsetof(xl_undo_batch, persistence) + sizeof(UndoPersistenceLevel))
After:
uint8 persistence; /* Persistence level (UNDOPERSISTENCE_*) */
/* Followed by total_len bytes of serialized UndoRecordHeader+payload */
} xl_undo_batch;
#define SizeOfUndoBatch (offsetof(xl_undo_batch, persistence) + sizeof(uint8))
📄 src/include/access/undo_xlog.h (L158-L161)
The xl_undo_apply CLR is serialized to WAL by struct cast (nbtree_undo.c: XLogRegisterData((char *) &xlrec, SizeOfUndoApply)) and replayed by (xl_undo_apply *) cast in undo_xlog.c. The layout contains interior padding (2 bytes between clr_flags and the 4-byte-aligned tuple_len) that producers never initialize (the struct is stack-allocated and only named fields are set), so indeterminate bytes enter the WAL stream. For a WAL-durable, replicated record this is a portability/determinism hazard. Either reorder fields to eliminate interior padding, memset the struct before populating it, or serialize field-by-field.
📄 src/include/access/undormgr.h (L45-L48)
This comment is factually wrong and dangerous for a WAL-durable ID space. IDs 2 and 5 are NOT reserved for out-of-core consumers: UNDO_RMID_FILEOPS = 2 is defined in-core (src/include/storage/fileops.h) and UNDO_RMID_FLUX = 5 is defined in-core (src/include/access/flux_undo.h). The genuinely unused values are 4 and 6 (RECNO takes 7). A future contributor trusting this note could reuse ID 2 or 5 and silently corrupt rollback dispatch on replay of existing UNDO records. Fix the comment to reflect the actual allocation (0=INVALID, 1=NBTREE, 2=FILEOPS, 3=HASH, 5=FLUX, 7=RECNO; 4 and 6 free).
💡 Suggested change
Before:
#define UNDO_RMID_INVALID 0
#define UNDO_RMID_NBTREE 1
#define UNDO_RMID_HASH 3
/* 2, 4, and 5 are reserved by out-of-core consumers (see their own headers) */
After:
#define UNDO_RMID_INVALID 0
#define UNDO_RMID_NBTREE 1
#define UNDO_RMID_HASH 3
/*
* Currently assigned by other in-core consumers (see their own headers):
* 2 = UNDO_RMID_FILEOPS (storage/fileops.h)
* 5 = UNDO_RMID_FLUX (access/flux_undo.h)
* 7 = UNDO_RMID_RECNO (access/recno_undo.h)
* Values 4 and 6 are currently free; take the next free value and never reuse.
*/
📄 src/include/access/undolog.h (L189-L197)
These are documented as "Legacy no-op stubs" for a path (separate undo segment files) that no longer exists in the UNDO-in-WAL architecture, yet they still have live callers that do nothing useful (e.g. xactundo.c calls UndoLogCloseFiles(), undoworker.c/undostats.c call UndoLogDeleteSegmentFile()/UndoLogSealAndRotate(), undo_xlog.c calls ExtendUndoLogFile()). This is dead scaffolding: either wire it up or remove the stubs and their now-pointless call sites (YAGNI / minimal-diff). Shipping no-op APIs that callers still invoke is a footgun and needless surface area.
📄 src/include/access/undo_bufmgr.h (L73-L76)
Aspirational/future-tense comment for behavior that has not shipped, and the internal "Task #5" tracker reference is meaningless in-tree. UNDO_FORKNUM is declared but "not currently used in buffer operations" -- this is speculative scaffolding. Per the comments-describe-now discipline, drop the Task-tracker reference and either remove UNDO_FORKNUM or document only what it does today.
📄 src/include/access/undolog.h (L147-L154)
Windows/MSVC portability: these extern GUC/state variables and the UndoLogShared pointer lack PGDLLIMPORT. They are not referenced cross-module in the current in-tree build, but the design explicitly anticipates out-of-core UNDO consumers (see undormgr.h's "reserved by out-of-core consumers" note). Any extension that reads e.g. undo_batch_size_kb or UndoLogShared would fail to link on MSVC without PGDLLIMPORT. Mark exported globals with PGDLLIMPORT, or keep them backend-private and expose accessors.
📄 src/include/access/undo_xlog.h (L40-L42)
This comment claims undodefs.h is safe to include "in both frontend and backend code", but the header uses uint64/uint16/bool (undodefs.h) without including c.h/postgres.h/postgres_fe.h, so it only compiles if the includer has already pulled in the base types. State the actual contract: this header must be included after postgres.h (backend) or postgres_fe.h/c.h (frontend).
📄 src/include/access/undoworker.h (L50-L51)
These two GUCs are already declared in access/undolog.h (lines 147-148), which this header #includes just above. Redeclaring them here is redundant. Worse, it invites divergence: if one declaration site gains a PGDLLIMPORT marker and the other doesn't, the build will be inconsistent across MSVC. Drop these two lines and rely on the undolog.h declaration.
📄 src/include/access/undoworker.h (L50-L51)
extern variables referenced across module/extension boundaries must be marked PGDLLIMPORT for Windows/MSVC, per the tree's portability gate. Sibling headers in this same change follow the convention (e.g. recno.h, flux.h mark all their GUC externs PGDLLIMPORT). These GUCs feed a bgworker; add PGDLLIMPORT. (Note: also see the duplicate-declaration issue -- the canonical declaration in undolog.h needs the marker too.)
💡 Suggested change
Before:
extern int undo_worker_naptime;
+extern int undo_retention_time;
After:
extern PGDLLIMPORT int undo_worker_naptime;
extern PGDLLIMPORT int undo_retention_time;
📄 src/include/access/undoworker.h (L22-L23)
This header only declares plain C functions plus UndoWorkerMain(Datum). Datum is provided by postgres.h (via c.h/postgres.h), not fmgr.h. No PG_FUNCTION_ARGS/fmgr symbol is used here, so the fmgr.h include is unnecessary and violates minimal-include hygiene; move it to the .c if needed.
💡 Suggested change
Before:
+#include "fmgr.h"
+#include "storage/lwlock.h"
After:
#include "storage/lwlock.h"
📄 src/include/access/xactundo.h (L107-L107)
extern GUC variable referenced across module/extension boundaries must be PGDLLIMPORT for Windows/MSVC. Sibling headers added in this change (recno.h, flux.h) already mark their GUC externs PGDLLIMPORT; follow the convention here.
💡 Suggested change
Before:
extern int undo_instant_abort_threshold;
After:
extern PGDLLIMPORT int undo_instant_abort_threshold;
📄 src/include/access/xactundo.h (L70-L78)
PrepareXactUndoDataParts has no callers anywhere in this change -- the only occurrences are its declaration here and its definition in xactundo.c. Per the minimalism/YAGNI discipline, speculative API surface with no caller should not be introduced. Either wire up a real caller in this patch or drop the scatter-gather variant until it is actually needed.
📄 src/include/access/undostats.h (L19-L20)
This header declares only plain C functions (GetUndoLogStats/GetUndoBufferStats) and explicitly documents that the SQL-callable function is NOT declared here. No fmgr symbol is used, so the fmgr.h include is unnecessary. Remove it to keep header includes minimal (move to the .c file if it needs it).
💡 Suggested change
Before:
+#include "access/undolog.h"
+#include "fmgr.h"
After:
#include "access/undolog.h"
📄 src/backend/access/undo/undo.c (L102-L111)
UndoShmemSize() is dead code: it is never consumed by any RequestAddinShmemSpace() call, and the framework's PG_SHMEM_SUBSYSTEM pattern only invokes request_fn/init_fn, never UndoShmemSize(). A tree-wide search confirms no caller (ipci.c does not reference it; the README claim is stale). Consequently the legacy ShmemInitStruct-based modules initialized in UndoShmemInit_internal() -- UndoLogShmemInit, UndoWorkerShmemInit, LogicalRevertShmemInit -- have their sizes reserved nowhere: they silently draw from the general shmem slop. UndoLogShmemSize() scales with MaxBackends (the backend_undo_lsns[] flexible array), so a large max_connections can push these allocations past the slop margin and fail shmem allocation at startup. Either wire RequestAddinShmemSpace(UndoShmemSize()) into UndoShmemRequest_internal(), or convert these modules to ShmemRequestStruct() like the pbu_* ones, and drop the unused UndoShmemSize().
📄 src/backend/access/undo/undo_xlog.c (L898-L905)
Record-ordering inconsistency (data-integrity hazard). This crash-recovery undo phase applies records within a batch in FORWARD (serialization / oldest-first) order via this while (pos < end) loop. The online-abort path ApplyUndoChainFromWALBounded() in undoapply.c deliberately does the opposite: it two-passes to apply records within a batch in REVERSE (newest-first) order, with a comment stating "ARIES requires that UNDO records within a batch be applied newest-first (reverse of serialization order)." If both are correct then one of them violates ARIES semantics for multi-record batches that touch the same tuple (e.g. INSERT then in-place UPDATE of the same row in one batch): applying oldest-first here leaves the wrong tuple version on the page after crash recovery, diverging from what an online rollback would produce. The two paths must use the same ordering; make this loop collect record offsets and replay them in reverse like undoapply.c does.
| flux_pbu.o \ | ||
| flux_pvs.o | ||
|
|
||
| include $(top_srcdir)/src/backend/common.mk No newline at end of file |
There was a problem hiding this comment.
Missing trailing newline at end of file. Every other backend Makefile (e.g. access/gin/Makefile, access/recno/Makefile) ends the final include ...common.mk line with a newline, and git diff --check / pgindent conventions require a trailing newline. Add a newline after this line. (high confidence)
| include $(top_srcdir)/src/backend/common.mk | |
| +include $(top_srcdir)/src/backend/common.mk |
| COMMIT; | ||
|
|
||
| -- STEP 6: Check UNDO log statistics | ||
| SELECT * FROM pg_stat_get_undo_logs(); |
There was a problem hiding this comment.
These functions are implemented in C (undostats.c, via PG_FUNCTION_INFO_V1) but are NOT registered in pg_proc.dat (nor anywhere else in the catalog). Without a pg_proc.dat entry there is no SQL-callable binding, so both statements fail with ERROR: function pg_stat_get_undo_logs() does not exist. Add entries to pg_proc.dat using OIDs from the 8000-9999 range before shipping these examples. (high confidence)
| -- STEP 5: Commit the transaction | ||
| COMMIT; |
There was a problem hiding this comment.
There is no matching BEGIN earlier in this script, so every preceding statement ran in its own autocommit transaction and was already committed. This bare COMMIT will emit WARNING: there is no transaction in progress, and the "STEP 5: Commit the transaction" narrative is misleading -- nothing is grouped into one transaction. Either add BEGIN; after STEP 1 (or before the DML) or drop the COMMIT and the comment. (high confidence)
| -- STEP 5: Commit the transaction | |
| COMMIT; | |
| -- STEP 5: (statements above run in autocommit; no explicit COMMIT needed) |
| -- ============================================================================ | ||
|
|
||
| -- View UNDO log statistics | ||
| SELECT * FROM pg_stat_get_undo_logs(); |
There was a problem hiding this comment.
pg_stat_get_undo_logs(), pg_stat_get_undo_buffers(), and pg_undo_force_discard() are implemented in C (undostats.c) but have no corresponding entries in pg_proc.dat (or any other catalog data file). As a result they are not SQL-callable and these statements fail with ERROR: function ... does not exist. Register them in pg_proc.dat with OIDs in the 8000-9999 range. (high confidence)
| @@ -0,0 +1,47 @@ | |||
| -- ============================================================================ | |||
| -- Example 4: Transactional File Operations (FILEOPS) | |||
There was a problem hiding this comment.
This new top-level examples/ directory is not referenced by any Makefile or meson.build rule, so these scripts are never built, installed, or executed by any test schedule -- they will silently rot as the flux/UNDO/FILEOPS APIs evolve. PostgreSQL patches document user-visible features in doc/src/sgml/ and exercise them via regression/TAP tests (the corresponding src/test/regress/sql/flux.sql and fileops.sql already exist in this change). Consider dropping these loose examples in favor of the SGML docs + regression coverage, or wiring them into an installed location. (moderate confidence)
|
|
||
| /* | ||
| * FluxForgetSerializeLocks -- drop the tracking set without releasing (abort | ||
| * path: LockReleaseAll has already/will release the locks themselves). |
There was a problem hiding this comment.
This comment contains a non-ASCII zero-width space (U+200B) between "already/" and "will". PostgreSQL source must be ASCII-only. Remove the embedded character (and the stray slash reads oddly - prefer "has already released, or will release").
| * path: LockReleaseAll has already/will release the locks themselves). | |
| * path: LockReleaseAll has already released, or will release, the locks itself). |
| FluxLockMultipleTuples(Relation rel, ItemPointerData *tids, int ntids, | ||
| LockTupleMode mode, bool wait) |
There was a problem hiding this comment.
FluxLockMultipleTuples has no callers anywhere in the tree (only this definition and the header declaration). Per the project's YAGNI/minimalism discipline, dead code should not be added. Beyond being unused, it does not dedup duplicate TIDs before locking, so a repeated TID would bump the lock-manager refcount twice but the partial-failure cleanup releases each slot once - an under-release footgun if it were ever wired up. Drop it until an actual caller exists.
| * mode - lock mode (typically ShareLock or ExclusiveLock) | ||
| */ | ||
| void | ||
| FluxLockPage(Relation rel, BlockNumber blkno, LOCKMODE mode) |
There was a problem hiding this comment.
FluxLockPage / FluxUnlockPage have no callers in the tree (definition + header declaration only). These are speculative scaffolding for a defragmentation/cross-page-move path that is not wired up. Remove until a caller exists.
| * lockmode - lock mode (typically AccessExclusiveLock for DDL) | ||
| */ | ||
| void | ||
| FluxLockRelationForDDL(Relation rel, LOCKMODE lockmode) |
There was a problem hiding this comment.
FluxLockRelationForDDL has no callers and is a trivial one-line wrapper around LockRelationOid(RelationGetRelid(rel), lockmode). This is dead code plus a premature abstraction over an existing helper - drop it and call LockRelationOid directly at the (currently nonexistent) call site.
| * Returns true if the current transaction holds the specified lock. | ||
| */ | ||
| bool | ||
| FluxHoldsTupleLock(Relation rel, ItemPointer tid, LockTupleMode mode) |
There was a problem hiding this comment.
Two problems. (1) FluxHoldsTupleLock has no callers - dead code. (2) Its LockTupleMode->LOCKMODE mapping (KeyShare/Share->ShareLock, NoKeyExclusive/Exclusive->ExclusiveLock) does NOT match the mapping used by FluxLockTuple/FluxUnlockTuple (KeyShare->AccessShareLock, Share->RowShareLock, NoKeyExclusive->ExclusiveLock, Exclusive->AccessExclusiveLock, i.e. heap's tupleLockExtraInfo). So a "holds lock" check would return false for a lock actually acquired at KeyShare/Share/Exclusive, making it unreliable for its stated debug-assert purpose. Remove it, or if kept, use the identical mapping as FluxLockTuple.
There was a problem hiding this comment.
🔍 OCR found 314 issue(s).
- 25 inline, 289 in summary (inline capped at 25)
⚠️ 2 warning(s) during review
📄 src/include/access/undo.h
This claim is inaccurate. UndoShmemSize()/UndoShmemInit() are not called from ipci.c -- ipci.c contains no UNDO references. undo.c wires the UNDO subsystem through the PG_SHMEM_SUBSYSTEM registration mechanism (UndoShmemRequest_internal / UndoShmemAttach_internal with .attach_fn), not via direct ipci.c calls. Update the comment to describe the actual registration path so it does not mislead readers about how UNDO shmem is initialized.
📄 src/include/access/slog.h
Aspirational/future-tense comment. There is no ZHEAP table AM in the tree (all 'zheap' references elsewhere are comparative design notes / a vendored per-backend UNDO engine, not an AM that consumes this tuple sLog). Per comment-accuracy discipline, describe current consumers only (FLUX and RECNO) rather than AMs 'that follow'.
📄 examples/05-undo-monitoring.sql (L13-L13)
pg_undo_force_discard(PG_FUNCTION_ARGS) reads a mandatory force_rotate bool argument via PG_GETARG_BOOL(0) (see src/backend/access/undo/undostats.c:274). This call passes zero arguments, so even once the function is registered in the catalog it will fail with ERROR: function pg_undo_force_discard() does not exist (no zero-arg signature) or a wrong-argument-count error. Pass an explicit boolean, e.g. SELECT pg_undo_force_discard(false);. (high confidence)
💡 Suggested change
Before:
SELECT pg_undo_force_discard();
After:
SELECT pg_undo_force_discard(false);
📄 examples/04-transactional-fileops.sql (L29-L30)
This comment is factually wrong. The FILEOPS DELETE path only writes the XLOG_FILEOPS_DELETE record at commit time, inside the if (isCommit && XLogIsNeeded()) branch of the pending-op processing (src/backend/storage/file/fileops.c:1685-1697), not when the DROP TABLE statement runs. At this point in the transaction no delete WAL record exists yet; the deletion (and its WAL record) are deferred until COMMIT. The comment misleads readers of a teaching example. (high confidence)
💡 Suggested change
Before:
-- The relation file still exists on disk (deletion deferred).
-- A XLOG_FILEOPS_DELETE WAL record has been written.
After:
-- The relation file still exists on disk (deletion deferred until commit).
-- No WAL record has been written yet; the XLOG_FILEOPS_DELETE record is
-- emitted atomically at COMMIT time.
📄 src/backend/access/flux/flux_dirtymap.c (L224-L225)
The lock-free correctness argument is unbacked by memory barriers on weakly-ordered CPUs (ARM64/PPC64/RISC-V/s390x). pg_atomic_write_u64 and pg_atomic_read_u64 in PostgreSQL provide NO barrier semantics (see port/atomics.h; the barrier variants are pg_atomic_write_membarrier_u64/pg_atomic_read_membarrier_u64). Two hazards: (1) In Mark, the key store can be reordered before the writer's preceding page modification becomes visible; a concurrent lock-free reader could observe the published key but a stale page, or vice versa. (2) In Check, a reader can observe a stale-empty slot even after the writer's spinlock release, because the plain atomic load carries no acquire ordering with respect to the store. The claimed "mark-before-buffer-unlock" happens-before edge only holds if the publish store and the reader load are ordered by barriers on both sides -- buffer-lock LWLock acquire/release do not order accesses to this independent shared array. Add a pg_write_barrier() before publishing the key in Mark, and a pg_read_barrier() after a matching load in Check (or use the membarrier atomic variants). Without this, scanners can skip a needed sLog before-image probe and serve the wrong tuple version -> silent wrong results. [high confidence]
📄 src/backend/access/flux/flux_dirtymap.c (L186-L192)
DirtyMap is dereferenced in both Mark and Check with no NULL guard or Assert. If either is reached before FluxDirtyMapShmemInit_cb ran, this is a NULL deref/crash. Given FluxDirtyMapMark is called from the DML paths and FluxDirtyMapCheck from scan paths, an Assert(DirtyMap != NULL) at the top of both functions would at least catch a mis-wired startup order in assert builds and document the precondition. [moderate confidence]
📄 src/backend/access/flux/flux_dirtymap.c (L70-L71)
The key is composed from relid (a plain Oid) only: ((uint64) relid << 32) | blkno. Table OIDs are unique only within a database, not cluster-wide. Two relations in different databases sharing the same OID map to identical keys, so a page marked dirty in DB A produces a false-dirty (harmless extra probe) for the same OID/blkno in DB B, but more importantly a shared, cluster-wide map keyed on a per-database OID cannot distinguish them -- the design assumption "Each (relid, blkno) page owns a distinct 64-bit key" (see header) is false across databases. Since callers pass RelationGetRelid (the pg_class OID, reused after DROP/OID wraparound), a dropped-and-recreated relation reusing an OID inherits stale dirty bits. Consider keying on the RelFileNumber/dboid or documenting that collisions only ever err toward false-dirty (safe) -- but confirm the always-safe direction actually holds. [moderate confidence]
📄 src/backend/access/flux/README (L12-L12)
Broken file reference: there is no DESIGN file in src/backend/access/flux/. The directory contains only the source files plus PHASE8_FLUX_PERBACKEND_DESIGN.md (itself a phase-scratch design note that arguably shouldn't ship). Either point at the actual document or drop this sentence so the README doesn't cite a non-existent file. (high confidence)
📄 src/backend/access/flux/flux_dirtymap.c (L158-L162)
FluxDirtyMapShmemInit() is an exported, empty no-op that is never called: all initialization runs through the FluxDirtyMapShmemCallbacks mechanism registered in subsystemlist.h (which invokes FluxDirtyMapShmemRequest/FluxDirtyMapShmemInit_cb). This is dead scaffolding and a footgun -- an author or extension might call it expecting the map to be initialized, but it does nothing. Remove the function and its declarations (it is declared in both flux_dirtymap.h and flux.h). (high confidence)
📄 src/backend/access/flux/flux_fsm.c (L160-L160)
FluxGetCommitTimestamp() is called inside the critical section, but it can raise elog(ERROR, "FLUX MVCC not initialized") when FluxMvccShmem == NULL (see flux_mvcc.c). Any elog(ERROR) inside START_CRIT_SECTION() escalates to PANIC. Hoist this call above START_CRIT_SECTION() so the error path is handled normally, matching the tree's convention of doing error-prone work outside crit sections. Confidence: high.
📄 src/backend/access/flux/flux_fsm.c (L233-L233)
This call to FreeSpaceMapPrepareTruncateRel() is both redundant and incomplete. Per its own contract (freespace.c:264-273), FreeSpaceMapPrepareTruncateRel() only prepares truncation: the caller must additionally smgrtruncate() the FSM fork and call FreeSpaceMapVacuumRange() to finalize. FluxVacuumFSM does neither, so this leaves the FSM in a prepared-but-not-truncated state (tail of the last FSM page zeroed, but pages not dropped and upper levels not updated). Moreover the only caller (flux_relation_vacuum, flux_operations.c:7374) already performs the real truncation via FluxTruncateRelation -> RelationTruncate, which does the complete FSM truncate internally (storage.c:315-327). That same caller then passes new_nblocks = RelationGetNumberOfBlocks(onerel) (the post-truncate size), so new_nblocks < old_nblocks is always false and the branch never even fires. This helper is effectively dead code and its FSM handling is broken; recommend removing it. Confidence: high.
📄 src/backend/access/flux/flux_fsm.c (L193-L193)
FreeSpaceMapVacuumRange() is invoked on every single relation extension. This is a per-extend cost on the insert hot path that heap deliberately avoids: heap relies on RecordAndGetPageWithFreeSpace()/RelationGetTargetBlock caching and only vacuums the FSM tree during VACUUM. Walking the FSM tree up to the root on each extend adds contention under concurrent inserts. Consider caching the just-extended block via RelationSetTargetBlock() (as heap does) so the extending backend reuses it directly instead of forcing an FSM upper-level update every time. Confidence: moderate.
📄 src/backend/access/flux/flux_fsm.c (L57-L58)
PostgreSQL comment style favors concise "why" comments over long narrative blocks with all-caps emphasis (ASK, OWN, HANGS, AND) and specific bug anecdotes with byte counts ("with fillfactor=10 the reservation is 7428 bytes, so a 1000-byte row asked for 8428..."). This block will draw style nits on -hackers. Trim to the essential invariant: the FSM request must never exceed FLUX_MAX_TUPLE_SIZE and must equal the recheck bar, or the insert retry loop cannot terminate. Confidence: moderate (style).
📄 src/backend/access/flux/flux_lock.c (L154-L154)
Line 154 contains a non-ASCII character: a zero-width space (U+200B) between "already/" and "will". PostgreSQL source and diffs must be ASCII-only. Remove it (and the stray slash reads oddly). Suggest: "path: LockReleaseAll has already released or will release the locks themselves)."
💡 Suggested change
Before:
* path: LockReleaseAll has already/will release the locks themselves).
After:
* path: LockReleaseAll has already released or will release the locks
* themselves).
📄 src/backend/access/flux/flux_lock.c (L318-L319)
FluxLockPage, FluxUnlockPage, FluxLockMultipleTuples, FluxLockRelationForDDL, and FluxHoldsTupleLock have no callers anywhere in the tree (verified across src/backend/access/flux and the headers - only their prototypes and definitions exist). This is speculative scaffolding: thin, unused wrappers over the standard lock manager plus a hand-rolled O(n^2) bubble-sort multi-tuple locker. Per YAGNI, remove the unused helpers (and their prototypes in flux.h); reintroduce them with an actual caller and tests when a real need arises. If any of these are intended to be used, wire up the caller in this patch so the lock-ordering and correctness can be reviewed.
📄 src/backend/access/flux/flux_lock.c (L145-L146)
The XACT_EVENT_PREPARE branch in FluxXactCallback (flux_mvcc.c) does not clear the flux_serlocks tracking set, while the serialization tuple locks themselves are transferred to the prepared xact's dummy PGPROC by the 2PC machinery. After PREPARE the backend-local flux_serlock_n stays non-zero with stale (dbid, relnumber, blkno, offnum) entries; the LockHeldByMe() guard below prevents a wrong LockRelease at the next COMMIT, but the stale entries survive into subsequent transactions and could spuriously match an unrelated row's lock. FluxXactCallback should call FluxForgetSerializeLocks() on XACT_EVENT_PREPARE. Please confirm/fix in flux_mvcc.c.
📄 src/backend/access/flux/flux_lock.c (L13-L15)
The file-header NOTES claims this implements "tuple-level locks with deadlock detection". The code does no deadlock detection of its own - it relies entirely on the standard heavyweight lock manager's detector via LockAcquire/LockAcquireExtended. Comments must describe what the code does now; drop the "deadlock detection" claim to avoid overstating the implementation.
📄 src/backend/access/flux/flux_fsm.c (L133-L133)
This wrapper passes needed straight to GetPageWithFreeSpace() without clamping to MaxFSMRequestSize. FluxTargetFreeSpace() exists precisely to keep the request inside the FSM's representable range, but nothing forces callers of FluxGetPageWithFreeSpace() through it. FluxGetPageWithFreeSpace() is an exported API (flux.h) and at least one caller path (speculative insert in flux_handler.c) passes a raw tuple_size that never went through FluxTargetFreeSpace(). If a caller ever passes needed > MaxFSMRequestSize, fsm_space_needed_to_cat() raises elog(ERROR, "invalid FSM request size") on a legal INSERT -- the exact footgun the comment on FluxTargetFreeSpace warns about, left undefended in the FSM wrapper itself. Clamp defensively here so the contract holds regardless of caller. (confidence: moderate)
💡 Suggested change
Before:
target_block = GetPageWithFreeSpace(rel, needed);
After:
/*
* Clamp to what the FSM can represent; a request above MaxFSMRequestSize
* makes fsm_space_needed_to_cat() raise "invalid FSM request size".
*/
needed = Min(needed, (Size) FLUX_MAX_TUPLE_SIZE);
target_block = GetPageWithFreeSpace(rel, needed);
📄 src/backend/access/flux/flux_fsm.c (L228-L230)
BlockNumber old_nblocks = RelationGetNumberOfBlocks(rel);
if (new_nblocks < old_nblocks)
📄 src/backend/access/flux/flux_fsm.c (L228-L230)
The truncation branch here is effectively dead code as wired: old_nblocks is fetched via RelationGetNumberOfBlocks(rel), and the sole caller (flux_operations.c) invokes FluxVacuumFSM(onerel, RelationGetNumberOfBlocks(onerel)), passing the current block count as new_nblocks. Both values are read from the same relation size, so new_nblocks < old_nblocks can never hold at that call site and FreeSpaceMapPrepareTruncateRel() is never reached. Combined with the confirmed finding that this truncation handling is itself incomplete (missing smgrtruncate + FreeSpaceMapVacuumRange), this branch should either be removed or the caller must pass the real post-truncation block count. (confidence: moderate)
📄 src/backend/access/flux/flux_handler.c (L927-L930)
This comment claims the function "delegates to the regular flux_scan_getnextslot", but it does not: it re-implements a bespoke page loop with its own visibility check. Two consequences, both serious:
-
WRONG RESULT / MVCC violation: unlike the seqscan, bitmap, and index-fetch paths, this loop performs NO before-image substitution for committed in-place UPDATEs. When FluxTupleVisibleToSnapshotDual() returns false for an on-page tuple carrying FLUX_TUPLE_UPDATED with a valid version pointer, it simply
continues (see the visibility check below), so an RR/SERIALIZABLE reader scanningWHERE ctid BETWEEN ...never sees the visible before-image reconstructed by FluxReconstructVisibleVersion() that every other scan path serves. The row is silently dropped or the wrong (newer) version implied. -
SSI hole: this path never acquires per-tuple SIREAD predicate locks (PredicateLockTID) for visible tuples nor calls FluxCheckForSerializableConflictOut for invisible ones, unlike flux_prepare_pagescan and flux_index_fetch_tuple. Under SERIALIZABLE a TID-range scan therefore leaves no rw-dependency trace, reintroducing exactly the write-skew hole the seqscan path documents fixing.
Either delegate to flux_scan_getnextslot and filter by TID as the comment claims, or replicate the before-image + SSI handling here.
📄 src/backend/access/flux/flux_handler.c (L1027-L1033)
MVCC violation: when the on-page (newest) version is invisible, this path just skips the tuple. Every other FLUX scan path (seqscan flux_prepare_pagescan/flux_scan_getnextslot, bitmap, index fetch) reconstructs and serves the visible before-image via FluxReconstructVisibleVersion() when the tuple is FLUX_TUPLE_UPDATED with a valid version pointer under an MVCC snapshot. Missing that here means an old-snapshot reader on a TID-range scan gets a wrong result for in-place-updated rows. Additionally, no SIREAD PredicateLockTID / FluxCheckForSerializableConflictOut is performed here, so SERIALIZABLE conflict detection is silently skipped for this scan path.
📄 src/backend/access/flux/flux_handler.c (L2683-L2689)
Dead-but-dangerous code violating hint-bit discipline. buffer is unconditionally valid here (ReadBuffer'd and share-locked at the top of flux_tuple_satisfies_snapshot, never released before this point), so the !BufferIsValid(buffer) else arm is dead. But if ever reached it does a raw tuple_hdr->t_flags &= ~FLUX_TUPLE_UNCOMMITTED on a shared-buffer page while holding only BUFFER_LOCK_SHARE, with no MarkBufferDirtyHint and no lock upgrade -- a torn/lost-write race against concurrent readers/writers. The codebase's own FluxSetHintBits explicitly refuses this ("Setting the in-memory bit without the lock would race a concurrent writer's t_flags update, so we do nothing rather than a bare |="). BufferSetHintBits16 (the other arm) is the only correct path. Drop the raw-write else arms in both this branch and the "No current transaction" branch below.
📄 src/backend/access/flux/flux_lock.c (L76-L79)
The dedup rationale here is factually wrong about lock-manager behavior. If the same row's tuple lock is ever acquired twice in one transaction, its LOCALLOCK nLocks reaches 2 but only one row is tracked. FluxReleaseSerializeLocks() then calls LockRelease exactly once (nLocks -> 1), leaving the LOCKTAG_TUPLE lock still held. The subsequent LockReleaseAll(DEFAULT_LOCKMETHOD, allLocks=false) on the commit path does NOT release it "naturally": it unconditionally fires elog(WARNING, "tuple lock held at commit") for any LOCKTAG_TUPLE locallock with nLocks > 0 (lock.c: LOCALLOCK_LOCKTAG(*locallock) == LOCKTAG_TUPLE && !allLocks), regardless of prior releases. The comment's claim that the extra refs "release naturally at LockReleaseAll, which does not assert once our explicit release has cleared the last one" is incorrect. The current sole caller happens to gate re-locking (self_holds_lock in flux_operations.c), so the double-ref case is not reached today, but the comment must describe actual behavior: either drop the incorrect justification or release once per tracked ref count rather than once per distinct row.
📄 src/backend/access/flux/flux_handler.c (L526-L528)
Allocating scan-scratch in TopMemoryContext (session lifetime) is a leak-by-design. This buffer is freed only in flux_scan_end (if (scan->rs_vistuples) pfree(...)). If the scan errors out via ereport between here and scan_end, or scan_end is not reached, the allocation leaks for the whole session; repeated failed scans accumulate MaxOffsetNumber*sizeof(OffsetNumber) each. Scan-lifetime scratch belongs in a scan/query MemoryContext (or the CurrentMemoryContext at beginscan) that is reset/deleted on error, not TopMemoryContext. The bitmap path below has the same problem.
📄 src/backend/access/flux/flux_handler.c (L1505-L1507)
Hot-path regression: flux_index_getnext_slot calls this per returned tuple during a delete-marking index scan, and each call does BuildIndexInfo() + CreateExecutorState() + FreeExecutorState() -- O(n) heavyweight allocation/teardown that heap's index scan never incurs. IndexScanFluxData (in flux.h) has no cache slot; the IndexInfo, EState and ExprContext should be built once in flux_index_scan_begin, cached in scan->xs_table_opaque, reused per tuple, and torn down in flux_index_scan_end. As written, an index range scan returning many rows on a delete-marking table pays a full executor-state build/free for every row.
📄 src/backend/access/flux/flux_handler.c (L3991-L3996)
Bottom-up freespace accumulation is broken: actualfreespace is declared and reset to 0 inside the loop body on every iteration, so it only ever holds the current entry's freespace, never a running total. The early-stop actualfreespace >= delstate->bottomupfreespace therefore fires (or fails to fire) based on a single entry rather than the cumulative reclaimable space, defeating the space-target early exit. Compare heap_index_delete_tuples() (heapam.c ~L8362), which declares actualfreespace = 0 ONCE outside the loop and accumulates across entries. Move the declaration/initialization out of the loop and use actualfreespace += istatus->freespace; cumulatively.
📄 src/backend/access/flux/flux_handler.c (L4786-L4796)
Per-plan buffer I/O during planning. When reltuples is unavailable (rd_rel->reltuples < 0, i.e. never ANALYZEd/VACUUMed), this fallback reads up to 10 pages under BUFFER_LOCK_SHARE on every planner call for the relation. The generic table_block_relation_estimate_size() (tableam.c) estimates tuple width from attribute datatypes with zero buffer access precisely to avoid this. On a freshly-loaded FLUX table under concurrency, every query plan pays real page reads plus share-lock acquisition. Prefer estimating tuple width from the tuple descriptor (as heap does) rather than probing live pages.
📄 src/backend/access/flux/flux_handler.c (L430-L432)
Rescan key/nkeys handling is inconsistent and risks a key/nkeys mismatch. When key != NULL this keeps the old rs_nkeys but re-points rs_key to the caller's new array; if a rescan ever supplies a different number of keys, rs_nkeys will not match the array length, leading to out-of-bounds ScanKey access. When key == NULL it zeroes rs_nkeys but then still assigns rs_key = key (NULL). Compare heap_rescan/initscan (heapam.c): it copies into its own rs_key array using the fixed rs_nkeys (if (key != NULL && rs_nkeys > 0) memcpy(rs_key, key, rs_nkeys*...)) and never leaves nkeys/key out of sync. Follow that contract.
📄 src/backend/access/flux/flux_handler.c (L1389-L1389)
Phase-tracking comments do not belong in committed code. "Phase 8c", "invariant I2", "Phase 8b", and "as of Phase 8b" describe an internal development timeline, not what the code does now. Comments must explain the current behavior and the why, without reference to internal phase numbers or a development roadmap. Rewrite to describe the delete-marking key recheck on its own terms. (Same for the other "Phase 8c" occurrences in flux_index_entry_key_mismatch's header, the recheck call site, and the TableAmRoutine header comment.)
📄 src/backend/access/flux/flux_handler.c (L3769-L3769)
Do not reference internal triage/design documents (flux_default_triage.md) in shipped source comments -- the file is not part of the tree a reader can consult, and this is a development-process artifact. State the actual invariant (slot->tts_tid may be invalid on the FK fast-path batch-flush caller, so ItemPointerIsValid is required) without the doc pointer.
📄 src/backend/access/flux/flux_mvcc.c (L717-L728)
SSI conflict-out is silently skipped for committed concurrent writers, which is the primary case SSI must detect. flux_header_dirty_xid() returns InvalidTransactionId unless the writer TransactionIdIsInProgress() is true (see flux_operations.c: it filters on TransactionIdIsInProgress(xmax)/(xmin) and otherwise returns InvalidTransactionId). Heap's HeapCheckForSerializableConflictOut deliberately resolves the writer XID for committed tuples too (HEAPTUPLE_LIVE -> xmin, HEAPTUPLE_RECENTLY_DEAD -> update xid), filtering only by TransactionXmin after obtaining the xid. Here, any writer that has already committed makes xid invalid, the function returns at the !TransactionIdIsValid(xid) branch, and CheckForSerializableConflictOut() is never reached -- so a serializable reader encountering a tuple written (and made invisible) by a recently-committed concurrent transaction records no rw-antidependency. This can miss serialization anomalies and produce incorrect SERIALIZABLE results. The inline comment claiming the header "stays authoritative through the writer's PRE_COMMIT window" is misleading: the helper never considers committed writers at all. Use a resolution that yields the committed writer's XID (t_xmin for insert-invisible, t_xmax for delete-visible), guarded by TransactionXmin, mirroring the heap path.
📄 src/backend/access/flux/flux_mvcc.c (L149-L149)
Non-ASCII em-dash (U+2014) in source comment violates the ASCII-only source rule (hard gate). Replace with ASCII "--".
💡 Suggested change
Before:
* no lock is needed — just a compiler barrier via volatile access.
After:
* no lock is needed -- just a compiler barrier via volatile access.
📄 src/backend/access/flux/flux_mvcc.c (L525-L525)
Non-ASCII em-dash (U+2014) in source comment violates the ASCII-only source rule (hard gate). Replace with ASCII "--".
💡 Suggested change
Before:
* reads it), so no lock is needed — just a write barrier.
After:
* reads it), so no lock is needed -- just a write barrier.
📄 src/backend/access/flux/flux_mvcc.c (L725-L725)
Non-ASCII em-dash (U+2014) in source comment violates the ASCII-only source rule (hard gate). Replace with ASCII "--".
💡 Suggested change
Before:
* conflict to report — analogous to heap's HEAPTUPLE_DEAD case.
After:
* conflict to report -- analogous to heap's HEAPTUPLE_DEAD case.
📄 src/backend/access/flux/flux_mvcc.c (L182-L185)
Dead fields in FluxTransactionState. is_read_only is set to true in FluxInitTransactionState and never set to false nor read anywhere in the FLUX code (no "first write" transition exists). rc_read_point_cid/rc_read_point_xcc are initialized but never read -- the RC per-command read point they document is no longer consulted. xact_commit_ts is written only to feed serializable_horizon, which is itself write-only (never read anywhere). Per the minimalism/YAGNI rule, remove these unused fields (and the write-only serializable_horizon + its mvcc_lock) rather than carrying speculative state.
📄 src/backend/access/flux/flux_mvcc.c (L278-L284)
Dead second shared-memory init path. Only FluxMvccShmemCallbacks (registered via PG_SHMEM_SUBSYSTEM in subsystemlist.h -> FluxMvccShmemRequest + FluxMvccShmemInit_cb) is actually wired. FluxMvccShmemInit() (the ShmemInitStruct variant) has no caller anywhere in the tree, yet it duplicates the full init body and separately registers on_shmem_exit(FluxShmemExit, 0). Two copies of the init logic risk drifting out of sync, and the unused ShmemInitStruct path is confusing dead code. Per the minimalism rule, drop FluxMvccShmemInit() (and its header declaration) and keep only the subsystem callback path.
📄 src/backend/access/flux/flux_mvcc.c (L818-L824)
Dead public API / unused code. FluxTupleVisibleToSnapshot() (a thin wrapper that only forwards to FluxTupleVisibleToSnapshotDual), FluxGetSnapshotTimestamp(), FluxAbortTransaction(), FluxUpdateOldestActiveTimestamp(), FluxGetMvccStats(), and the FLUX_RESTART_* macros have no callers in the tree. This is speculative scaffolding; per the minimalism rule remove the unused functions/macros (and their header declarations) or wire them to a real caller.
📄 src/backend/access/flux/flux_mvcc.c (L895-L896)
oldest_active_ts is a plain (non-atomic) uint64 in shared memory written concurrently without a common lock from multiple backends: FluxGetOldestActiveTimestamp() slow path, FluxCleanupTransactionState(), FluxResolvePreparedSlot() (under TwoPhaseStateLock), and read here without a lock. On 32-bit platforms a 64-bit store/load is not atomic, so a torn value is possible. This is not a vacuum/reclamation gate (reclamation uses FluxGetOldestXminHorizon -> GetOldestNonRemovableTransactionId), so it cannot cause data loss, but a torn value still flows into the cosmetic DEFRAG WAL commit-ts word and the VM all-visible hint. The "benign race" comment understates the 32-bit torn-read hazard; consider pg_atomic_uint64 or documenting the field as a lock-free hint that consumers must not treat as monotonic/exact.
📄 src/backend/access/flux/flux_mvcc.c (L1206-L1209)
Hot-path stack cost: for every visibility check on a current-transaction uncommitted tuple (FLUX_TUPLE_UNCOMMITTED set), this allocates SLogTupleOp e[SLOG_MAX_TUPLE_OPS] on the stack (SLOG_MAX_TUPLE_OPS == 128; sizeof(SLogTupleOp) ~40B => ~5KB frame) and performs an sLog seqlock read + hash probe + memcpy of up to 128 ops. During a scan over a transaction's own freshly-inserted rows this repeats per tuple. Consider capping the fetch (only SLOG_OP_ABORTED and self-INSERT ops are inspected, so a small fixed buffer or an early-out lookup that stops at the first ABORTED / matching-cid op would avoid the 5KB frame and the full 128-op copy).
📄 src/backend/access/flux/flux_operations.c (L385-L388)
API-contract defect: FluxPagePruneOpt is declared int (flux.h) and this comment states it "Returns the number of tuples pruned", but the success path ends with return true; (i.e. 1) regardless of how many tuples (ndead) were actually pruned, while every other exit returns 0. So the documented count semantics are false. Callers only use it as a boolean (if (FluxPagePruneOpt(...)) in flux_tuple_insert; the scan paths ignore the result), so either change the declared type and this comment to bool, or actually return ndead. As written the int/return true mismatch is a POLA violation waiting to bite any future caller that trusts the doc.
💡 Suggested change
Before:
* Returns the number of tuples pruned.
*/
int
FluxPagePruneOpt(Relation relation, Buffer buffer)
After:
* Returns true if any tuples were pruned, false otherwise.
*/
bool
FluxPagePruneOpt(Relation relation, Buffer buffer)
📄 src/backend/access/flux/flux_operations.c (L6141-L6151)
Dead code / unreachable index-maintenance apparatus. FluxVacuumCrossPageDefrag returns early with if (nindexes > 0) return 0; (a few lines above, line ~6105). Consequently this entire if (nindexes > 0) block that builds the EState, slot and indexInfoArray can NEVER execute, and likewise the later deferred_moves accumulation and the deferred index_insert/index_insert_cleanup replay loop are unreachable. The function also takes nindexes/indrels parameters it can never use for their stated purpose. Per the minimalism/YAGNI discipline, remove the unreachable index-handling code paths (and simplify the signature) rather than shipping dead scaffolding.
📄 src/backend/access/flux/flux_operations.c (L1195-L1195)
ASCII-only source is a hard gate for pgsql-hackers, and these comments use a non-ASCII em-dash (U+2014). This file contains many such em-dashes (e.g. lines around 937, 968, 1079, 1195, 1332, 4288, 5528, 5557, 5576, 5671, 5973, 7604, 7636, 7684, 7802, 7863) as well as elsewhere. Replace every em-dash with ASCII -- (or restructure the sentence). git diff --check / the ASCII gate will otherwise reject the patch.
💡 Suggested change
Before:
* to prevent concurrent modifications — heavyweight tuple locks
After:
* to prevent concurrent modifications -- heavyweight tuple locks
📄 src/backend/access/flux/flux_operations.c (L113-L113)
WIP/phase/bug-tracker comments do not belong in a committed patch. This file is littered with references to internal development phases and bug numbers meaningless upstream -- "Phase 8b/8c" (lines 113, 2369, 2574, 2723, 2820, 2984, 2986, 3752, 7346, 8117), "BUG 2/BUG 4/BUG 5" / "bug 4"/"bug 5" (lines 1961, 1972, 3687-3688, 3988, 4000, 4320, 4646, 5037, 5065, 5562-5563), a FIXME(flux hot-row scaling) with "Deferred:" (lines 3061-3064), and "Future enhancement:" aspirational comments (lines 2019, 5626). pgsql-hackers rejects FIXME/future-tense/phase-tracking comments; rewrite them to describe what the code does now (the "why"), and drop the phase/bug shorthand and the empty placeholder comments like the two bare "BUG 5 ..." lines that document the ABSENCE of code.
📄 src/backend/access/flux/flux_handler.c (L3889-L3892)
Hot-standby correctness bug: this function always returns InvalidTransactionId as the snapshotConflictHorizon, but never advances it from the deleted tuples' xmax the way heap_index_delete_tuples() does via HeapTupleHeaderAdvanceConflictHorizon(). The caller _bt_delitems_delete() (nbtpage.c) WAL-logs this value into the index-deletion record whenever XLogStandbyInfoActive(), and hot standby uses it to generate recovery conflicts that cancel queries whose snapshots could still see the now-removed index entries. Returning InvalidTransactionId means FLUX index-entry removals produce no standby recovery conflict, so a standby query can follow a dangling index entry to a reclaimed heap slot -> wrong results / apparent corruption on the replica. Track the newest committed deleter XID (FluxTupleGetXmax) of the entries marked knowndeletable and return it here.
📄 src/backend/access/flux/flux_handler.c (L2235-L2239)
Read of on-page tuple bytes without the buffer content lock, on an AM that does IN-PLACE updates. After this BUFFER_LOCK_UNLOCK the buffer is only pinned, yet the code below dereferences tuple_hdr (a page pointer): it reads tuple_hdr->t_flags, ItemIdGetLength(itemid), and hands tuple_hdr to FluxReconstructVisibleVersion() and (further down) FluxSlotStoreTuple(). Because a concurrent transaction holding BUFFER_LOCK_EXCLUSIVE can rewrite these exact bytes in place at any time, this is a TOCTOU race that can read a torn/half-updated header or content. Heap can read post-unlock only because its on-page tuples are immutable once written; FLUX's in-place model breaks that assumption. Copy the tuple header + length into local storage while still holding the share lock, or defer the unlock until after the on-page reads.
📄 src/backend/access/flux/flux_handler.c (L1984-L1984)
Non-ASCII em-dash (U+2014) in source comments. PostgreSQL source must be ASCII-only (no smart quotes, em-dashes, or ellipsis characters). Em-dashes appear throughout this file (e.g. lines 2056, 2083, 2100, 2104, 2402, 2867, 4218, and many more); replace each with -- or , .
💡 Suggested change
Before:
* aborted — check sLog for ABORTED entries before declaring it
After:
* aborted -- check sLog for ABORTED entries before declaring it
📄 src/backend/access/flux/flux_handler.c (L4849-L4849)
Typo in comment: ~9%% should be ~9% (the doubled percent is not printf format text, it's a plain comment).
💡 Suggested change
Before:
* which showed up as ~9%% of CPU under high-concurrency pgbench. A stale
After:
* which showed up as ~9% of CPU under high-concurrency pgbench. A stale
📄 src/backend/access/flux/flux_mvcc.c (L293-L296)
mvcc_lock is initialized with LWTRANCHE_BUFFER_MAPPING, which is the tranche for the shared buffer mapping partition locks. Reusing an unrelated core tranche mislabels FLUX's lock in wait-event views (pg_stat_activity.wait_event will report "BufferMapping"), making contention diagnosis misleading. A new subsystem lock must register its own tranche (LWLockRegisterTranche / a dedicated LWTRANCHE_* entry). This appears twice (here and in FluxMvccShmemInit_cb at line 402). Note: if the serializable_horizon dead-state is removed, this lock disappears entirely.
📄 src/backend/access/flux/flux_mvcc.c (L1324-L1328)
NULL-pointer dereference inconsistency. This function explicitly guards snapshot != NULL when computing curcid, so the author treats snapshot == NULL as a reachable input. But the same possibly-NULL snapshot is then passed to FluxTupleSatisfiesMVCC(), whose first substantive operation is if (!IsMVCCSnapshot(snapshot)) -- and IsMVCCSnapshot() is ((snapshot)->snapshot_type == SNAPSHOT_MVCC), an unconditional dereference. A NULL snapshot therefore crashes. Either FluxTupleSatisfiesMVCC must handle NULL up front (e.g. if (snapshot == NULL) return ...;), or this defensive snapshot != NULL check is dead and should be dropped for consistency. The two disagree about whether NULL is possible.
📄 src/backend/access/flux/flux_operations.c (L4782-L4784)
TOAST temporaries and superseded external datums are leaked on the in-place UPDATE path. flux_toast_tuple() here sets upd_toasted = true and populates upd_ttc, but no exit path of flux_tuple_update_inplace (the TM_Ok success return, the force-shrink TM_Invisible/TM_Deleted returns, or the out-of-place fallback returns) ever calls flux_toast_cleanup(&upd_ttc). Compare the insert path (flux_tuple_insert), which calls if (toasted) flux_toast_cleanup(&ttc); after the tuple is stored. toast_tuple_cleanup() both frees palloc'd compressed/toasted temporaries and, on UPDATE, deletes superseded external TOAST datums. Missing it leaks memory and, more seriously, orphans old TOAST data on every wide-column UPDATE. Add the cleanup on all exit paths after this call.
📄 src/backend/access/flux/flux_operations.c (L4782-L4784)
The in-place UPDATE passes oldvalues=NULL, oldisnull=NULL to flux_toast_tuple(). Per that function's contract (flux_tuple.c), the old values are what let it "reuse unchanged external datums and schedule superseded ones for deletion". Passing NULL means every UPDATE that re-toasts a wide column re-externalizes it and never deletes the old external datum, orphaning TOAST rows. Pass the pre-update deformed values as oldvalues so unchanged externals are reused and superseded ones are reclaimed (this also depends on flux_toast_cleanup being called, see the other comment).
📄 src/backend/access/flux/flux_operations.c (L5792-L5792)
Stray double percent in a plain comment (not a printf format string): 100%% should be 100%. The doubling is only meaningful inside format strings.
💡 Suggested change
Before:
* in place, so a page packed 100%% full leaves no room for later
After:
* in place, so a page packed 100% full leaves no room for later
📄 src/backend/access/flux/flux_pbu.c (L88-L88)
The write-protocol state lives in module-level statics, and flux_pbu_prepared is only cleared in the success path (FluxPbuFinishUndo) or the never-called FluxPbuCancelUndo. Nothing resets it on error. Between FluxPbuPrepareInsert() (which sets it true) and the critical section, throwable code runs at the call sites: in the DELETE path GetCurrentTransactionId() (flux_operations.c:1860) can ereport(ERROR) on first XID assignment; in the UPDATE path the FluxXLogPrepareLogicalImage() calls (flux_operations.c:5396-5398) palloc and can OOM, and that path's PG_CATCH (flux_operations.c:5400) releases only the overflow buffers, never cancelling the prepared undo. On such a throw the transaction aborts but flux_pbu_prepared stays true (there is no XactCallback and FluxAbortTransaction() in flux_mvcc.c does not touch it). The next DML op then trips Assert(!flux_pbu_prepared) in assert builds, or in a production build silently reuses stale flux_pbu_urp/flux_pbu_undometa, corrupting the next record's version chain. This state needs an abort-time reset (e.g. a RegisterXactCallback / SubXactCallback hook, or wrapping the caller's prepare..finish window in PG_TRY with FluxPbuCancelUndo on error).
📄 src/backend/access/flux/flux_pbu.c (L290-L293)
FluxPbuCancelUndo() is dead code: it is declared and documented as the no-op-CAS cancel path, but no caller exists anywhere in the tree. The no-op CAS branch in flux_operations.c (around line 3483) explicitly notes "No UNDO record was prepared yet ... so nothing to cancel" because the prepare happens after that branch. Either wire this into the error/abort path (see the leaked-static issue above, where it is actually needed) or drop it per YAGNI.
📄 src/backend/access/flux/flux_pbu.c (L228-L228)
payload_len is a uint16 but is fed the full uur_payload.len (an int). Today the before-image is an on-page item bounded by the 15-bit line-pointer length (< 32KB), so this cannot overflow in the current paths. But this is an unchecked narrowing footgun: if a future caller ever passes a before-image >= 64KB, the length truncates silently here and FluxPbuRedoUndo() would then PANIC on its datalen != SizeOfFluxUndo + payload_len consistency check (or worse, read a truncated image). Add a defensive guard so the invariant is enforced where the field is filled rather than relying on a distant caller-side bound.
📄 src/backend/access/flux/flux_pbu.c (L711-L712)
The comment "the xact writes only these" is inaccurate. pbuUndoStartPtr/pbuUndoLatestPtr (via SetCurrentUndoLocation) track ALL per-backend undo records for the subtransaction regardless of rmid; a subxact that also performs a fileops operation would interleave UNDO_RMID_FILEOPS records into this same chain. The code is in fact correct because it filters on uur_rmid == UNDO_RMID_FLUX and walks the physical prev-record chain (so termination at stop_inclusive holds for any rmid mix), but the comment should reflect that foreign records may appear and are intentionally skipped rather than claiming the chain is FLUX-only.
📄 src/backend/access/flux/flux_pvs.c (L179-L184)
Visibility of the updater is decided solely by !XidInMVCCSnapshot(urec_xid, snapshot), without a commit-status check. An aborted or crashed updater whose xid is no longer in the snapshot's in-progress set will pass this test and its rolled-back image will be served as the visible version — a wrong-result/data-visibility bug. The canonical routine FluxTupleVisibleToSnapshotDual (flux_mvcc.c) combines the snapshot test with TransactionIdDidCommit: it treats a not-in-snapshot xid as visible only after confirming the xid actually committed (and returns invisible when !TransactionIdDidCommit). This step-visibility test must do the same: an updater that is not in the snapshot AND did not commit means the current candidate was rolled back and the walk should step back, not serve it. Note the confusingly-inverted variable naming too: !XidInMVCCSnapshot here means "updater committed-and-visible -> serve candidate", which is the opposite of the branch's stepped logic below.
📄 src/backend/access/flux/flux_pvs.c (L202-L204)
UndoRecPtr is typedef uint64 (see undodefs.h / pbu_undolog.h). Casting to unsigned long long and printing with %llu is a portability violation on platforms where the 64-bit type is not unsigned long long. Use UINT64_FORMAT (drop the cast), or the type-provided UndoRecPtrFormat. This pattern repeats in every WARNING in this function (FLUX_UNDO_UPDATE too-short, without-tuple, size mismatch, DELTA too-short, without-tuple, reverse-diff failed, and the default DEBUG2).
📄 src/backend/access/flux/flux_pvs.c (L96-L96)
The (void) tid; and "currently unused but reserved for future diagnostics" claim is inaccurate: tid is dereferenced below in the depth-cap WARNING (ItemPointerGetBlockNumber/Offset) and again in the invalid-verptr block is not, but the depth-cap path is real usage. All callers pass a non-NULL tid, so there is no crash today, but the comment contradicts the code. Drop the (void) tid; and the "unused" wording so the comment describes current behavior.
📄 src/backend/access/flux/flux_xlog.c (L1756-L1757)
Redo PANIC asymmetry with the INSERT path is a recoverability hazard. In flux_xlog_insert_redo(), a block-0 overflow PageAddItem() failure is treated as benign (WARNING + goto insert_skip_tuple), and the code explicitly documents that PageAddItem can legitimately fail after a later checkpointed operation (CLR/defrag/prune). Here the identical condition on the UPDATE path elog(PANIC)s, which would render a standby (or the primary after crash) permanently unrecoverable on a page state that INSERT redo tolerates. Make the UPDATE overflow-replay guards consistent with INSERT: WARN and skip rather than PANIC when the page is ahead of this record. (high confidence)
📄 src/backend/access/flux/flux_slot.c (L478-L481)
cmin/cmax lookup can return a value from an unrelated transaction. When GetTopTransactionIdIfAny() returns InvalidTransactionId, SLogTupleLookupFiltered() disables the xid filter and returns ALL active ops for the TID (see slog_tuple.c: "If InvalidTransactionId, returns all active ops"), so slog_entry.cid may come from a different in-progress transaction rather than this backend's command. Skip the sLog lookup (or leave cid = InvalidCommandId) when there is no current top transaction id instead of passing InvalidTransactionId as the filter. (medium confidence)
📄 src/backend/access/flux/flux_tuple.c (L518-L519)
Dead compression/overflow scaffolding. is_compressed[]/is_overflowed[] are palloc0'd (or stack-zeroed) and never set to true anywhere in this file, and has_compressed/has_overflow are always false (confirmed by the (void) has_compressed; marker and the comment that "work_values therefore mirrors values exactly"). This cleanup loop is therefore dead code today, but it is a footgun: work_values[i] for a varlena points into the caller's Datum (a page or slot), so if any future path ever set these flags, this would pfree memory the tuple does not own. Per YAGNI, drop the unused is_compressed/is_overflowed arrays and this loop (and the work_values copy) rather than leaving unwired scaffolding. (medium confidence)
📄 src/backend/access/flux/flux_xlog.c (L1065-L1065)
pgindent/whitespace defect: two forward declarations are joined on one line. This will not survive pgindent and violates one-declaration-per-line. Split into two lines.
💡 Suggested change
Before:
static void flux_xlog_delete_redo(XLogReaderState *record);static void flux_xlog_defrag_redo(XLogReaderState *record);
After:
static void flux_xlog_delete_redo(XLogReaderState *record);
static void flux_xlog_defrag_redo(XLogReaderState *record);
📄 src/backend/access/flux/flux_slot.c (L858-L858)
Non-ASCII em-dash in comment. PostgreSQL source is ASCII-only; replace the em-dash with an ASCII hyphen/space. Same issue on line 868 ("Different buffer — full clear...").
💡 Suggested change
Before:
/* Same buffer — just free any materialized data */
After:
/* Same buffer - just free any materialized data */
📄 src/backend/access/flux/flux_xlog.c (L2291-L2291)
Non-ASCII em-dash in comment. Replace with an ASCII hyphen to keep the source ASCII-only.
💡 Suggested change
Before:
* unreachable, but if it ever fires we must not PANIC —
After:
* unreachable, but if it ever fires we must not PANIC -
📄 src/backend/access/flux/flux_vm.c (L58-L59)
Maintainability: the redo handlers (flux_xlog_vm_set_redo/flux_xlog_vm_clear_redo in flux_xlog.c) recompute mapByte/mapOffset with open-coded (BLCKSZ - MAXALIGN(SizeOfPageHeaderData)) * 4 arithmetic that duplicates HEAPBLK_TO_MAPBYTE/HEAPBLK_TO_OFFSET defined here. The two are arithmetically identical today, but because they are maintained independently, any future change to these macros would silently make redo write the wrong VM bit. Consider exposing/sharing these macros so the forward and redo paths cannot diverge. (low confidence)
📄 src/backend/access/flux/flux_stats.c (L249-L249)
Portability convention: int64 fields are printed with %lld and a (long long) cast. The tree convention for int64/uint64 is INT64_FORMAT/UINT64_FORMAT. The explicit cast makes this technically portable, but for consistency with the codebase prefer INT64_FORMAT here and UINT64_FORMAT for the commit-ts range below. The same %llu-with-(unsigned long long)-cast pattern appears in flux_undo.c for UndoRecPtr (a uint64). (low confidence)
📄 src/backend/access/flux/flux_pvs.c (L226-L231)
Reconstructed images are re-cast to FluxTupleHeader * and dereferenced (hdr->t_verptr at offset 8, hdr->t_xmin at offset 24) on the next loop iteration, but their length is never validated against sizeof(FluxTupleHeader) (40 bytes). Here next_len = phdr.tuple_len is only checked to be non-zero and to fit within payload_size; a corrupted/short undo record with 1 <= tuple_len < FLUX_TUPLE_OVERHEAD yields an image smaller than the header, and the following iteration reads past the end of next_image -- an out-of-bounds read. Reject images shorter than the fixed header before stepping (e.g. if (phdr.tuple_len < FLUX_TUPLE_OVERHEAD) { warn; break; }).
💡 Suggested change
Before:
next_len = phdr.tuple_len;
next_image = (char *) palloc(next_len);
memcpy(next_image,
payload + SizeOfFluxUndoPayloadHeader,
next_len);
stepped = true;
After:
if (phdr.tuple_len < FLUX_TUPLE_OVERHEAD)
{
elog(WARNING,
"FLUX PVS: FLUX_UNDO_UPDATE image (%u) shorter than tuple header at " UINT64_FORMAT,
phdr.tuple_len, verptr);
break;
}
next_len = phdr.tuple_len;
next_image = (char *) palloc(next_len);
memcpy(next_image,
payload + SizeOfFluxUndoPayloadHeader,
next_len);
stepped = true;
📄 src/backend/access/flux/flux_pvs.c (L270-L274)
Same out-of-bounds hazard on the DELTA path: FluxUndoDeltaReconstruct validates internal delta consistency but does not enforce old_len >= FLUX_TUPLE_OVERHEAD. A malformed delta with old_len < 40 produces a reconstructed image smaller than FluxTupleHeader, and the next iteration's FluxTupleGetVersionPtr(hdr, ...)/FluxTupleGetXmin(hdr) read out of bounds. Validate delta->old_len >= FLUX_TUPLE_OVERHEAD before palloc/step.
💡 Suggested change
Before:
next_len = delta->old_len;
next_image = (char *) palloc(next_len);
if (!FluxUndoDeltaReconstruct(delta, delta_len,
candidate, candidate_len,
next_image))
After:
if (delta->old_len < FLUX_TUPLE_OVERHEAD)
{
elog(WARNING,
"FLUX PVS: FLUX_UNDO_UPDATE_DELTA old_len (%u) shorter than tuple header at " UINT64_FORMAT,
delta->old_len, verptr);
break;
}
next_len = delta->old_len;
next_image = (char *) palloc(next_len);
if (!FluxUndoDeltaReconstruct(delta, delta_len,
candidate, candidate_len,
next_image))
📄 src/backend/access/flux/flux_pbu.c (L378-L383)
Critical: the redo scan window is too small to survive an UPDATE that uses more than a few overflow pages. On the DO side, FluxXLogUpdate() folds undo at flux_undo_block_id = next_block_id, which grows past block id 4 once several distinct overflow pages (up to MAX_OVERFLOW_BUFFERS = 32) and/or a cross-page destination block are registered ahead of it. But flux_xlog_update_redo() calls FluxPbuRedoUndo(record, 1), and this loop only scans block ids first_block_id .. first_block_id + FLUX_PBU_MAX_UNDO_BLOCKS - 1, i.e. blocks 1..4. When the undo landed at block id >= 5, the scan skips the forward blocks (continue), runs out at block 4, leaves data == NULL, and returns WITHOUT replaying the undo insert. Per the code's own contract that leaves the undo log's insert pointer un-advanced during recovery, so every later record in that log is misplaced and the before-image needed for post-crash rollback / MVCC version chains is lost -- silent data corruption.
The window must cover the forward blocks the scan has to skip, not just the undo blocks. Either bound the loop by XLR_MAX_BLOCK_ID (the true upper limit of block ids on the record) instead of a fixed 4-slot window, or pass the DO-side first_block_id through to redo so the scan starts at the right block. Note FLUX_PBU_MAX_UNDO_BLOCKS legitimately bounds the number of undo blocks, but here it is misused to bound the scan across forward + undo blocks. (high confidence)
📄 src/backend/access/flux/flux_tuple.c (L1125-L1126)
Form/deform alignment mismatch: this varlena branch applies an extra trailing alignment using the current attribute's alignment (att_align_nominal(data_ptr + attr_len, att->attalign)). The FORM path (FluxFormTuple/FluxComputeDataSize) only aligns at the start of each attribute and advances by exactly attr_len -- and the sibling deform routines tts_flux_deform() and FluxDeformTupleUpTo() correctly do data_ptr += attr_len with no trailing re-align. When the current varlena's attalign is larger than the following attribute's (e.g. a double-aligned varlena followed by an int-aligned column), this over-advances data_ptr past the real next-attribute start, so every subsequent attribute is read from the wrong offset (garbage VARSIZE / corrupt reads). This is the primary sequential-scan retrieval path, so it silently corrupts scans for those alignment sequences. Advance by exactly attr_len to match the form path and the other deform routines.
💡 Suggested change
Before:
slot->tts_values[i] = PointerGetDatum(data_ptr);
data_ptr = (char *) att_align_nominal(data_ptr + attr_len, att->attalign);
After:
slot->tts_values[i] = PointerGetDatum(data_ptr);
data_ptr += attr_len;
📄 src/backend/access/flux/flux_tuple.c (L1135-L1139)
Inconsistent cstring (attlen == -2) handling: this retrieval path lacks the attlen == -2 branch that FluxComputeDataSize, FluxFormTuple, FluxDeformTupleUpTo and tts_flux_deform all implement, so a cstring-typed column falls into the else and errors with "unsupported attribute length" on scan while other paths deform it fine. Add the cstring case (align, CStringGetDatum, advance by strlen+1) so this primary scan path matches the sibling deform routines.
📄 src/backend/access/flux/flux_slot.c (L858-L864)
values_block leak on the same-buffer fast path. tts_flux_clear() frees both rslot->tuple and rslot->values_block when TTS_SHOULDFREE is set, but here the same-buffer branch only frees rslot->tuple. A slot left SHOULDFREE by tts_flux_materialize_values()/tts_flux_copyslot() owns a values_block with tuple == NULL; if this store hits the same-buffer path (buffer == rslot->buffer, e.g. both InvalidBuffer) that block is leaked and rslot->values_block is left dangling (never reset to NULL). Free values_block here too and clear the pointer, matching tts_flux_clear().
💡 Suggested change
Before:
/* Same buffer — just free any materialized data */
if (unlikely(TTS_SHOULDFREE(slot)))
{
if (rslot->tuple)
pfree(rslot->tuple);
slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
}
After:
/* Same buffer - just free any materialized data */
if (unlikely(TTS_SHOULDFREE(slot)))
{
if (rslot->tuple)
pfree(rslot->tuple);
if (rslot->values_block)
{
pfree(rslot->values_block);
rslot->values_block = NULL;
}
slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
}
📄 src/backend/access/flux/flux_stats.c (L50-L52)
Dead module (YAGNI). Neither FluxCollectRelationStats nor FluxLogRelationStats has any caller in the backend -- the entire flux_stats.c is not wired into ANALYZE despite the header comment claiming it is "designed to be called during ANALYZE" and "made available ... so that the planner can incorporate FLUX-specific cost adjustments." pgsql-hackers rejects speculative scaffolding that isn't reachable; either wire it into the ANALYZE/planner path in this patch or drop the module (and the FluxRelationStats struct + externs in flux.h). The commit-ts and compression stats it computes are also moot since FLUX never sets FLUX_TUPLE_COMPRESSED and t_commit_ts now packs xmax, not a commit timestamp.
📄 src/backend/access/flux/flux_vm.c (L72-L79)
Dead code (YAGNI). FluxVMInit has an empty body (only a comment), and FluxVMInit, FluxVMExtend, FluxVMTruncate, FluxVMPinBuffer, FluxVMGetPageSize and FluxVMMapHeapToVM have no callers anywhere in the tree (only their declarations in flux.h). The core VM ops (Set/Clear/Check/CheckCached/VacuumPage/UpdateFor*) are used; these extras are speculative surface area. Drop them (and their flux.h externs) to keep the patch minimal. Note also: FluxVMTruncate, if ever wired up, does not clear the tail bits of the last remaining VM page like heap's visibilitymap_prepare_truncate does -- stale ALL_VISIBLE/ALL_FROZEN bits would survive a truncate+re-extend -- so it is not merely unused but latently incorrect.
📄 src/backend/access/flux/flux_tuple.c (L859-L861)
Dead code (YAGNI). FluxPageUpdateTuple and FluxPageGetLiveTuples have no callers (FluxPageGetLiveTuples even self-documents "This helper is only an estimate (no current callers)"). Remove them and their flux.h externs to keep the patch minimal, per the community's preference against unused code.
📄 src/include/access/flux.h (L16-L18)
A header must not include postgres.h. Per its own docstring, postgres.h is the "Primary include file for PostgreSQL backend .c files ... Client-side code should include postgres_fe.h instead" and it unconditionally pulls in backend-only utils/elog.h/utils/palloc.h. Each .c file is responsible for including postgres.h/postgres_fe.h first; an installed access-method header doing so is a hard convention violation and breaks any frontend translation unit. No other in-tree access-method header does this. Remove this include and rely on the includer having pulled in the appropriate primary header.
Confidence: high.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/heapam.h"
After:
#include "access/heapam.h"
📄 src/include/access/flux.h (L204-L206)
FluxTupleWriter{TryLock,Unlock,IsLocked} cast the address of the plain uint32 t_writer field to pg_atomic_uint32 *. This violates PostgreSQL's atomics contract: pg_atomic_uint32 must be initialized with pg_atomic_init_u32() and only ever accessed through the pg_atomic_* API; on platforms using the spinlock-backed fallback, pg_atomic_uint32 is not layout-compatible with a bare uint32 and this cast is undefined. t_writer here is memset/palloc0'd and byte-copied through WAL, never pg_atomic_init_u32'd. Moreover, these three macros have zero call sites in src/backend/access/flux/*.c (the code manipulates t_writer as a plain field, e.g. tuple->t_writer = 0), so this is unused, non-portable scaffolding. Remove the macros, or if per-tuple CAS is actually needed, do not overlay atomics on an on-disk field.
Confidence: high.
📄 src/include/access/flux.h (L848-L848)
flux_lazy_uncommitted_clear is a GUC-backed global (defined in flux_operations.c, wired in guc_parameters.dat) referenced across modules. An exported extern variable used cross-module must be declared PGDLLIMPORT or it fails to link on Windows/MSVC. Compare TTSOpsFluxTuple above, which is correctly marked.
Confidence: high.
💡 Suggested change
Before:
extern bool flux_lazy_uncommitted_clear;
After:
extern PGDLLIMPORT bool flux_lazy_uncommitted_clear;
📄 src/include/access/flux.h (L843-L846)
FluxDirtyMapShmemSize, FluxDirtyMapShmemInit, and FluxDirtyMapShmemCallbacks are declared identically here and in flux_dirtymap.h. Duplicate cross-header declarations invite silent divergence if one signature changes. Declare them in one place (flux_dirtymap.h) and have flux.h include that header, or drop these three lines from flux.h.
Confidence: high.
📄 src/include/access/flux.h (L136-L136)
t_gen is documented as reserved/unused and is only ever written to 0 in the backend (flux_operations.c:3389, 5324) and never read. This is YAGNI reserved header space kept solely to avoid changing the tuple geometry, with an aspirational "a future slimming pass may remove it" comment. Per project convention, comments describe current behavior, not future intentions; and dead reserved fields on an on-disk struct should be removed now rather than carried. Drop the field (adjusting the 40-byte layout/StaticAssert) or, at minimum, remove the future-tense narration.
Confidence: high (usage), medium (whether to remove vs. keep for on-disk stability).
📄 src/include/access/flux.h (L342-L343)
FluxIsOverflowPtr / FluxGetOverflowPtr have no call sites anywhere in the tree (only their definitions exist). Beyond being dead code, the design is a robustness footgun: a user-supplied varlena (bytea/text) whose first bytes happen to equal FLUX_OVERFLOW_PTR_MAGIC and fall in the size range would be misidentified as an overflow pointer, causing ov_first_block to be dereferenced as a page reference during deform. If this identification path is intended to run against user column values, magic-only discrimination is insufficient; otherwise remove the unused helpers.
Confidence: high (unused); medium (severity of the magic-collision hazard depends on whether user data ever reaches this check).
📄 src/include/access/flux.h (L435-L441)
The flux_compression_algorithm GUC and its FluxCompressionAlgoGuc enum, plus FluxChooseCompressionType(), are referenced only in comments; neither the GUC nor the function exists in the tree. This is unwired speculative scaffolding. Remove the enum/comments until the compression-selection path is actually implemented and wired.
Confidence: high.
📄 src/include/access/flux.h (L596-L597)
There is no static assertion guarding the total header size or the byte offsets the on-disk/WAL format depends on. The struct doc claims a fixed 40-byte layout, and FluxTupleGetXmax/SetXmax plus WAL redo rely on t_commit_ts (offset 0) and t_xmin's positions being stable across ABIs. Only offsetof(t_attrs_bitmap) == FLUX_TUPLE_OVERHEAD is asserted. Add StaticAssertDecl(sizeof(FluxTupleHeader) == FLUX_TUPLE_OVERHEAD, ...) (and ideally offset asserts for t_commit_ts/t_xmin) so a future field/padding change cannot silently corrupt on-disk reads and cross-platform replication.
Confidence: medium (fields are naturally aligned so current layout is likely correct on supported ABIs; the assertion is defensive).
📄 src/include/access/flux_xlog.h (L16-L18)
This header includes postgres.h, yet it is consumed by the frontend translation unit src/bin/pg_waldump/rmgrdesc.c (which does #define FRONTEND 1) and it itself branches on #ifdef FRONTEND. postgres.h is backend-only (it pulls in utils/elog.h/utils/palloc.h); frontend code must use postgres_fe.h. A header pulling in postgres.h is a hard convention violation and is contradictory with the FRONTEND branch below. Remove the include; the frontend/backend includer supplies the correct primary header. (Note fluxdesc.c already sidesteps this header by hand-copying frontend-safe struct definitions, which is itself a DRY smell caused by this include.)
Confidence: high.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/xlogreader.h"
After:
#include "access/xlogreader.h"
📄 src/include/access/flux_xlog.h (L74-L75)
The entire compression-dictionary WAL path is unwired scaffolding: XLOG_FLUX_WRITE_DICT, xl_flux_write_dict, and FluxXLogWriteDict() have no implementation or caller anywhere in the tree, and the referenced FLUX_DICT_FORKNUM is not defined in any header. Declaring a WAL opcode and redo-relevant record with no emitter/redo handler is dead code that also risks fork-number confusion if later half-wired. Remove until the dict fork is actually implemented.
Confidence: high.
📄 src/include/access/flux_undo.h (L43-L45)
FLUX_UNDO_INSERT is documented as "no longer written" and kept only to revert INSERT records produced by older servers, and flux_undo.c still carries a live apply_flux_undo_insert path for it. For a brand-new access method with no released prior on-disk format, there are no "older servers" to be compatible with, so this is dead compatibility scaffolding. Remove the subtype and its apply path (or justify the compatibility target concretely).
Confidence: high.
📄 src/include/access/flux_undo.h (L188-L189)
FluxUndoDeltaCompute truncates uint32 prefix/suffix lengths to uint16 (d->prefixlen = (uint16) prefixlen). Current callers pass old_len = tuple t_len bounded by FLUX_MAX_TUPLE_SIZE (< BLCKSZ, well under 65536) and force_offset = offsetof(t_verptr) = 8, so the truncation is safe today, but nothing enforces it. Additionally, when force_offset < old_len, suffixlen = old_len - 1 - force_offset computed on unsigned uint32 will wrap if force_offset == old_len - 1 combined with an existing larger suffix, and a malformed on-disk delta reaching FluxUndoDeltaReconstruct during redo could drive an out-of-bounds memcpy. Add an assertion that old_len fits uint16 (and prefixlen+suffixlen <= old_len) in Compute, and keep the bounds checks in Reconstruct strict against cur_len. Defensive hardening on the recovery path.
Confidence: medium.
📄 src/backend/access/recno/recno_operations.c (L3086-L3090)
Dangling braceless if bug. The if (update_indexes) has no braces, so it guards the immediately-following pfree(new_tuple);. When update_indexes is NULL (a legitimate caller passing no output pointer), new_tuple is never freed — a per-UPDATE memory leak. When update_indexes is non-NULL, the branch is a no-op guarding an unconditional cleanup that must always run. The pfree must be unconditional; the comment block above it also describes returning TU_Summarizing which no longer matches the code (index strategy is set earlier), so the stale comment should go too.
This reads as an editing accident (the if lost its body). Fix:
💡 Suggested change
Before:
if (update_indexes)
pfree(new_tuple);
return TM_Ok;
}
After:
pfree(new_tuple);
return TM_Ok;
}
📄 src/backend/access/recno/recno_clock.c (L368-L368)
Portability defect: %lu is used to print uint64 error_ms. On LLP64 (Windows/MSVC) and 32-bit platforms unsigned long is 32-bit, so %lu does not match a uint64 argument — this prints garbage and is a varargs ABI mismatch. Use UINT64_FORMAT. Both occurrences (80%% and 50%% messages) are affected.
💡 Suggested change
Before:
(errmsg("recno clock error bound %lu ms exceeds 80%% of maximum %d ms",
After:
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 80%% of maximum %d ms",
📄 src/backend/access/recno/recno_clock.c (L378-L378)
Same %lu-for-uint64 portability defect here (the 50%% WARNING). Use UINT64_FORMAT.
💡 Suggested change
Before:
(errmsg("recno clock error bound %lu ms exceeds 50%% of maximum %d ms",
After:
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 50%% of maximum %d ms",
📄 src/backend/access/recno/recno_clock.c (L358-L358)
Float arithmetic on a safety threshold that triggers ereport(FATAL) (taking the whole instance down) and sets shutdown_pending. recno_max_clock_offset_ms is a signed int GUC; * 0.8 promotes it to double and compares against a uint64 error_ms, which is fragile (rounding, signed/unsigned/double mixing) for a path that kills the server. Use integer arithmetic, e.g. error_ms > (uint64) recno_max_clock_offset_ms * 8 / 10. More fundamentally, a self-inflicted FATAL as the default (recno_fatal_on_clock_drift defaults true) is a footgun: a transient clock-bound read hiccup can crash a healthy primary. Reconsider defaulting this to on.
💡 Suggested change
Before:
if (error_ms > recno_max_clock_offset_ms * 0.8)
After:
if (error_ms > (uint64) recno_max_clock_offset_ms * 8 / 10)
📄 src/backend/access/recno/recno_clock.c (L373-L373)
Same float-threshold fragility on the WARNING path: recno_max_clock_offset_ms * 0.5 mixes a signed int GUC with a double literal and compares to a uint64. Use integer arithmetic ((uint64) recno_max_clock_offset_ms / 2).
💡 Suggested change
Before:
else if (error_ms > recno_max_clock_offset_ms * 0.5)
After:
else if (error_ms > (uint64) recno_max_clock_offset_ms / 2)
📄 src/backend/access/recno/recno_clock.c (L255-L256)
Torn-read hazard. This is a plain multi-word memcpy of an externally-written daemon segment (struct timespec earliest/latest, uint64 error_bound_ns, ...), yet the comment claims an "atomic read". There is no seqlock/generation/version protocol, so a concurrent daemon write can produce a torn earliest/latest/error_bound_ns, which then feeds the MVCC bounds and the fatal-drift shutdown logic. The sanity checks below (latest >= earliest, nonzero) do not detect all torn combinations. A seqlock-style read (read version, memcpy, re-read version, retry on mismatch) is required, and the ClockBoundData struct also hard-codes an external ABI (timespec/uint64/uint32 layout) that is not guaranteed portable.
📄 src/backend/access/recno/recno_clock.c (L205-L206)
The bgworker installs PG_SIG_DFL for SIGTERM. This violates the background-worker contract: a database-connected dynamic bgworker should install die (or SignalHandlerForShutdownRequest) so a fast-shutdown/terminate SIGTERM causes a clean exit at the next CHECK_FOR_INTERRUPTS/latch wait. With PG_SIG_DFL the process is killed by the default disposition, and the main loop only reacts to WL_EXIT_ON_PM_DEATH (never to a shutdown request), so the worker cannot be shut down cleanly or rotated. Use pqsignal(SIGTERM, die); and let the wait loop honor the interrupt.
💡 Suggested change
Before:
pqsignal(SIGTERM, PG_SIG_DFL);
BackgroundWorkerUnblockSignals();
After:
pqsignal(SIGTERM, die);
BackgroundWorkerUnblockSignals();
📄 src/backend/access/recno/recno_fsm.c (L292-L295)
Per-DML memory leak on a hot path. RecnoMarkPageForDefrag calls RecnoGetFSMState, which pallocs a RecnoFSMState plus a dedicated AllocSetContext ("RECNO FSM Context") and a defrag_queue inside it — but this function never frees either. RecnoMarkPageForDefrag is reached from RecnoRecordFreeSpace, which is called on every insert/update/delete free-space update, so each such call leaks a MemoryContext + palloc for the lifetime of the surrounding context. (Only RecnoOpportunisticDefrag frees its fsm_state.) Either cache the FSM state per-relation or free it here before returning.
📄 src/backend/access/recno/recno_fsm.c (L324-L327)
Reentrancy / stale-queue design smell compounding the leak above: RecnoGetFSMState allocates a fresh, empty defrag queue on every call, so the "queue full -> process one page" branch in RecnoScheduleDefrag can never trigger from RecnoMarkPageForDefrag (the queue always has exactly one entry and is then discarded). The scheduled page is effectively dropped. This whole transient-state machinery does nothing useful on the RecnoRecordFreeSpace path; consider removing it or making the state persistent per-relation.
📄 src/backend/access/recno/recno_compress.c (L957-L962)
Data-loss hazard: dictionary compression stores only a 4-byte process-local dict_id on disk (*((int *) output) = dict_id;), and the dictionary itself lives in per-backend CacheMemoryContext, is never persisted, and is keyed by insertion order. Any tuple written with RECNO_COMP_DICTIONARY becomes undecodable in another backend or after a restart — RecnoDecompressDictionary will read a bogus/OOB entry and either error or return corrupt data. RecnoChooseCompressionType never selects DICTIONARY (only ZSTD/DELTA), so the codec is only reachable if a caller explicitly passes it; given the corruption risk it should not be a persistable codec at all. Remove the dictionary codec from the on-disk path or gate it so it can never reach storage.
📄 src/backend/access/recno/recno_compress.c (L1214-L1214)
Dead speculative code with an unsafe 64KB on-stack buffer. RecnoCompressLZ4Dict/RecnoDecompressLZ4Dict (and the ZSTD dict variants) are marked pg_attribute_unused() and have no callers anywhere in the tree. Beyond being dead code that shouldn't ship (YAGNI), char dict_buf[65536] is a 64KB stack allocation, which is well outside PostgreSQL norms and risks stack overflow if ever wired up. Remove these dictionary-accelerated variants entirely.
📄 src/backend/access/recno/recno_compress.c (L1280-L1283)
Same dead-code + 64KB-stack-buffer issue in the LZ4 dictionary decompress variant. Remove.
📄 src/backend/access/recno/recno_compress.c (L1329-L1332)
Fabricated statistics. RecnoGetCompressionStats returns hardcoded placeholders (*total_compressed = 0; *avg_compression_ratio = 0.7; /* Estimated */) with no real tracking behind them. Reporting invented numbers to any monitoring caller is misleading; either track the real values or drop the function until it can.
📄 src/backend/access/recno/recno_mvcc.c (L1350-L1352)
Unlocked, un-WAL'd mutation of an on-page tuple header. In the else branch (!BufferIsValid(buffer)) this does tuple->t_flags &= ~RECNO_TUPLE_UNCOMMITTED directly. If the caller ever reaches this with a pointer into a shared buffer that is only pinned (not content-locked), this is a data race and a hint-bit write with no MarkBufferDirty/WAL — it can be torn or silently lost, and can race another backend reading the same buffer. This is only safe for genuinely private/materialized tuples. Please assert the tuple is not buffer-resident here (or route all buffer cases through BufferSetHintBits16 under the content lock). The same pattern appears in RecnoTupleVisibleHLC's hlc_clear_uncommitted and in RecnoTupleVisibleWithUncertainty.
📄 src/backend/access/recno/recno_handler.c (L1679-L1683)
Dead abort-recovery branch built on a removed field. hint_xid is initialized to InvalidTransactionId and never reassigned, so TransactionIdIsValid(hint_xid) is always false and this entire CLOG-fallback block (detecting an aborted committed-delete via the deleter's XID) is unreachable. The comment still refers to "t_xid_hint (deleter's XID)", a field that no longer exists. This silently disables the intended aborted-delete recovery: when the UNDO worker has removed the sLog entries (slog_nfound == 0), the code falls straight through to treating the deletion as committed. Either wire in the real deleter XID (from t_xmin / the tuple) or remove the dead branch and fix the stale comment.
📄 src/backend/access/recno/recno_handler.c (L1756-L1759)
Second occurrence of the same dead branch, in the RECNO_TUPLE_UPDATED (out-of-place update) handling: hint_xid is hardcoded to Invalid, so every TransactionIdIsValid(hint_xid) arm (current-txn, aborted-updater, in-progress-updater) is unreachable and the code always falls to "Updater committed -- old version is dead". The comment again cites the removed t_xid_hint field. This means an aborted out-of-place update whose sLog entries were discarded is wrongly treated as committed, hiding the (still-live) old version. Fix the XID source or delete the dead code and stale comment.
📄 src/backend/access/recno/recno_fsm.c (L324-L325)
RecnoGetFSMState-style stale/aspirational comment: "In practice, this would be cached per relation" describes work that isn't done. Per the comment-accuracy rule, WIP/aspirational comments should not ship; either implement the caching or state the current behavior factually.
📄 src/backend/access/recno/recno_compress.c (L72-L74)
Aspirational TODO comment should not ship per pgsql-hackers comment-accuracy standards (no future-tense scaffolding notes). Track this in the -hackers thread / commitfest entry instead of the source.
📄 src/backend/access/recno/recno_clock.c (L619-L623)
Empty GUC assign hooks whose only content is an aspirational comment ("Will take effect on next server restart"). If the hook truly does nothing, it does not need to exist — attach the GUC without an assign hook. Shipping empty stub hooks with future-tense comments is exactly the WIP scaffolding pgsql-hackers rejects.
📄 src/backend/access/recno/recno_operations.c (L299-L305)
RecnoPagePruneOpt is declared int and its header comment states "Returns the number of tuples pruned." But the success path returns true (i.e. 1), not the actual ndead count, while early exits return 0. Both callers (here at the update path, and recno_handler.c) use it purely as a boolean. Either change the return type/comment to bool (matching actual use), or return the real ndead count as documented. As written the contract is self-contradictory and misleading.
💡 Suggested change
Before:
return true;
}
/*
* Insert a tuple into a RECNO table
*/
After:
return ndead;
}
/*
* Insert a tuple into a RECNO table
*/
📄 src/backend/access/recno/recno_operations.c (L3086-L3090)
Dangling if (update_indexes) with no braces directly guards pfree(new_tuple). This means new_tuple is freed ONLY when update_indexes != NULL; when the caller passes update_indexes == NULL, new_tuple leaks per UPDATE on a hot path. The preceding comment block clearly intends the pfree to be unconditional (it documents index-update strategy, not a pfree guard). Remove the stray if.
💡 Suggested change
Before:
if (update_indexes)
pfree(new_tuple);
return TM_Ok;
}
After:
pfree(new_tuple);
return TM_Ok;
}
📄 src/backend/access/recno/recno_clock.c (L367-L369)
error_ms is uint64, but %lu is unsigned long, which is 32-bit on LLP64 (Windows/MSVC) and 32-bit platforms. This prints garbage / triggers a format warning there. Use UINT64_FORMAT. Both this FATAL site and the WARNING site below have the same defect.
💡 Suggested change
Before:
ereport(FATAL,
(errmsg("recno clock error bound %lu ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
After:
ereport(FATAL,
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
📄 src/backend/access/recno/recno_clock.c (L377-L379)
Same %lu-for-uint64 portability defect as the FATAL path above: error_ms is uint64. Use UINT64_FORMAT.
💡 Suggested change
Before:
ereport(WARNING,
(errmsg("recno clock error bound %lu ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
After:
ereport(WARNING,
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
📄 src/backend/access/recno/recno_clock.c (L358-L358)
Using floating-point math (* 0.8, * 0.5) for a safety threshold that can ereport(FATAL) and take the whole instance down is fragile: it mixes a signed int GUC with double literals and depends on rounding. Prefer integer arithmetic, e.g. error_ms * 5 > (uint64) recno_max_clock_offset_ms * 4 for the 80% gate and error_ms * 2 > (uint64) recno_max_clock_offset_ms for the 50% gate. A spurious FATAL here kills every backend; the exactness matters.
💡 Suggested change
Before:
if (error_ms > recno_max_clock_offset_ms * 0.8)
After:
if (error_ms * 5 > (uint64) recno_max_clock_offset_ms * 4)
📄 src/backend/access/recno/recno_clock.c (L255-L256)
This is a plain multi-word memcpy of an externally-written mmap segment; the "atomic read" claim is false. earliest/latest/error_bound_ns can be torn if the clock-bound daemon updates the region concurrently, feeding corrupt bounds into MVCC and the fatal-drift shutdown path. A cross-process shared region written by an external daemon needs a seqlock/generation protocol (read version, memcpy, re-read version, retry on change). The ClockBoundData layout (struct timespec + uint64 + uint32) is also assumed to match the daemon's ABI byte-for-byte across platforms with no version field.
📄 src/backend/access/recno/recno_clock.c (L205-L206)
This installs SIGTERM as PG_SIG_DFL, which means a fast-shutdown/terminate SIGTERM kills this bgworker with the default disposition rather than a clean die() exit. Standard database-connected bgworkers install die (or SignalHandlerForShutdownRequest) for SIGTERM. Combined with the main loop below only reacting to WL_EXIT_ON_PM_DEATH (it never checks a shutdown flag or calls CHECK_FOR_INTERRUPTS), the worker cannot respond to a normal shutdown/terminate request and can be killed uncleanly. Use pqsignal(SIGTERM, die) and add CHECK_FOR_INTERRUPTS() in the wait loop.
📄 src/backend/access/recno/recno_clock.c (L234-L236)
The wait loop only reacts to WL_EXIT_ON_PM_DEATH; there is no handling of a SIGTERM shutdown request (no die handler installed above, no CHECK_FOR_INTERRUPTS()). A normal server shutdown/terminate cannot cleanly stop this worker. Add CHECK_FOR_INTERRUPTS() after ResetLatch and install a proper SIGTERM handler.
📄 src/backend/access/recno/recno_clock.c (L255-L256)
This is a plain multi-word memcpy of an mmap'd region written by an external daemon, labelled an "atomic read" but with no seqlock/generation/version protocol. A torn read of earliest/latest/error_bound_ns is possible (the daemon can update between the words being copied), feeding corrupt time bounds into MVCC and into the fatal-drift shutdown path. Additionally the ClockBoundData layout (struct timespec + uint64 + uint32) is assumed to byte-match the daemon's ABI across platforms/compilers, which is fragile. Add a seqlock/version handshake (read version, memcpy, re-read version, retry on change) before trusting the data.
📄 src/backend/access/recno/recno_mvcc.c (L485-L489)
on_shmem_exit(RecnoShmemExit, 0) is registered in BOTH the legacy RecnoMvccShmemInit and the subsystem-callback RecnoMvccShmemInit_cb. If both init paths run during startup (legacy ShmemInitStruct fallback plus the PG_SHMEM_SUBSYSTEM callback), the exit callback is registered twice and the shared struct is re-initialized, wiping any state set between the two calls. Confirm exactly one path executes per backend; if both can run, guard the registration with a static flag or remove the duplicate init body (the two bodies are byte-for-byte duplicated, a DRY hazard on their own).
📄 src/backend/access/recno/recno_handler.c (L1677-L1683)
Dead branch: hint_xid is initialized to InvalidTransactionId and never reassigned, so TransactionIdIsValid(hint_xid) is always false and this entire CLOG-fallback abort-recovery block is unreachable. The comment claims it uses "t_xid_hint (deleter's XID)", but that field was removed. Either wire in the real deleter XID (e.g. from the sLog entry) or delete the dead block and its misleading comment.
📄 src/backend/access/recno/recno_handler.c (L1749-L1756)
Dead branch: hint_xid is initialized to InvalidTransactionId and never set, so every TransactionIdIsValid(hint_xid) test below is false and this whole CLOG-fallback recovery path for aborted out-of-place updates is unreachable. The comment references the removed t_xid_hint field. Remove the dead code (and the stale comment) or supply the real updater XID.
📄 src/include/access/flux_xlog.h (L399-L408)
This entire #else /* FRONTEND */ block of "frontend-safe" WAL record structs is dead code. The only frontend consumer of flux_xlog.h is src/bin/pg_waldump/rmgrdesc.c, and the actual desc implementation (fluxdesc.c) defines its own local _fe mirror structs (xl_flux_insert_fe, etc.) and never references these. A code_search across src/bin/ finds zero uses of any of these frontend struct names. Maintaining two parallel copies of the on-WAL layout (backend + this frontend branch) that no one reads is a silent-divergence footgun and pure scaffolding. Remove the FRONTEND struct block, or have fluxdesc.c consume these instead of re-declaring its own mirrors. (moderate confidence)
📄 src/include/access/flux_undo.h (L208-L214)
FluxUndoDeltaReconstruct dereferences the caller-supplied delta pointer (reading delta->prefixlen, delta->suffixlen, delta->old_len on lines 208-210) BEFORE it validates that the buffer is at least SizeOfFluxUndoDelta bytes (the guard on line 213). If a malformed/truncated undo record hands in a delta_len < SizeOfFluxUndoDelta, these reads run off the end of the underlying buffer. Since this reconstructs from WAL/undo bytes during redo, a corrupt record is exactly the case the guard exists for. Move the if (delta_len < SizeOfFluxUndoDelta) return false; check ahead of the field reads. (moderate confidence)
💡 Suggested change
Before:
uint32 prefixlen = delta->prefixlen;
uint32 suffixlen = delta->suffixlen;
uint32 old_len = delta->old_len;
uint32 middle_len;
if (delta_len < SizeOfFluxUndoDelta)
return false;
After:
uint32 prefixlen;
uint32 suffixlen;
uint32 old_len;
uint32 middle_len;
if (delta_len < SizeOfFluxUndoDelta)
return false;
prefixlen = delta->prefixlen;
suffixlen = delta->suffixlen;
old_len = delta->old_len;
📄 src/backend/access/recno/recno_overflow.c (L951-L952)
Overflow-record page mutations here are not WAL-logged and not wrapped in a critical section, yet the buffer is dirtied. RecnoPageIndexTupleDelete() moves tuple data and rewrites pd_lower/pd_upper on the page, then MarkBufferDirty() persists that change with no corresponding WAL record. On a standby (or after a primary crash) the change is never replayed, so the deleted overflow records reappear while the parent tuple's reference may already be gone -- producing primary/standby divergence and dangling overflow chains. The comment's claim that this is "similar to PostgreSQL's TOAST cleanup strategy" is incorrect: TOAST deletions are heap deletes on the toast relation and ARE fully WAL-logged. This mutation must be inside START_CRIT_SECTION/END_CRIT_SECTION and emit a WAL record with a matching redo path (e.g. an overflow-delete opcode), same as every other on-disk mutation in recno_xlog.c. The same defect applies to the identical delete in RecnoVacuumOverflowRecords pass 2, where RecnoXLogInitPage logs a whole-page re-init that does not describe the actual per-item deletions and races concurrent readers.
📄 src/backend/access/recno/recno_overflow.c (L383-L384)
This target_block == 0 branch is a fragile heuristic that assumes block 0 is always the caller's main-tuple page. That is not true: in a RECNO relation block 0 is an ordinary data page tracked by the FSM and can legitimately hold overflow records that belong to other tuples. When the FSM returns block 0 for the first overflow record, this code unconditionally rejects it and extends the relation, wasting a page and an extra I/O on every such insert, and defeating the page-reuse strategy the file otherwise implements. The correct disambiguation is to compare target_block against the caller's actual main-page block number (pass it in), not to hard-code 0. The comment already flags this as a "WORKAROUND"; it should be replaced with a correct check before this ships.
📄 src/backend/access/recno/recno_overflow.c (L1173-L1181)
This full-relation scan has no CHECK_FOR_INTERRUPTS in its outer loop, so RecnoGetOverflowStats is uninterruptible on a large table (a user cannot cancel it). The two VACUUM passes in RecnoVacuumOverflowRecords already call CHECK_FOR_INTERRUPTS per block; do the same here.
📄 src/backend/access/recno/recno_overflow.c (L1552-L1554)
The entire "Legacy interface" block (RecnoStoreOverflow / RecnoFetchOverflow / RecnoDeleteOverflow) is dead code: this is a brand-new file, there is no prior on-disk format to transition from, and grep finds no callers outside their own declarations. RecnoFetchOverflow unconditionally throws ERRCODE_FEATURE_NOT_SUPPORTED, and RecnoDeleteOverflow guesses FirstOffsetNumber for the chain start (which would delete wrong records or nothing if it were ever reached). Per YAGNI, remove all three functions and their declarations in recno.h rather than shipping speculative scaffolding.
📄 src/backend/access/recno/recno_xlog.c (L157-L165)
This spins with pg_usleep() and is reachable during WAL replay: recno_redo_handle_hlc() calls RecnoReplicaHandleUncertainty() from the INSERT/UPDATE/DELETE redo handlers, and recno_uncertainty_wait defaults to true (recno_hlc.c). When recno_use_hlc is on, the startup process therefore sleeps in a wall-clock-driven loop for every replayed DML record, arbitrarily delaying recovery and lagging replay behind the primary. The only interrupt check is CHECK_FOR_INTERRUPTS, which does not honor recovery-conflict/startup handling the way a proper wait (e.g. a latch wait via WaitLatch with a timeout, or deferring the wait out of the redo path entirely) would. Blocking the redo loop on physical clock is not acceptable; the uncertainty wait must not run inside recno_redo.
📄 src/backend/access/recno/recno_xlog.c (L330-L332)
Boundary check is off by one and does not protect against exceeding XLR_MAX_BLOCK_ID. overflow_buffers->count is capped at MAX_OVERFLOW_BUFFERS (32) by RecnoStoreOverflowColumn, and XLR_MAX_BLOCK_ID is also 32 (valid block ids are 0..31). With count == 32 overflow records on separate pages, the main tuple takes block id 0 and the overflow buffers take block ids 1..32; the id 32 registration is out of range. The guard count > XLR_MAX_BLOCK_ID (32 > 32 is false) never fires for this case, so XLogRegisterBuffer(32, ...) is reached. The check must account for block 0 (and any undo blocks), e.g. reject when count + reserved_block_ids >= XLR_MAX_BLOCK_ID. The identical guard in RecnoXLogUpdate has the same flaw.
📄 src/backend/access/recno/recno_slog.c (L1609-L1619)
This PG_CATCH swallows the error (EmitErrorReport + FlushErrorState) but does not release the resources acquired inside the PG_TRY block: ReadBuffer() pins and LockBuffer() exclusively locks buf, and try_relation_open() holds a relcache reference plus AccessShareLock. If the error is thrown after ReadBuffer/LockBuffer but before the UnlockReleaseBuffer/relation_close at the end of the TRY block, those are leaked past commit -- a buffer pin+lock and a relation reference held for the rest of the session, because FlushErrorState continues the commit rather than aborting (which would normally trigger ResourceOwner cleanup). Either move relation_close/UnlockReleaseBuffer into the CATCH path, or let the error propagate instead of swallowing it. Swallowing errors mid-commit to keep a hot path quiet is a footgun.
📄 src/backend/access/recno/recno_slog.c (L980-L981)
Acquiring ALL RECNO_SLOG_PARTITIONS partition LWLocks at once, then running hash_seq_search under them, is a lock-ordering hazard: any other path that takes a single partition lock (RecnoSLogLockPartition, used throughout this file) and then blocks can deadlock against this global acquire, and it fully serializes the sLog. It also palloc's an array sized RecnoSLogNumEntries() -- up to RECNO_SLOG_MAX_ENTRIES (262144) * sizeof(RecnoSLogKey) -- while holding every partition lock. Separately, RecnoSLogRemoveByXidGlobal has no callers anywhere in the tree, so this is currently dead code; per YAGNI it should be removed rather than shipped as a latent deadlock. If it is intended to be wired up, the collection phase should lock one partition at a time and the buffer should be sized/allocated before taking any lock.
📄 src/backend/access/recno/recno_slog.c (L529-L529)
Busy-waiting with pg_usleep(1000) in a retry loop to wait for concurrent transactions' cleanup callbacks to fire is a footgun: it blocks the backend for up to 3ms per contended TID with no latch/interrupt-aware wait, and it can mask a genuine RECNO_SLOG_MAX_OPS exhaustion (a real "too many concurrent operations" condition) as a transient delay. Combined with the one-sLog-entry-per-live-row model documented in this file's header, a normal bulk INSERT can drive the shared hash table to "out of shared memory" whose only remedy is a restart-required GUC (recno_slog_entries_per_backend). This is a scalability cliff that will surprise operators; at minimum it needs documentation and ideally a spill path rather than a hard ERROR.
📄 src/backend/access/recno/recno_stats.c (L246-L252)
int64 values are formatted with %lld and (long long) casts here and in RecnoGetOverflowStats/RecnoVacuumOverflowRecords (recno_overflow.c). The rest of the RECNO subsystem (recno_hlc.c, recno_mvcc.c, recno_pbu.c) uses the tree-standard INT64_FORMAT/UINT64_FORMAT for 64-bit values. Use INT64_FORMAT here for consistency and to avoid relying on long long being 64-bit.
📄 src/backend/access/recno/recno_xlog.c (L1443-L1446)
This comment is duplicated verbatim ("Use the specific offset from WAL record. / Overflow pointers reference these offsets." appears twice back-to-back), and the surrounding indentation is broken. This is a botched edit -- collapse it to a single, correctly-indented comment.
📄 src/backend/access/recno/recno_slot.c (L469-L472)
When no physical tuple is attached, getsysattr does try_relation_open() + ReadBuffer() + LockBuffer(SHARE) + RelationGetNumberOfBlocks() to fetch xmin/xmax off the page. slot_getsysattr can be invoked from executor paths, and a relation open plus buffer read + share-lock per system-attribute fetch is a heavy, lock-acquiring operation to bury inside a slot op -- it also opens a relation in a context (mid-executor) where callers may already hold buffer locks, risking lock-ordering surprises. If this path is only exercised by ExecOnConflictLockRow (as the comment states), consider having that caller pass the needed xmin instead of re-opening the relation from the slot layer; at minimum document that this must never run in a tight loop.
📄 src/include/access/recno.h (L24-L26)
A public header must not #include "postgres.h". Every existing src/include/access/*.h (genam.h, heapam.h, tableam.h, nbtree.h, ...) omits it; the .c file is required to include postgres.h first itself. Including it from a header breaks frontend/extension consumers and violates the header-cleanliness gate. The only other headers that do this are the sibling FLUX/recno files added in this same (uncommitted) change set, so they are not a valid precedent. Drop this include (same applies to recno_diff.h, recno_slog.h, recno_xlog.h).
💡 Suggested change
Before:
#include "postgres.h"
#include "access/heapam.h"
After:
#include "access/heapam.h"
📄 src/include/access/recno.h (L132-L134)
Stale on-disk header fields. The RECNO sources' own NOTES state the tuple header "no longer carries t_xmin, t_xmax, or t_xact_ts" (recno_mvcc.c) and that t_cid/t_xid_hint were removed to "save 4 bytes per tuple" (recno_slot.c, recno_mvcc.c). Verified against the .c code: t_xmax and t_xact_ts are never written or read anywhere, and t_xid_hint is read but never assigned (no ->t_xid_hint = exists). These are dead bytes in a persistent per-tuple header, permanently inflating every stored row. Either the header is wrong or the code comments are; reconcile and drop the fields that are truly unused from the on-disk layout.
📄 src/include/access/recno.h (L308-L308)
This extern does not match any definition. The variable is defined as recno_clock_check_interval_ms (recno_clock.c:99); recno_clock_check_interval (no _ms) exists only in this declaration. Any consumer that references the header name gets an unresolved symbol at link time. Rename to recno_clock_check_interval_ms to match the definition (and its GUC registration/assign hook).
💡 Suggested change
Before:
extern PGDLLIMPORT int recno_clock_check_interval;
After:
extern PGDLLIMPORT int recno_clock_check_interval_ms;
📄 src/include/access/recno.h (L324-L324)
RelationHasUndo(rel) is defined as constant (true) yet is used in real branch conditions in recno_operations.c (if (RelationHasUndo(relation))). A macro that ignores its argument and always returns true turns those into dead conditionals (a footgun / POLA violation: it silently discards any per-relation intent the argument implies). If undo is genuinely unconditional, drop the macro and the if guards rather than keeping a knob that looks live but isn't.
📄 src/include/access/recno.h (L102-L102)
100%% is an errant printf-style escape in a plain C comment; it reads as a literal double percent. This looks like text copied from an errmsg/printf format string. Use a single %.
💡 Suggested change
Before:
* A page packed to 100%% therefore makes every row on it permanently
After:
* A page packed to 100% therefore makes every row on it permanently
📄 src/include/access/recno.h (L294-L296)
Empty comment section: the heading is present but there is no content beneath it (the WAL id is stated to be positional in rmgrlist.h, so nothing is defined here). Remove this stale/empty block.
📄 src/include/access/recno_undo.h (L27-L29)
The rmid enumeration in this comment is inaccurate and will mislead the next consumer picking an ID. In this change set the assignments are 0=INVALID, 1=NBTREE, 2=FILEOPS (fileops.h), 3=HASH, 5=FLUX, 7=RECNO. There is no ZHEAP registered anywhere (6 = ZHEAP is speculative), and FILEOPS=2 is omitted while the comment claims "2 ... reserved out-of-core". Correct the list to reflect the IDs actually reserved in the tree.
📄 src/include/access/recno_xlog.h (L149-L149)
Inconsistent with the sibling size macros (SizeOfXlRecnoHlcInfo, SizeOfRecnoUndoPayloadHeader) which wrap sizeof in parens. Although sizeof x + y parses correctly here because sizeof binds tighter than +, an unparenthesized function-like macro used in expression context is a maintenance hazard. Wrap in parentheses.
💡 Suggested change
Before:
#define SizeOfRecnoUndo sizeof(xl_recno_undo)
After:
#define SizeOfRecnoUndo (sizeof(xl_recno_undo))
📄 src/include/access/recno.h (L345-L346)
Potential unaligned access on strict-alignment platforms (portability gate: ARM64/PPC64/s390x). VARDATA_ANY(ptr) on a short-header (1-byte) varlena returns a pointer that is not 4-byte aligned, and RecnoOverflowPtr begins with uint32 ov_magic (and contains further uint32/BlockNumber fields). A RecnoOverflowPtr datum is ~24 bytes total, which is within the range PostgreSQL stores with a 1-byte varlena header, so ovp->ov_magic here is an unaligned 4-byte load -> SIGBUS or trap-and-fixup on strict-alignment CPUs. These functions are used on the hot path (recno_overflow.c, recno_slot.c, recno_tuple.c). Read the magic via memcpy into a uint32, or guarantee overflow-pointer datums always use a 4-byte-aligned (long) varlena header. Same issue applies to callers dereferencing the struct returned by RecnoGetOverflowPtr.
📄 src/backend/access/undo/undo_xlog.c (L347-L351)
Integer-overflow bounds check can be bypassed, leading to a heap buffer overflow during CLR redo (high confidence).
prefix_len/suffix_len are uint16 and changed_len/old_tuple_len are uint32 (see undo_xlog.h). In prefix_len + changed_len + suffix_len the sum is computed in uint32 and can wrap modulo 2^32 before the (Size) cast is applied. A crafted/corrupt CLR with e.g. changed_len = 0xFFFFFFFE, prefix_len = 1, suffix_len = 1, old_tuple_len = 0 makes the wrapped sum equal old_tuple_len and passes the check. Then restored = palloc(old_tuple_len) (tiny) followed by memcpy(restored + prefix_len, changed_data, changed_len) (below) overflows the buffer. datalen < hdr_size + changed_len can likewise overflow a 32-bit Size.
Compute the sum in uint64/Size before comparing, and validate changed_len <= datalen - hdr_size without adding, e.g.:
if (prefix_len > cur_len ||
suffix_len > cur_len ||
(Size) prefix_len + (Size) suffix_len > cur_len ||
(Size) prefix_len + (Size) changed_len + (Size) suffix_len != (Size) old_tuple_len ||
changed_len > datalen - hdr_size)
📄 src/backend/access/undo/atm.c (L386-L392)
AtmStateRecord is serialized to a durable on-disk file with raw write()/read() of sizeof(struct), which is not a portable/stable on-disk format (high confidence). The struct has interior/trailing padding (bool + Oid/XLogRecPtr alignment); field layout, padding, bool size, and endianness differ across compilers/platforms, so this format is fragile and non-canonical. Worse, rec is stack-allocated and only its 5 named fields are set, so the padding bytes fed to both COMP_CRC32C() and write() are uninitialized -- this writes indeterminate stack memory to disk (a minor infoleak and a non-deterministic CRC). At minimum memset(&rec, 0, sizeof(rec)) before populating; better, serialize each field explicitly into a byte buffer with fixed widths rather than dumping the struct.
📄 src/backend/access/undo/undo_xlog.c (L1006-L1008)
dboid is captured as MyDatabaseId, but PerformUndoRecovery() runs in the startup/recovery process (called from xlogrecovery.c StartupXLOG), which is not attached to any user database, so MyDatabaseId is InvalidOid here (medium-high confidence). The deferred entry is later handed to the ATM, and the logical revert launcher spawns a per-database worker via ATMCollectUnrevertedDatabases() + StartLogicalRevertWorker() -> BackgroundWorkerInitializeConnectionByOid(dboid). With dboid == InvalidOid the worker never matches (logical_revert_worker.c: if (entry_dboid != MyDatabaseId) continue;), so the deferred rollback is silently never applied and the affected tuples stay UNCOMMITTED indefinitely. The correct dboid must be derived from the transaction's UNDO records (e.g. the batch/relation), not from MyDatabaseId.
📄 src/backend/access/undo/undo_xlog.c (L100-L102)
Comment states "with MAX_UNDO_LOGS=64" but the compile-time value is 100 (src/include/access/undolog.h:60), which the README also states. The bound checks use MAX_UNDO_LOGS so there is no overrun, but the comment is wrong. Fix the comment to 100 (or drop the specific number).
📄 src/backend/access/undo/atm.c (L372-L372)
The ATM state file borrows WAIT_EVENT_TWOPHASE_FILE_WRITE/SYNC/READ, which are named for two-phase (2PC) state I/O. This mislabels pg_stat_activity wait diagnostics -- an operator watching ATM checkpoint I/O sees a "TwophaseFile*" wait. New wait events must be declared in src/backend/utils/activity/wait_event_names.txt (which generates the header); add dedicated ATM events rather than reusing the 2PC ones. (Same applies to the WRITE and SYNC sites below.)
📄 src/backend/access/undo/undo_xlog.c (L475-L485)
For a net-new subsystem there is no pre-existing WAL to be "backward compatible" with, so this XLOG_UNDO_PAGE_WRITE case is dead code: the record type is defined (undo_xlog.h) but never emitted anywhere. Likewise XLOG_UNDO_ALLOCATE/EXTEND/ROTATE are marked "(legacy)" in the header. Dead redo cases and never-emitted WAL opcodes are rejection triggers on -hackers; remove the unused record types and their redo arms.
📄 src/backend/access/undo/undo_xlog.c (L37-L40)
Open FIXME(reviewer-item-2) blocks admitting an unresolved agnosticism breach (including access/heapam_xlog.h into the AM-agnostic UNDO core, RM_HEAP_ID branches with hard-coded heap opcode->offset knowledge and XLH_*_HAS_UNDO flag tests) are commit-blockers on -hackers. Either land the proposed rm_undo_batch_locate rmgr callback so core does not embed per-RM WAL formats, or gate this on the design being agreed on-list; a shipped subsystem should not carry self-flagged FIXMEs describing a known layering violation.
📄 src/backend/access/undo/xactundo.c (L947-L950)
This inline-abort PG_CATCH releases buffer locks, leaves the inline-apply state, and flushes the error, but -- unlike the logical revert worker's catch (logical_revert_worker.c:276) -- it does NOT call UndoResetBatchReader(). If ApplyUndoChainFromWAL() longjmps mid-read, the module-static undo_batch_reader (undo_xlog.c) is left with a stale segment FD / partial decode buffer, and the next inline abort's UndoReadBatchFromWAL() reuses the corrupted reader (the header comment on UndoResetBatchReader warns of exactly this SIGSEGV). Add UndoResetBatchReader() to this catch block. (medium-high confidence)
📄 src/backend/access/undo/README (L81-L82)
This section (and lines ~131, ~512, ~953, ~1022) documents a "Per-relation RelUndo fork" facility -- relundo.c, relundo_page.c, relundo_apply.c, RELUNDO_FORKNUM, RM_RELUNDO_ID, RelUndoVacuum/RelUndoMaybeVacuum -- as if it exists. None of these symbols or files are in the tree (no relundo.c; grep for RM_RELUNDO_ID / RELUNDO_FORKNUM finds nothing). A README for a shipped subsystem must describe what exists now; aspirational/future-tense documentation of an unimplemented facility misleads readers. Remove or clearly mark the RelUndo material as not-yet-implemented.
📄 src/backend/access/undo/slog_tuple.c (L105-L105)
Stale mechanism references throughout this file: the seqlock was replaced by a plain per-partition writer_lock (LW_SHARED reads / LW_EXCLUSIVE writes) -- SLogSeqWriteBegin/End are now no-ops (slog_flathash.h), and reads run exactly once under LW_SHARED with no retry. The SLOG_ABORT_PUBLISH_BATCH end/re-begin-"seqlock" batching here (and in SLogTupleCleanupRetained) no longer bounds any "odd-hold window" because the begin/end are no-ops; the whole batching dance and its "wait-free readers spin" justification are dead now. Either drop the now-pointless SLOG_ABORT_PUBLISH_BATCH loop breaks or, if the intent is to periodically drop the writer_lock so LW_SHARED readers can proceed, implement that explicitly. As written the comments describe a scheme that no longer exists.
📄 src/backend/access/undo/undo_bufmgr.c (L235-L236)
last_block is accepted but never used -- DropRelationBuffers drops all blocks >= first_block, so the range's upper bound is silently ignored (the comment admits this). An unused parameter that looks like it bounds the range is a footgun for callers who assume [first_block, last_block] is honored. Either drop the parameter (YAGNI) or actually bound the drop to the range; if kept for a future caller, that is speculative scaffolding.
📄 src/include/access/recno.h (L221-L221)
The or_magic field is documented as RECNO_OVERFLOW_MAGIC, but recno_overflow.c actually stores and checks RECNO_OVERFLOW_RECORD_MAGIC (0x524F5652, "ROVR") in this field (recno_overflow.c:485,809,936,1141,1423). RECNO_OVERFLOW_MAGIC (0x524F5646, "ROVF") defined below has no reference anywhere in the tree -- it is a dead macro, and the comment points a future reader at the wrong constant. Fix the comment to reference RECNO_OVERFLOW_RECORD_MAGIC and drop the unused RECNO_OVERFLOW_MAGIC define. (high confidence)
💡 Suggested change
Before:
uint32 or_magic; /* RECNO_OVERFLOW_MAGIC */
After:
uint32 or_magic; /* RECNO_OVERFLOW_RECORD_MAGIC */
📄 src/include/access/recno.h (L231-L237)
Dead scaffolding. RecnoOverflowRef and its three exported functions RecnoStoreOverflow/RecnoFetchOverflow/RecnoDeleteOverflow (declared below near lines 496/531/etc.) are a "legacy" overflow API with no callers anywhere in the tree -- RecnoFetchOverflow is a stub that unconditionally ereport(ERROR)s (recno_overflow.c:1582-1595). The struct exists only to feed these unused functions. Per minimalism/YAGNI, remove the struct and the three functions rather than shipping unreachable code that also exports a footgun (an API whose fetch path always errors). (high confidence)
📄 src/backend/access/recno/recno_vm.c (L149-L152)
WAL corruption: the VM buffer is registered with REGBUF_STANDARD, but a RECNO VM page is initialized with PageInit(page, BLCKSZ, 0), so its bitmap lives in PageGetContents() -- i.e. in the region between pd_lower and pd_upper that REGBUF_STANDARD treats as a removable "hole". When XLogInsert takes a full-page image of this buffer (first touch after a checkpoint, torn-page protection, or under wal_consistency_checking), the entire bitmap is stripped out of the FPI, and replay/standby restores an all-zero VM page -- silently losing every visibility bit. Heap deliberately registers its VM buffer with flags 0 (see visibilitymap set paths in heapam.c) for exactly this reason. Use 0 here (and in RecnoVMClear), not REGBUF_STANDARD.
💡 Suggested change
Before:
XLogRegisterBuffer(0, heapBuf, REGBUF_NO_IMAGE | REGBUF_NO_CHANGE);
XLogRegisterBuffer(1, vmBuf, REGBUF_STANDARD);
recptr = XLogInsert(RM_RECNO_ID, XLOG_RECNO_VM_SET);
After:
XLogRegisterBuffer(0, heapBuf, REGBUF_NO_IMAGE | REGBUF_NO_CHANGE);
XLogRegisterBuffer(1, vmBuf, 0);
recptr = XLogInsert(RM_RECNO_ID, XLOG_RECNO_VM_SET);
📄 src/backend/access/recno/recno_vm.c (L234-L236)
Same FPI-corruption bug as in RecnoVMSet: REGBUF_STANDARD on the VM buffer causes the bitmap (which lives in the pd_lower..pd_upper hole of a PageInit'd VM page) to be stripped from any full-page image, so FPI replay restores an all-zero VM page and loses all visibility bits. Register the VM buffer with flags 0.
💡 Suggested change
Before:
XLogRegisterBuffer(1, vmBuf, REGBUF_STANDARD);
recptr = XLogInsert(RM_RECNO_ID, XLOG_RECNO_VM_CLEAR);
After:
XLogRegisterBuffer(0, heapBuf, REGBUF_NO_IMAGE | REGBUF_NO_CHANGE);
XLogRegisterBuffer(1, vmBuf, 0);
recptr = XLogInsert(RM_RECNO_ID, XLOG_RECNO_VM_CLEAR);
📄 src/backend/access/recno/recno_vm.c (L363-L364)
RecnoVMTruncate truncates the VM fork with smgrtruncate() but emits no WAL record for it. smgrtruncate does not self-log; the caller is responsible for XLOG_SMGR_TRUNCATE (see how heap/storage.c logs relation truncation). Without a WAL record the VM fork is not truncated on a standby or after crash recovery, leaving stale VM blocks that map to heap blocks that no longer exist -- an index-only scan on the standby could then read an all-visible bit for a truncated-away block and skip a heap fetch that should have occurred. Either WAL-log the truncation here or route it through the storage-layer truncation that already logs.
📄 src/backend/access/recno/recno_stats.c (L155-L163)
The compression header is read from the wrong offset. RECNO_TUPLE_OVERHEAD is offsetof(RecnoTupleHeader, t_attrs_bitmap), so (char *) hdr + RECNO_TUPLE_OVERHEAD points at the START of the null bitmap, not at any attribute data. The actual RecnoCompressionHeader is embedded inside a varlena attribute, which sits after the MAXALIGN'd null bitmap and past that attribute's varlena header (compare with RecnoDeformTuple / RecnoTupleToSlotWithOverflow, which locate it via att_align_nominal(...) + VARDATA_ANY(data_ptr)). Reading comp_hdr->orig_size/comp_hdr->comp_size off the bitmap region yields garbage, so total_uncompressed_size/total_compressed_size -- and hence the reported compression_ratio -- are meaningless. Either deform the tuple to find the compressed attribute or drop this ratio estimate.
📄 src/backend/access/recno/recno_xlog.c (L417-L420)
Tail-read conflict between HLC info and the logical-decoding tuple image. recno_redo_handle_hlc() extracts xl_recno_hlc_info from the very end of the record: data + total_len - SizeOfXlRecnoHlcInfo. But here the HLC info is registered FIRST (line 417) and then RecnoXLogMaybeAppendLogicalTuple() appends the heap image plus a trailing uint32 len AFTER it. When a relation is both HLC-enabled (recno_use_hlc) and logically logged, the record tail is the heap image / its length trailer, not the HLC info -- so redo advances the standby HLC and drives uncertainty handling from garbage bytes. The decoder (per the header comment) also reads its heap_len from (end-4), so the two consumers fight over the same tail region. The HLC block must be appended LAST (after the logical tuple), or the HLC info needs a self-describing position rather than "last SizeOfXlRecnoHlcInfo bytes". The same ordering exists in RecnoXLogUpdate and RecnoXLogDelete.
📄 src/backend/access/undo/undoapply.c (L40-L41)
Stale/contradictory function-header comment. It says "Checks the CLR pointer to avoid double-application, then dispatches", but the body does no such check -- the block comment inside the function explicitly explains idempotency comes from page-LSN comparison, not from any CLR-pointer check (and urec_ptr is passed as InvalidUndoRecPtr by all callers). Comments must describe what the code does now. Rewrite the header to match the page-LSN mechanism.
💡 Suggested change
Before:
* Checks the CLR pointer to avoid double-application, then dispatches
* to the appropriate resource manager's rm_undo callback.
After:
* Dispatches to the appropriate resource manager's rm_undo callback.
* Idempotency is provided by page-LSN comparison during CLR redo (see the
* design note in the function body), not by any per-record applied marker.
📄 src/backend/access/undo/xactundo.c (L922-L923)
Control-flow bug in a post-abort "MUST NOT FAIL" path. undo_ctx is created and old_ctx = MemoryContextSwitchTo(undo_ctx) at lines 902-905, but on the UndoValidateBatchLSN(perm_lsn) == false branch goto inline_undo_done jumps to the label at line 958, which sits AFTER the success-path restore/cleanup (MemoryContextSwitchTo(old_ctx); MemoryContextDelete(undo_ctx);). Consequently, on the validate-false path undo_ctx is never deleted (leak) and CurrentMemoryContext is left pointing at the leaked undo_ctx for the rest of AtAbort_XactUndo and beyond. Move the switch-back/delete before the validate check (or into the label), or restructure so both paths restore old_ctx and free undo_ctx.
📄 src/backend/access/undo/undoapply.c (L170-L174)
No CHECK_FOR_INTERRUPTS() in this unbounded chain-walk loop. ApplyUndoChainFromWAL()/Bounded() is called from the live-backend abort path (xactundo.c AtAbort_XactUndo) and from the logical revert worker. Rolling back a large transaction can process an unbounded number of batches/records with no way to respond to query-cancel or fast-shutdown signals, blocking the backend indefinitely. Add a CHECK_FOR_INTERRUPTS() per batch iteration (interrupts are already held off inside crit sections in rm_undo, so the top of the loop is safe).
💡 Suggested change
Before:
while (XLogRecPtrIsValid(batch_lsn))
{
UndoBatchData *batch;
char *pos;
char *end;
After:
while (XLogRecPtrIsValid(batch_lsn))
{
UndoBatchData *batch;
char *pos;
char *end;
CHECK_FOR_INTERRUPTS();
📄 src/backend/access/undo/undo_xlog.c (L231-L235)
xlrec->target_offset comes straight from the WAL record and is passed to PageGetItemId() (raw pointer arithmetic) with no bounds check against PageGetMaxOffsetNumber(page). A corrupt/torn CLR whose target_offset exceeds the page's line-pointer count causes an out-of-bounds read/write during redo. This BLK_NEEDS_REDO path is reachable whenever the FPI is absent. Standard heap redo routines validate the offset first; add an if (target_offset == InvalidOffsetNumber || target_offset > PageGetMaxOffsetNumber(page)) ereport(ERROR, ...) (or report a corrupt-page) before the PageGetItemId() calls in all of the LP_DEAD/LP_UNUSED/HAS_TUPLE/HAS_DELTA/HAS_VISIBILITY/HOT branches.
📄 src/backend/access/undo/undoworker.c (L128-L129)
shutdown_requested is written from the SIGTERM handler undo_worker_sigterm() and read from the main loop, but it is declared as a plain bool in UndoWorkerShmemData (undoworker.h). A flag touched from an async signal handler must be volatile sig_atomic_t; a plain bool is not guaranteed atomic or re-read across the loop, so the shutdown request can be missed or torn. Change the struct field type to volatile sig_atomic_t and store 1/0.
📄 src/backend/access/undo/undolog.c (L47-L49)
These variables are documented and used as GUCs (see undobuffer.c comment "Tunable via the undo_batch_size_kb and undo_batch_record_limit GUCs"), but they are never registered in guc_tables.c nor via DefineCustomIntVariable — the guc_tables.c diff only adds #include lines. As written they are plain compile-time-constant globals: users cannot tune them, and there is no min/max bound. Either register them properly (with bounds) or drop the "GUC"/"Tunable" wording. Related: because there is no upper bound, undo_batch_size_kb * 1024 in undobuffer.c is an int multiply without the (Size) cast used in xactundo.c and would overflow if the value ever exceeded ~2GB/1024.
📄 src/backend/access/undo/undobuffer.c (L40-L40)
undo_batch_size_kb * 1024 is an int*int multiply with no (Size) cast, unlike the (Size) undo_batch_size_kb * 1024 used in xactundo.c. Combined with the absence of any registered GUC upper bound, this can overflow int. Add a Size cast for consistency and safety.
💡 Suggested change
Before:
#define UNDO_BUFFER_FLUSH_THRESHOLD (undo_batch_size_kb * 1024)
After:
#define UNDO_BUFFER_FLUSH_THRESHOLD ((Size) undo_batch_size_kb * 1024)
📄 src/backend/access/undo/README (L656-L657)
Documented default is wrong. Code default is undo_worker_naptime = 10000 (10s, undolog.c) and postgresql.conf.sample uses 60s; neither matches the README's "Default: 60000 (1 minute)". Fix the README to the actual default (or drop the specific number).
📄 src/backend/access/undo/README (L659-L660)
Documented default is wrong. Code default is undo_retention_time = 60000 (60s, undolog.c) and postgresql.conf.sample uses 300s; neither matches the README's "Default: 3600000 (1 hour)". Fix the README to the actual default (or drop the specific number).
📄 src/backend/access/undo/undo.c (L102-L108)
Dead code / stale contract. UndoShmemSize() (exported in undo.h) has no caller: ipci.c does not call it (shmem is now sized via the PG_SHMEM_SUBSYSTEM callbacks / individual ShmemRequest() calls in UndoShmemRequest_internal). Worse, its accounting is now inconsistent with the request path -- it sums UndoLogShmemSize/XactUndoShmemSize/UndoWorkerShmemSize/LogicalRevertShmemSize/ATMShmemSize, but UndoShmemRequest_internal only issues Pbu/PendingUndo/UndoLauncher requests. Either remove UndoShmemSize() and its declaration, or make it the single source of truth actually used for the request. (The README's "ipci.c calls UndoShmemSize()" is likewise stale.)
📄 src/backend/access/undo/undoworker.c (L606-L607)
Use of sprintf into fixed-size bgw_* buffers is the flagged unsafe pattern and is inconsistent with the snprintf() used two lines below for bgw_name/bgw_type. Although the string literals fit today, prefer bounded copies: strlcpy(worker.bgw_library_name, "postgres", sizeof(worker.bgw_library_name)) and likewise for bgw_function_name.
💡 Suggested change
Before:
sprintf(worker.bgw_library_name, "postgres");
sprintf(worker.bgw_function_name, "UndoWorkerMain");
After:
strlcpy(worker.bgw_library_name, "postgres", sizeof(worker.bgw_library_name));
strlcpy(worker.bgw_function_name, "UndoWorkerMain", sizeof(worker.bgw_function_name));
📄 src/backend/access/undo/undoworker.c (L395-L396)
retained_mb is uint64 but is formatted with %lu and cast to (unsigned long); on LLP64 platforms (Windows) unsigned long is 32 bits, truncating the value. Use UINT64_FORMAT (and drop the cast) for 64-bit quantities.
📄 src/backend/access/undo/undobuffer.c (L270-L271)
UndoBufferTakePayload() and UndoBufferReset() (and the whole embed-into-DML-WAL contract they document) have no callers anywhere in the tree — code_search finds them referenced only in comments in recno_handler.c. This is dead scaffolding (YAGNI): either wire it up in an AM's WAL path or remove both functions and their header declarations. As written, the documented take->XLogRegisterData->reset sequence that the comments rely on is never exercised.
📄 src/backend/access/undo/slog_tuple.c (L579-L584)
Potential lock-ordering hazard: TransactionIdIsInProgress()/TransactionIdDidCommit() acquire ProcArrayLock while fp->writer_lock (LWTRANCHE_SLOG) is held, establishing writer_lock -> ProcArrayLock. Any path that acquires a sLog writer_lock while holding ProcArrayLock would deadlock. It is also a performance footgun to run per-op ProcArrayLock-taking status probes across a full hash scan under an exclusive LWLock. Prefer collecting candidate keys/xids under writer_lock, releasing it, then resolving transaction status without the writer_lock held (as other code in this file does for the horizon computation).
📄 src/backend/access/undo/xactundo.c (L731-L735)
Asymmetry with AtAbort_XactUndo: this commit path calls UndoRecordSetFree() (which re-parents the last freed MemoryContext to TopMemoryContext and stashes it in the static UndoRecordReusableContext) but never calls UndoRecordSetResetCache(). Only AtAbort_XactUndo() releases that cache. As a result, after a committing transaction the recycled context persists reparented under TopMemoryContext across the transaction boundary. This contradicts the undorecord.c header, which states the cached context is "cleaned up at transaction end by UndoRecordSetResetCache()". Either call UndoRecordSetResetCache() here too (symmetry, and matches the documented contract) or fix the comment to state the cache intentionally survives across commits.
📄 src/backend/access/undo/undobuffer.c (L203-L207)
The rel parameter is never used: UndoBufferEnd operates entirely on the global undo_t2buf and deactivates whatever relation is currently active. UndoBufferBegin() even calls UndoBufferEnd(rel) at line 187 passing the NEW relation while it is actually flushing/deactivating the OLD one (undo_t2buf.relid) — it only works because rel is ignored. This is misleading (POLA). Either drop the parameter from UndoBufferEnd (and the header) or assert that it matches the active relation. The single-global-buffer model also silently assumes no two relations are interleaved within one statement; that assumption should be documented/asserted, since a mismatched Begin/End sequence would silently misattribute or drop buffered UNDO records.
📄 src/backend/access/undo/slog.c (L367-L371)
The "never fails ... always true" contract is inaccurate and a footgun on the abort path. slog_atm_set() allocates from the sLog DSA area, which has a hard limit (dsa_set_size_limit(slog_dsa_max_size_mb), default 256MB). When that limit is hit, the radix tree's dsa_allocate ereport(ERROR)s -- it does not return false. So SLogTxnInsert can neither return false nor complete. This defeats ATMAddAborted()'s documented fallback (a false return is supposed to trigger synchronous rollback); instead the abort path ERRORs after XLOG_ATM_ABORT has already been written. Either propagate a real full/failure indication back to ATMAddAborted for the fallback to work, or document precisely that exhaustion ERRORs out and justify why that is acceptable on the abort path.
📄 src/backend/access/undo/undobuffer.c (L37-L38)
These are described as GUCs, but undo_batch_size_kb / undo_batch_record_limit are plain int globals defined in undolog.c and are not registered in guc_tables.c (nor via DefineCustomIntVariable). They cannot be tuned at runtime. Either register them as real GUCs or correct this comment to not call them GUCs.
📄 src/backend/access/undo/undolog.c (L154-L156)
uint64 offsets formatted with %llu and cast to (unsigned long long). Project convention is UINT64_FORMAT for 64-bit values; the same applies to the total_allocated/total_discarded/retained lines in CheckPointUndoLog and the discard-worker messages. Prefer UINT64_FORMAT and drop the casts.
📄 src/backend/access/undo/undostats.c (L114-L117)
GetUndoBufferStats() unconditionally returns all zeros, so the SQL-callable pg_stat_get_undo_buffers() always reports num_buffers=0, hits=0, misses=0, and hit_ratio=0.0. Shipping a monitoring function that reports meaningless constant data is a footgun (operators will read it as "no cache activity") and dead observability surface (YAGNI). Under UNDO-in-WAL, consider removing this function (and its pg_proc entry) and pointing users at pg_buffercache, rather than shipping a constant-zero reporter.
📄 src/backend/access/undo/undostats.c (L289-L291)
pg_undo_force_discard() drives the SEALED/DISCARDABLE lifecycle and calls UndoLogSealAndRotate() / UndoLogDeleteSegmentFile(), both of which are explicit no-ops under UNDO-in-WAL (see undolog.c stubs). Outside of WAL replay, no normal-operation path allocates a log (in_use/UNDO_LOG_ACTIVE are set only in the undo_xlog.c redo path), so in a running server this function scans MAX_UNDO_LOGS empty slots, performs no rotation, and returns 0. This is a user-visible, privileged SQL function whose seal/rotate behavior does nothing. Either remove the dead seal/rotate/discardable scaffolding or gate/repurpose this function for the WAL-based model.
📄 src/backend/access/undo/undoinsert.c (L55-L59)
UndoWalBatchFlush/UndoWalBatchReset are pure no-ops "kept for callers that haven't been updated yet", but every caller (xactundo.c AtCommit/AtPrepare/AtAbort) is in this same patch and could simply not call them. Per YAGNI and comment-accuracy, do not ship dead no-op functions plus their call sites with "not updated yet"/"kept for safety" comments in a new subsystem; either the callers need the call (then it isn't a no-op) or they don't (then remove both the functions and the calls). After this patch the code should read as if it had always been written this way.
📄 src/backend/access/undo/logical_revert_worker.c (L241-L242)
Starvation/liveness bug. ATMGetNextUnreverted -> SLogTxnGetNextUnreverted always returns the FIRST unreverted entry in ascending xid order, regardless of database (confirmed in slog.c). If that oldest entry belongs to another database, this worker takes goto sleep, wakes after logical_revert_naptime, re-reads the SAME entry, and loops forever -- never reaching a later entry that does match MyDatabaseId. The foreign-db entry only clears when its own database's worker forgets it, so a matching-db entry queued behind it can be starved indefinitely. The per-database filtering must happen inside the ATM scan (pass MyDatabaseId to the iterator and skip non-matching entries there), not by bailing out of the loop on the first non-matching entry.
📄 src/backend/access/undo/logical_revert_worker.c (L304-L307)
Incorrect recycled-vs-retained test can PANIC the whole cluster on a benign transient error. GetRedoRecPtr() returns the last checkpoint's redo pointer, which routinely advances far past WAL that is still on disk and NOT recycled. KeepLogSeg() (xlog.c) pins WAL against removal using UndoGetOldestBatchLSN(); the redo pointer sits somewhere between that retention floor and the insert point. So last_batch_lsn < redo is true for essentially any batch older than the most recent checkpoint even though its WAL is fully retained. A transient apply error (e.g. lock timeout, OOM) on such a batch will therefore PANIC the entire cluster rather than retry -- a denial-of-service / crash hazard. The correct "was recycled" test must compare against the actual oldest segment still present (e.g. XLogGetLastRemovedSegno converted to an LSN), not against GetRedoRecPtr().
📄 src/backend/access/undo/logical_revert_worker.c (L369-L370)
Unbounded sprintf into fixed bgw_ fields. Use snprintf with the buffer size, consistent with LogicalRevertLauncherRegister() in this same file which correctly uses snprintf(..., MAXPGPATH/BGW_MAXLEN, ...). e.g. snprintf(worker.bgw_library_name, MAXPGPATH, "postgres"); snprintf(worker.bgw_function_name, BGW_MAXLEN, "LogicalRevertWorkerMain");
📄 src/backend/access/undo/perbackend/pbu_discardworker.c (L60-L60)
Async-signal-safety violation. got_SIGTERM is a plain static bool that is written from a signal handler (undoworker_sigterm_handler) and read in the main loop (while (!got_SIGTERM)). A plain bool accessed across a signal handler is not guaranteed to be read/written atomically and the compiler may cache or reorder the load, so the shutdown flag can be missed or torn. It must be volatile sig_atomic_t, as logical_revert_worker.c correctly does for its handlers.
📄 src/backend/access/undo/perbackend/pbu_discardworker.c (L90-L91)
Unbounded sprintf into fixed-size bgw_library_name / bgw_function_name. Use snprintf with the field size to match the bgw_name line just above (which correctly uses snprintf(..., BGW_MAXLEN, ...)): snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres"); snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DiscardWorkerMain");
📄 src/backend/access/undo/perbackend/pbu_discardworker.c (L9-L10)
Stale IDENTIFICATION path and copyright. This file lives at src/backend/access/undo/perbackend/pbu_discardworker.c, not "src/backend/postmaster/discardworker.c". Also the file header names it "discardworker.c". Fix the IDENTIFICATION to the real path (and consider updating the copyright year range, which reads 1996-2019).
📄 src/include/access/logical_revert_worker.h (L40-L41)
These GUC-backed globals are defined in logical_revert_worker.c and read from other translation units (e.g. guc_tables.c registration and postmaster.c launcher registration). On Windows/MSVC an extern variable referenced across module boundaries needs PGDLLIMPORT on its declaration, or the link fails for the backend and any extension. Add PGDLLIMPORT to both externs. (Also note: a header under src/include should not #include "postgres.h"; that is added at the top of this file and is against convention.)
📄 src/backend/access/undo/perbackend/pbu_undoaction.c (L9-L9)
Stale IDENTIFICATION path. This file is at src/backend/access/undo/perbackend/pbu_undoaction.c, not "src/backend/access/undo/undoaction.c"; the header title also reads "undoaction.c". Fix the IDENTIFICATION and title to the actual path/name, and consider updating the 1996-2019 copyright range.
📄 src/backend/access/undo/perbackend/pbu_undoactionxlog.c (L11-L11)
Stale IDENTIFICATION path. Actual path is src/backend/access/undo/perbackend/pbu_undoactionxlog.c, not "src/backend/access/undo/undoactionxlog.c"; header title also reads "undoactionxlog.c". Correct to match the real file.
📄 src/backend/access/undo/perbackend/pbu_undodiscard.c (L9-L9)
Stale IDENTIFICATION path. Actual path is src/backend/access/undo/perbackend/pbu_undodiscard.c, not "src/backend/access/undo/undodiscard.c"; header title also reads "undodiscard.c". Correct to match the real file.
📄 src/backend/access/undo/perbackend/pbu_undoinsert.c (L9-L9)
Stale IDENTIFICATION path. Actual path is src/backend/access/undo/perbackend/pbu_undoinsert.c, not "src/backend/access/undo/undoinsert.c"; header title also reads "undoinsert.c". Correct to match the real file.
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L15-L15)
Stale IDENTIFICATION path. Actual path is src/backend/access/undo/perbackend/pbu_undolog.c, not "src/backend/access/undo/undolog.c"; header title also reads "undolog.c". Correct to match the real file.
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L639-L639)
Debug logging explicitly marked "XXX: remove me" left in a production path. This emits a LOG line for every undo segment created, which spams the server log under normal write load. Remove before commit (same applies to the other "XXX: remove me" elog(LOG, ...) traces in this file: recycled/unlinked undo segment in PbuUndoLogDiscard, undolog_xlog_discard, and ResetUndoLogs).
📄 src/backend/access/undo/logical_revert_worker.c (L582-L585)
Death-handling idiom is inconsistent with LogicalRevertWorkerMain in this same file, which uses WL_EXIT_ON_PM_DEATH. The modern PostgreSQL convention is WL_EXIT_ON_PM_DEATH (which exits cleanly inside WaitLatch) rather than WL_POSTMASTER_DEATH plus a manual proc_exit(1). Prefer WL_EXIT_ON_PM_DEATH here too for consistency and to drop the manual check.
📄 src/backend/access/undo/perbackend/pbu_undoaction.c (L740-L741)
Minimalism concern: this AM-dispatch machinery is dead in this patch. The file's own comments state no consumer registers a per-backend UNDO rmgr in this phase, so GetUndoRmgr() always returns NULL here and the loop body reduces to continue for every record; correspondingly PbuAtAbort_ApplyUndo() always returns immediately because pbuUndoStartPtr is never set to a valid value (verified: SetCurrentTransactionPbuUndoLocation has no live caller this phase). Shipping a large unwired apply/dispatch path with no producer is speculative scaffolding -- either wire up the first consumer in the same series or split this out so each committed piece is exercised by a caller/test. At minimum, a reviewer needs a clear statement of which later patch activates it.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1052-L1053)
The rollback hash table key size is wrong. Every lookup builds a RollbackHashKey (full_xid + start_urec_ptr = 16 bytes) and the entry struct comments even mark full_xid as "must be first entry", but here keysize = sizeof(TransactionId) is only 4 bytes. With HASH_BLOBS, dynahash hashes and compares exactly keysize bytes, so only the low 4 bytes of full_xid participate and start_urec_ptr is ignored entirely. Two distinct requests that share the low 32 bits of the xid collide as "equal", so a real request can be silently matched to the wrong entry (or a duplicate insert rejected), corrupting rollback bookkeeping. This should be sizeof(RollbackHashKey).
💡 Suggested change
Before:
info.keysize = sizeof(TransactionId);
info.entrysize = sizeof(RollbackHashEntry);
After:
info.keysize = sizeof(RollbackHashKey);
info.entrysize = sizeof(RollbackHashEntry);
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1112-L1112)
These memsets clear only the first element of each queue array (sizeof(UndoXidQueue) is the element type size, not the array size). The remaining ROLLBACK_REQUEST_QUEUE_SIZE - 1 (1023) elements are left uninitialized, so their full_xid is not set to InvalidFullTransactionId. UndoXidQueueGetFreeElem() / UndoSizeQueueGetFreeElem() / UndoErrorQueueGetFreeElem() scan for full_xid == InvalidFullTransactionId to find a free slot; with garbage in those slots they can hand out an occupied slot or fail to find a free one, corrupting the queues. Multiply by the array length.
💡 Suggested change
Before:
memset(UndoXidQueueElems, 0, sizeof(UndoXidQueue));
After:
memset(UndoXidQueueElems, 0,
ROLLBACK_REQUEST_QUEUE_SIZE * sizeof(UndoXidQueue));
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1116-L1116)
Same bug for the size queue: sizeof(UndoSizeQueue) clears only element 0. Use the full array length.
💡 Suggested change
Before:
memset(UndoSizeQueueElems, 0, sizeof(UndoSizeQueue));
After:
memset(UndoSizeQueueElems, 0,
ROLLBACK_REQUEST_QUEUE_SIZE * sizeof(UndoSizeQueue));
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1120-L1120)
Same bug for the error queue: sizeof(UndoErrorQueue) clears only element 0. Use the full array length.
💡 Suggested change
Before:
memset(UndoErrorQueueElems, 0, sizeof(UndoErrorQueue));
After:
memset(UndoErrorQueueElems, 0,
ROLLBACK_REQUEST_QUEUE_SIZE * sizeof(UndoErrorQueue));
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1197-L1199)
rh is dereferenced (rh->in_progress = false) without a NULL check. hash_search(..., HASH_FIND, ...) returns NULL when the entry is absent, which can happen if the request was already removed/discarded by the time the error path runs. Every other RollbackHT lookup in this file guards with if (!rh || ...); this one does not, so an absent entry crashes the undo worker with a NULL dereference. Guard it (and decide the correct behavior when the entry is gone).
💡 Suggested change
Before:
rh = (RollbackHashEntry *) hash_search(RollbackHT, (void *) &urinfo->full_xid,
HASH_FIND, NULL);
rh->in_progress = false;
After:
rh = (RollbackHashEntry *) hash_search(RollbackHT, (void *) &urinfo->full_xid,
HASH_FIND, NULL);
if (rh == NULL)
{
LWLockRelease(RollbackRequestLock);
return false;
}
rh->in_progress = false;
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1197-L1198)
This HASH_FIND uses a bare &urinfo->full_xid as the key, whereas everywhere else the code passes a full RollbackHashKey (full_xid + start_urec_ptr). Combined with the too-small keysize above, both sides currently agree only by accident (4 bytes off the front). Once keysize is corrected to sizeof(RollbackHashKey), this lookup will read past full_xid into whatever follows it on the stack. Build a proper RollbackHashKey with start_urec_ptr set, as the other lookups do.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L986-L986)
Signed-integer overflow risk: this is int * int * int arithmetic performed before promotion to the uint64 req_size. The GUC undo_rollback_overflow_size has max INT_MAX / 1024, so rollback_overflow_size * 1024 can reach ~INT_MAX and the subsequent * 1024 overflows a signed int (undefined behavior), mis-classifying request size for large GUC values. Promote to 64-bit before multiplying, e.g. (uint64) rollback_overflow_size * 1024 * 1024.
💡 Suggested change
Before:
if (req_size >= rollback_overflow_size * 1024 * 1024 ||
After:
if (req_size >= (uint64) rollback_overflow_size * 1024 * 1024 ||
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L156-L156)
Comment typos in the new file: "rollabck" -> "rollback".
💡 Suggested change
Before:
/* This is the hash table to store all the rollabck requests. */
After:
/* This is the hash table to store all the rollback requests. */
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L9-L9)
Stale IDENTIFICATION path for a ported file: this header claims src/backend/access/undo/undorequest.c, but the actual path is src/backend/access/undo/perbackend/pbu_undorequest.c. Also the copyright year (2019) is stale relative to the tree. Fix the path to the real file location per PG convention.
💡 Suggested change
Before:
* src/backend/access/undo/undorequest.c
After:
* src/backend/access/undo/perbackend/pbu_undorequest.c
📄 src/backend/access/undo/perbackend/pbu_undorecord.c (L308-L308)
Comment typos: "bookkeeeping infrormation" -> "bookkeeping information".
💡 Suggested change
Before:
/* Update bookkeeeping infrormation. */
After:
/* Update bookkeeping information. */
📄 src/backend/access/undo/perbackend/pbu_undorecord.c (L9-L9)
Stale IDENTIFICATION path for a ported file: this header claims src/backend/access/undo/undorecord.c, but the actual path is src/backend/access/undo/perbackend/pbu_undorecord.c. Fix it to the real file location per PG convention.
💡 Suggested change
Before:
* src/backend/access/undo/undorecord.c
After:
* src/backend/access/undo/perbackend/pbu_undorecord.c
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L740-L740)
Comment typo: "wroker" -> "worker".
💡 Suggested change
Before:
* Get the dbid where the wroker should connect to and get the worker
After:
* Get the dbid where the worker should connect to and get the worker
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L10-L10)
Stale IDENTIFICATION path for a ported file: this header claims src/backend/postmaster/undoworker.c, but the actual path is src/backend/access/undo/perbackend/pbu_undoworker.c. Fix it to the real file location per PG convention.
💡 Suggested change
Before:
* src/backend/postmaster/undoworker.c
After:
* src/backend/access/undo/perbackend/pbu_undoworker.c
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L878-L880)
w->proc->procLatch (and below, w->proc->pid) are dereferenced under only the w->in_use && w->lingering guard. A slot is in_use with proc == NULL during the attach window (UndoWorkerLaunch sets in_use=true, proc=NULL before the worker attaches). In practice lingering is only set by an already-attached worker, so proc is non-NULL then and the crash is unlikely -- but the invariant is implicit and TOCTOU-prone. Consider adding an explicit w->proc != NULL guard to both loops to harden against a future caller setting lingering earlier.
💡 Suggested change
Before:
if (w->in_use && w->lingering && w->dbid == dbid)
{
SetLatch(&w->proc->procLatch);
After:
if (w->in_use && w->lingering && w->proc != NULL && w->dbid == dbid)
{
SetLatch(&w->proc->procLatch);
📄 src/backend/access/undo/xactundo.c (L307-L312)
Persistence level is dropped: the per-plevel record set is created with UndoRecordSetCreate(), which unconditionally sets uset->persistence = UNDOPERSISTENCE_PERMANENT (undorecord.c:153) and is never fixed up here. UndoRecordSetInsert() then writes xlrec.persistence = uset->persistence, so an UNLOGGED/TEMP record set emits its XLOG_UNDO_BATCH tagged PERMANENT. During crash recovery UndoRecoveryTrackBatch() slots the LSN into entry->last_batch_lsn[persistence]; the recovery-undo loop only skips j == TEMP/UNLOGGED (undo_xlog.c:863), so a mis-tagged UNLOGGED batch lands in the PERMANENT slot (index 0) and gets applied against relation files that were reset to empty on crash recovery -- corruption. The current in-tree consumers (nbtree/hash) happen to hardcode RELPERSISTENCE_PERMANENT, so this is latent, but the xactundo API contract accepts a persistence argument and silently loses it. Set the record set's persistence to plevel at creation.
💡 Suggested change
Before:
uset = UndoRecordSetCreate(xid, GetCurrentTransactionUndoRecPtr());
XactUndo.record_set[plevel] = uset;
}
/* Remember persistence level for InsertXactUndoData. */
ctx->plevel = plevel;
After:
uset = UndoRecordSetCreate(xid, GetCurrentTransactionUndoRecPtr());
uset->persistence = plevel;
XactUndo.record_set[plevel] = uset;
}
/* Remember persistence level for InsertXactUndoData. */
ctx->plevel = plevel;
📄 src/backend/access/undo/xactundo.c (L383-L391)
Same persistence-loss bug as in PrepareXactUndoData(): this is the create site exercised by the live nbtree/hash index-UNDO consumers. UndoRecordSetCreate() hardcodes uset->persistence = UNDOPERSISTENCE_PERMANENT and it is never set to plevel here, so a record set for an UNLOGGED/TEMP relation would emit XLOG_UNDO_BATCH mis-tagged PERMANENT and be applied during crash recovery against files that were reset to empty. Set uset->persistence = plevel right after create.
💡 Suggested change
Before:
uset = UndoRecordSetCreate(xid, GetCurrentTransactionUndoRecPtr());
XactUndo.record_set[plevel] = uset;
}
/* Remember persistence level for InsertXactUndoData. */
ctx->plevel = plevel;
ctx->uset = uset;
/* Add the record using scatter-gather payload API. */
After:
uset = UndoRecordSetCreate(xid, GetCurrentTransactionUndoRecPtr());
uset->persistence = plevel;
XactUndo.record_set[plevel] = uset;
}
/* Remember persistence level for InsertXactUndoData. */
ctx->plevel = plevel;
ctx->uset = uset;
/* Add the record using scatter-gather payload API. */
📄 src/backend/access/undo/undorecord.c (L153-L153)
uset->persistence is hardcoded to UNDOPERSISTENCE_PERMANENT and never overwritten anywhere in the tree, yet the field is documented as "Persistence level of this set" and is consumed by UndoRecordSetInsert() (xlrec.persistence = uset->persistence) which stamps it into the XLOG_UNDO_BATCH header. This makes the batch header's persistence always PERMANENT regardless of the real relation persistence (see the correctness impact flagged in xactundo.c PrepareXactUndoData/Parts). Either accept the persistence level as a parameter to UndoRecordSetCreate() and initialize it here, or have callers set it after creation.
📄 src/backend/access/undo/xactundo.c (L973-L974)
Stray consecutive blank lines. Per patch hygiene, remove the extra vertical whitespace so the diff reads cleanly (pgindent/git diff --check).
💡 Suggested change
Before:
else
elog(WARNING, "ATM full: could not record aborted transaction %u", GetCurrentTransactionId());
After:
else
elog(WARNING, "ATM full: could not record aborted transaction %u", GetCurrentTransactionId());
📄 src/backend/access/undo/undorecord.c (L263-L264)
Comment is inaccurate: this function is NOT called at commit. AtAbort_XactUndo() calls UndoRecordSetResetCache(), but AtCommit_XactUndo() does not -- it only calls UndoRecordSetFree() (which stashes a context in UndoRecordReusableContext). As written the comment overstates the guarantee and masks the commit-path leak. Either fix the comment to say "at transaction abort" or (preferably) add the missing call in AtCommit_XactUndo() and keep the comment.
📄 src/backend/access/undo/undorecord.c (L241-L244)
Unsubstantiated, unverifiable micro-benchmark numbers in a comment ("~5 cycles vs ~300 cycles"). These are not reproducible and will drift; -hackers reviewers reject specific performance figures without a benchmark. State the qualitative reason (avoids MemoryContextReset/AllocSetContextCreate) and drop the cycle counts.
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L1274-L1275)
Non-portable format specifier for an int64. checkPointRedo is an XLogRecPtr (uint64); %016llX assumes unsigned long long is exactly 64-bit and mismatches the argument type on LP64 platforms, triggering -Wformat warnings and technically UB. It is also inconsistent with the write/read paths in this same file (CheckPointUndoLogs/StartupUndoLogs), which correctly use "%016" INT64_MODIFIER "X". Since UndoCheckPointFilenamePrecedes() string-compares filenames produced by both formats, any divergence in width/case would break the comparison. Use the same format here.
💡 Suggested change
Before:
snprintf(oldest_path, MAXPGPATH, "%016llX",
checkPointRedo);
After:
snprintf(oldest_path, MAXPGPATH, "%016" INT64_MODIFIER "X",
checkPointRedo);
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L2519-L2523)
Uninitialized tuple column. PG_STAT_GET_UNDO_LOGS_COLS is 9 (indices 0..8), but this loop only ever fills values[0]..values[7]. Column index 8 is never assigned and nulls[8] is never set to true (the nulls array is initialized {false}), so tuplestore_putvalues() reads values[8] from uninitialized stack memory and treats it as a non-null Datum -- undefined behavior and a likely crash when the result rowtype has 9 attributes. Either set values[8]/nulls[8] for the intended column, or reduce PG_STAT_GET_UNDO_LOGS_COLS to match the columns actually produced.
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L2503-L2503)
Spelling error in a user-visible string: "" should be "". This value can appear in the output of this stat function.
💡 Suggested change
Before:
log->meta.persistence == UNDO_TEMP ? "temporary" : "<uknown>");
After:
log->meta.persistence == UNDO_TEMP ? "temporary" : "<unknown>");
📄 src/backend/access/undo/perbackend/pbu_undoinsert.c (L500-L504)
Wrong variable in bounds assertion. Here idx is the (constant) function parameter selecting the xact_urec_info[] slot, while i is the per-buffer loop counter that indexes idx_undo_buffers[i] and is incremented on each iteration. The intent is to assert the buffer index stays within MAX_BUFFER_PER_UNDO, so this must check i, not idx (compare the analogous asserts in PrepareUndoInsert using index and InsertPreparedUndo using bufidx). As written the assert never catches a buffer-array overrun.
💡 Suggested change
Before:
starting_byte = UndoLogBlockHeaderSize;
i++;
Assert(idx < MAX_BUFFER_PER_UNDO);
} while (true);
After:
starting_byte = UndoLogBlockHeaderSize;
i++;
Assert(i < MAX_BUFFER_PER_UNDO);
} while (true);
📄 src/backend/access/undo/undoworker.c (L310-L313)
Dead scaffolding under UNDO-in-WAL. This entire SEALED/DISCARDABLE lifecycle loop (and the Phase 1 discard-pointer loop above it) is unreachable in normal operation: UndoLogControl.in_use is only ever set true in the undo_xlog.c redo path, never during normal running, so if (!log->in_use) continue; skips every slot. undolog.h itself documents that "the ACTIVE->SEALED->DISCARDABLE rotation no longer occurs", and UndoLogDeleteSegmentFile() is a documented no-op stub. Only the per-backend slot scan and the trailing UndoSetDiscardHorizon(UndoGetOldestBatchLSN()) are live. Per YAGNI, drop the segment-lifecycle machinery (Phase 1/Phase 2, sealed_log_count, UndoLogDeleteSegmentFile call) or gate/remove it rather than shipping a discard worker whose bulk never runs.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L273-L278)
SetErrorQueueElem sets dbid, full_xid, and err_occurred_at but never sets start_urec_ptr. However, the error queue element's start_urec_ptr is later read to build the RollbackHT lookup key in both RemoveOldElemsFromErrorQueue() (hkey.start_urec_ptr = elem->start_urec_ptr) and GetRollbackHashKeyFromQueue() for ERROR_QUEUE (hkey->start_urec_ptr = elem->start_urec_ptr). Because keysize is full_xid + start_urec_ptr (16 bytes), an error-queue element carries a stale/zero start_urec_ptr, so the hash lookup fails to find the matching RollbackHashEntry (which was registered with the real start_urec_ptr). Consequently requests that hit the error queue can never be re-processed and their undo is silently dropped. Add start_urec_ptr to SetErrorQueueElem and pass urinfo->start_urec_ptr from PushErrorQueueElem.
💡 Suggested change
Before:
#define SetErrorQueueElem(elem, e_dbid, e_full_xid, e_occurred_at) \
( \
GetErrorQueueElem(elem).dbid = e_dbid, \
GetErrorQueueElem(elem).full_xid = e_full_xid, \
GetErrorQueueElem(elem).err_occurred_at = e_occurred_at \
)
After:
#define SetErrorQueueElem(elem, e_dbid, e_full_xid, e_start_urec_ptr, e_occurred_at) \
( \
GetErrorQueueElem(elem).dbid = e_dbid, \
GetErrorQueueElem(elem).full_xid = e_full_xid, \
GetErrorQueueElem(elem).start_urec_ptr = e_start_urec_ptr, \
GetErrorQueueElem(elem).err_occurred_at = e_occurred_at \
)
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L717-L721)
if (GetErrorQueueSize() <= n)
{
cur_undo_queue++;
return false;
}
💡 Suggested change
Before:
if (GetErrorQueueSize() <= n)
{
cur_undo_queue++;
return false;
}
After:
if (GetErrorQueueSize() <= n)
return false;
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L717-L721)
The ERROR_QUEUE branch mutates the global round-robin cursor cur_undo_queue when the queue is empty, but the XID_QUEUE and SIZE_QUEUE branches do not. This side effect is wrong in the peek loop of UndoGetWork(): that loop iterates a local cur_queue (0..MAX_UNDO_WORK_QUEUES) and is meant to be read-only, yet each empty-error-queue probe silently advances the global cur_undo_queue, perturbing where the next non-peek scan starts. In the non-peek path the caller already does its own cur_undo_queue++ after a false return, so this extra increment double-advances. Remove the stray increment so the accessor stays side-effect free and consistent with the other two branches.
💡 Suggested change
Before:
if (GetErrorQueueSize() <= n)
{
cur_undo_queue++;
return false;
}
After:
if (GetErrorQueueSize() <= n)
return false;
📄 src/include/access/logical_revert_worker.h (L25-L25)
A header must never include postgres.h; by convention postgres.h is included first by each .c file, before any other header. logical_revert_worker.c already does #include "postgres.h" as its first line and then includes this header, so this line is at best redundant and at worst breaks the include discipline for any future consumer. The other two headers in this group (pbu_undorequest.h, pbu_undoworker.h) correctly omit it. Remove this include; the header only needs Size/Datum, which come in via the mandatory postgres.h in every .c that uses it. [high]
📄 src/include/access/logical_revert_worker.h (L40-L41)
GUC variables exported for use by other modules need PGDLLIMPORT or the Windows/MSVC build fails to link. Both logical_revert_naptime and max_logical_revert_workers are defined in logical_revert_worker.c and max_logical_revert_workers is already referenced cross-module from slog_tuple.c; without PGDLLIMPORT this breaks the MSVC build. [high]
💡 Suggested change
Before:
+extern int logical_revert_naptime;
+extern int max_logical_revert_workers;
After:
+extern PGDLLIMPORT int logical_revert_naptime;
+extern PGDLLIMPORT int max_logical_revert_workers;
📄 src/include/access/perbackend/pbu_undorequest.h (L31-L31)
Exported extern variable used across modules needs PGDLLIMPORT for the Windows/MSVC build. latestRecentGlobalXmin is defined in pbu_undorequest.c and exposed here for other translation units; a plain extern breaks linking on MSVC. [high]
💡 Suggested change
Before:
+extern TransactionId latestRecentGlobalXmin;
After:
+extern PGDLLIMPORT TransactionId latestRecentGlobalXmin;
📄 src/include/access/perbackend/pbu_undoworker.h (L18-L18)
UndoWorkerDelay is declared here but never defined or referenced anywhere in the tree (grep finds only this declaration). Dead scaffolding: either wire it up to a GUC/definition and add PGDLLIMPORT (it is described as a GUC), or drop the declaration. [medium]
📄 src/include/access/perbackend/pbu_undoworker.h (L3-L9)
Header identity comments are wrong (copy-pasted from the old zheap patch). The file name line says undoworker.h and the IDENTIFICATION path says src/include/access/undoworker.h, but the real path is src/include/access/perbackend/pbu_undoworker.h. Fix the file name and path so the comment matches reality. The stale 1996-2019 copyright range should also be updated. [medium]
💡 Suggested change
Before:
+ * undoworker.h
+ * Exports from undoworker.c.
+ *
+ * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/access/undoworker.h
After:
+ * pbu_undoworker.h
+ * Exports from pbu_undoworker.c.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/access/perbackend/pbu_undoworker.h
📄 src/include/access/perbackend/pbu_undorequest.h (L3-L8)
Header identity comments are wrong (copy-pasted from the old zheap patch). The file name line says undorequest.h and the IDENTIFICATION path says src/include/access/undorequest.h, but the real path is src/include/access/perbackend/pbu_undorequest.h. Fix to match the actual file, and update the stale 1996-2019 copyright range. [medium]
💡 Suggested change
Before:
+ * undorequest.h
+ * Exports from undo/undorequest.c.
+ *
+ * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
+ *
+ * src/include/access/undorequest.h
After:
+ * pbu_undorequest.h
+ * Exports from undo/perbackend/pbu_undorequest.c.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ *
+ * src/include/access/perbackend/pbu_undorequest.h
📄 src/include/access/perbackend/pbu_undorequest.h (L111-L118)
Macro arguments are not parenthesized: urinfo-> and rh-> are used directly, unlike the sibling ResetUndoRequestInfo which correctly writes (urinfo)->. If a caller ever passes a compound expression this evaluates incorrectly (classic macro footgun). Parenthesize both operands. Note also this macro sets only 5 of 6 fields (omits request_size); it happens to be safe today only because every call site in pbu_undorequest.c calls ResetUndoRequestInfo(urinfo) first, which zeroes request_size. That implicit dependency is fragile and worth a comment. [medium]
💡 Suggested change
Before:
+#define SetUndoRequestInfoFromRHEntry(urinfo, rh, cur_queue) \
+( \
+ urinfo->full_xid = rh->full_xid, \
+ urinfo->start_urec_ptr = rh->start_urec_ptr, \
+ urinfo->end_urec_ptr = rh->end_urec_ptr, \
+ urinfo->dbid = rh->dbid, \
+ urinfo->undo_worker_queue = cur_queue \
+)
After:
+#define SetUndoRequestInfoFromRHEntry(urinfo, rh, cur_queue) \
+( \
+ (urinfo)->full_xid = (rh)->full_xid, \
+ (urinfo)->start_urec_ptr = (rh)->start_urec_ptr, \
+ (urinfo)->end_urec_ptr = (rh)->end_urec_ptr, \
+ (urinfo)->dbid = (rh)->dbid, \
+ (urinfo)->undo_worker_queue = (cur_queue) \
+)
📄 src/include/access/logical_revert_worker.h (L15-L15)
Stale copyright range copied from the source material. The rest of this new patch's headers use 1996-2019; this one uses 1996-2026, which is inconsistent and also ahead of the current year. Use the tree's current standard range. [low]
💡 Suggested change
Before:
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
After:
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
📄 src/include/access/perbackend/pbu_compat.h (L47-L49)
[portability, high] INT64_MODIFIER is hardcoded to "ll", assuming int64 == long long. On current master int64 is int64_t, and on LP64 platforms where int64_t maps to plain long, "ll" produces a wrong-width printf specifier for UndoRecPtrFormat ("%016" INT64_MODIFIER "X"), which is undefined behavior. The tree's portable convention is the PRI*64 macros: e.g. UndoRecPtrFormat should be built from PRIX64 ("%016" "X" via "%016" PRIX64), matching how c.h defines INT64_FORMAT as "%" PRId64. Replace the hand-rolled modifier with PRIX64.
📄 src/include/access/perbackend/pbu_compat.h (L115-L120)
[maintainability/style, low] PROCARRAY_FLAGS_* and the GetOldestXmin() shim are bundled under a single #ifndef PROCARRAY_FLAGS_DEFAULT guard. If a future master ever defines PROCARRAY_FLAGS_DEFAULT alone (without the others or without reintroducing 2-arg GetOldestXmin), this block is skipped entirely and the remaining names stay undefined, breaking the build in a confusing way. Guard each name independently, or at minimum assert the coupling with a comment; a single-name upstream reintroduction is a realistic drift given these are exactly the symbols that churn.
📄 src/include/access/perbackend/pbu_compat.h (L119-L119)
[maintainability, medium] The GetOldestXmin(rel, flags) shim silently discards flags. Verified call sites: pbu_discardworker.c passes PROCARRAY_FLAGS_AUTOVACUUM|PROCARRAY_FLAGS_VACUUM (intent: ignore vacuum backends to get a younger, more aggressive discard horizon), and pbu_undolog.c passes PROCARRAY_FLAGS_DEFAULT. GetOldestNonRemovableTransactionId(NULL) returns shared_oldest_nonremovable, which does NOT ignore (auto)vacuum, so the discard worker now retains strictly more undo than the original code intended. The comment's correctness argument holds, but note this changes the discard-aggressiveness behavior versus the vendored code (a vacuum backend's snapshot now pins undo it previously would not). This is a behavior regression worth calling out explicitly rather than burying as "a tuning knob".
📄 src/include/access/perbackend/pbu_compat.h (L212-L212)
[maintainability, low] #define RelFileNode RelFileLocator is a token replacement over a core type name that leaks into every translation unit including this header. Verified RelFileNode no longer exists as a live type in master (fully renamed to RelFileLocator in PG16), so there is no direct symbol collision today; however recno_pbu.h's own banner notes these pbu headers "conflict, redefining UndoRecordHeader / RelFileNode etc.", so the alias is already forcing include-isolation workarounds elsewhere in this change set. A wholesale core-type macro is a maintenance footgun; a per-site fix (or a local typedef confined to the pbu .c files) would avoid leaking the alias through the public header.
📄 src/include/access/perbackend/pbu_undolog.h (L189-L190)
[maintainability, medium] Macro hygiene: the do/while(false) wrapper ends with a trailing semicolon (} while (false);). The standard multi-statement-macro idiom omits the trailing ; so the macro behaves like a single statement and callers append their own ;. As written, if (cond) UndoRecPtrAssignRelFileNode(rfn, urp); else ... fails to compile (the stray empty statement breaks the if/else), and every call site emits a spurious empty statement. Drop the semicolon after while (false).
💡 Suggested change
Before:
(rfn).relNumber = UndoRecPtrGetRelNode(urp); \
} while (false);
After:
(rfn).relNumber = UndoRecPtrGetRelNode(urp); \
} while (false)
📄 src/include/access/perbackend/pbu_undorecord.h (L43-L46)
[portability, medium] UndoRecordHeader and the other on-disk/WAL structs (UndoRecordBlock/Transaction/Payload, and xl_undolog_* in pbu_undolog_xlog.h) are serialized to undo pages via a raw byte copy: pbu_undorecord.c does InsertUndoBytes((char *) &work_hdr, SizeOfUndoRecordHeader, ...). Because these structs are NOT declared packed, the header's own noted alignment hole after urec_rmid (RmgrId=uint8 followed by more uint8s then Oid) becomes part of the persisted layout. Any ABI/compiler that pads differently produces an on-disk/WAL format that another build cannot decode. Either mark these structs pg_attribute_packed or serialize field-by-field; do not persist sizeof/offsetof-based blobs whose padding is compiler-defined.
📄 src/include/access/perbackend/pbu_undorecord.h (L43-L46)
[portability, medium] This struct and the other on-disk undo structs (UndoRecordBlock/Transaction/Payload) are serialized to undo pages by raw byte copy: pbu_undorecord.c does InsertUndoBytes((char *) &work_hdr, SizeOfUndoRecordHeader, ...). Because the struct is NOT declared packed, the alignment hole this very comment flags (RmgrId urec_rmid is uint8, followed by uint8/uint16/Oid) becomes part of the persisted layout. Any ABI/compiler that inserts different padding produces an on-disk format another build cannot decode - a hard portability gate for a persistent format. Either mark these structs pg_attribute_packed, or serialize field-by-field instead of copying sizeof/offsetof-based blobs whose padding is compiler-defined. Resolve the TODO rather than shipping it.
📄 src/include/access/perbackend/pbu_helpers.h (L9-L10)
[documentation, medium] Stale/aspirational comment. This says SetCurrentUndoLocation "is a no-op here; it is wired into xact.c in Phase 2b", but the implementation (pbu_helpers.c) already calls SetCurrentTransactionPbuUndoLocation(), and xact.h/xact.c already export/define that function. The behavior is shipped, not deferred, so the "Phase 2a no-op / Phase 2b" framing is inaccurate. Per the tree's comment-accuracy convention, drop the future-tense/phase scaffolding and describe what the function does now. Note this affects the trailing per-declaration comment below too ("wired to xact.c in Phase 2b").
📄 src/include/access/perbackend/pbu_undorecord.h (L9-L9)
[documentation, low] File-header banner path is wrong: it reads src/include/access/undorecord.h but the file lives at src/include/access/perbackend/pbu_undorecord.h. This violates the tree's file-header convention and misleads readers/grep. Same stale-path issue exists in the sibling new headers (pbu_undolog.h -> "undolog.h", pbu_undoinsert.h -> "undoinsert.h", pbu_undodiscard.h -> "undoinsert.h"/"undodiscard.h", pbu_undoaction_xlog.h -> "undoaction_xlog.h", pbu_undolog_xlog.h -> "undolog_xlog.h"); fix them to match the actual paths.
💡 Suggested change
Before:
* src/include/access/undorecord.h
After:
* src/include/access/perbackend/pbu_undorecord.h
📄 src/include/access/perbackend/pbu_undoaction_xlog.h (L3-L4)
[documentation, low] Header banner is doubly wrong: the leading name says undoaction_xlog.h and the IDENTIFICATION path says src/include/access/undoaction_xlog.h, but the actual file is src/include/access/perbackend/pbu_undoaction_xlog.h. Fix both to match the real filename/path per the tree's file-header convention.
📄 src/include/access/perbackend/pbu_undorequest.h (L28-L28)
InvalidUndoWorkerQueue (-1) is assigned to UndoRequestInfo.undo_worker_queue, which has enum type UndoWorkerQueueType (see ResetUndoRequestInfo below at line 107, and pbu_undoworker.c:442). All enumerators of UndoWorkerQueueType are non-negative (0, 1, 2), so a conforming compiler is free to pick an unsigned underlying type for the enum; storing -1 then wraps to a large positive value, making the sentinel and any future == InvalidUndoWorkerQueue comparison unreliable. Additionally the macro body is unparenthesized, so InvalidUndoWorkerQueue expands to a bare -1 that can bind incorrectly in surrounding expressions. Parenthesize the value, and use a real enumerator (e.g. add INVALID_UNDO_WORKER_QUEUE = -1 to the enum) rather than an out-of-range int macro.
💡 Suggested change
Before:
#define InvalidUndoWorkerQueue -1
After:
#define InvalidUndoWorkerQueue (-1)
📄 src/include/access/undo_xlog.h (L267-L271)
SizeOfUndoBatch uses sizeof(UndoPersistenceLevel), and the trailing persistence member of this enum type is written into the WAL stream verbatim: undobuffer.c:326 does XLogRegisterData((char *) &xlrec, SizeOfUndoBatch) and the redo/read side casts the raw bytes back to xl_undo_batch. The width of a bare C enum is implementation-defined (typically int on GCC/Clang, but affected by -fshort-enums and differing on other ABIs), so this makes the WAL/on-disk record layout non-portable across the compilers/platforms PostgreSQL targets. A record written by one build cannot be reliably replayed by another. Use a fixed-width integer field (e.g. uint8 persistence;) in the on-disk struct and cast to/from UndoPersistenceLevel in memory. The same discipline is needed anywhere UndoPersistenceLevel is embedded in a durable record.
📄 src/include/access/undo_xlog.h (L52-L55)
These record types are defined and fully handled in the redo path (undo_redo() in undo_xlog.c) and in undodesc.c, but a codebase-wide search shows none of them is ever emitted: the only XLogInsert(RM_UNDO_ID, ...) call sites use XLOG_UNDO_APPLY_RECORD and XLOG_UNDO_BATCH. undo_xlog.c even comments that XLOG_UNDO_PAGE_WRITE "is no longer emitted". Dead on-disk record definitions plus a redo path that can never be exercised (hence untestable) are speculative scaffolding in a brand-new subsystem. Remove the never-emitted record types (XLOG_UNDO_ALLOCATE, XLOG_UNDO_EXTEND, XLOG_UNDO_ROTATE, XLOG_UNDO_PAGE_WRITE), their structs (xl_undo_allocate, xl_undo_extend, xl_undo_rotate, xl_undo_page_write), and the UNDO_ROTATE_* reasons, along with their redo/desc handling.
📄 src/include/access/undormgrlist.h (L39-L43)
This list statically enumerates FLUX, RECNO and FILEOPS as built-in UNDO resource managers, and undo.c's RegisterUndoRmgrs() #includes this file to call each init function unconditionally. That directly contradicts the design stated in undormgr.h ("The core defines only UNDO_RMID_INVALID and the built-in index-AM IDs ... this core header names no specific table AM or subsystem", "2, 4, and 5 are reserved by out-of-core consumers"). Hardcoding flux/recno/fileops here couples the generic UNDO core to specific optional AMs -- they will be link-time required in every build even when those AMs are not wired in. Either these AMs are genuinely in-core (then fix the undormgr.h comment that claims they are out-of-core and reserve their IDs there), or they should register themselves without appearing in this core list. As written the two files disagree about the design.
📄 src/include/access/undormgr.h (L45-L48)
This comment is stale/incorrect relative to the shipped code. The actual RM IDs are UNDO_RMID_FILEOPS=2, UNDO_RMID_FLUX=5 and UNDO_RMID_RECNO=7 (see fileops.h/flux_undo.h/recno_undo.h), and undormgrlist.h -- included by the core undo.c -- statically registers all of them as "built-in". So (a) ID 4 is not actually used by anything, (b) ID 7 (RECNO) is in use but not listed as reserved, and (c) the claim that the core "names no specific table AM or subsystem" is false given undormgrlist.h. Since these IDs are stamped into WAL-durable records and must stay unique/stable, the reserved-ID comment must accurately list every consumer's ID.
📄 src/include/access/undo.h (L49-L50)
UndoContext is an exported global. On Windows/MSVC any variable read from another module or an extension needs PGDLLIMPORT, or the reference fails to link. Add PGDLLIMPORT if UndoContext is (or may be) referenced outside its defining translation unit.
📄 src/include/access/undormgr.h (L106-L107)
UndoRmgrs[] is an exported global array. Reads go through GetUndoRmgr() in-tree, but the array is declared extern in a public header, so any cross-module/extension direct reference on Windows/MSVC needs PGDLLIMPORT. Either add PGDLLIMPORT or keep this global private to undormgr.c and expose only the GetUndoRmgr()/RegisterUndoRmgr() accessors in the header.
📄 src/include/access/undolog.h (L138-L139)
UndoLogShared is an exported global that is dereferenced directly from multiple modules (undo_xlog.c, undoworker.c, undostats.c, ...). For Windows/MSVC and extension safety, exported globals referenced cross-module need PGDLLIMPORT. The GUC externs below (undo_retention_time, pbu_undo_workers_enabled, undo_buffer_size, etc.) have the same exposure. Add PGDLLIMPORT to these extern declarations.
📄 src/include/access/undolog.h (L189-L197)
These declare an exported API surface of pure no-op stubs (their definitions in undolog.c do nothing). Callers still invoke some of them (e.g. xactundo.c calls UndoLogCloseFiles()/UndoFlushResetMaxWritePtr(), undoworker.c/undostats.c call UndoLogSealAndRotate()/UndoLogDeleteSegmentFile()), so the dead code is spread across call sites too. Retaining an exported no-op API "for callers not yet fully updated" is exactly the speculative scaffolding -hackers rejects: it should either do something or be removed along with its callers. UNDO_LOG_SEGMENT_SIZE is not referenced anywhere outside this header and can be dropped.
📄 src/include/access/undorecord.h (L75-L75)
The comment (and access/undo/README) confirm urec_prev is written by the record builders (undobuffer.c, undorecord.c) but never read -- the real chain is between batches via xl_undo_batch.chain_prev. Carrying an 8-byte field (plus 4 bytes of alignment padding, per the README layout) in every WAL-serialized UNDO record header purely "for format stability" is dead payload that costs space on every DML that writes UNDO. Consider dropping it from the serialized header rather than paying the per-record cost for a field with no reader.
📄 src/include/access/perbackend/pbu_undorecord.h (L102-L103)
SizeOfUndoRecordRelationDetails uses sizeof(uint8) (1 byte) but urec_fork is a ForkNumber enum whose storage is int-sized (4 bytes on all platforms). pbu_undorecord.c serializes/deserializes this field by raw byte copy: InsertUndoBytes((char *) &work_rd, SizeOfUndoRecordRelationDetails, ...) / matching ReadUndoBytes(...). Copying only 1 byte of a 4-byte enum works by accident on little-endian (it grabs the LSB) but on big-endian (s390x, PPC64-BE) it copies the always-zero MSB, silently losing the fork number. This is a portable-on-disk-format defect. Either declare the field as uint8/int16 explicitly, or size the copy with sizeof(uur->uur_fork)/sizeof(ForkNumber) and serialize the full width consistently. [high confidence]
📄 src/include/access/perbackend/pbu_undolog.h (L196-L200)
UndoLogMetaData embeds two C enums (UndoLogStatus status, UndoPersistence persistence) yet the whole struct is serialized to durable storage by raw byte copy: pbu_undolog.c writes it into WAL via XLogRegisterData((char *) xlrec, sizeof(xl_undolog_meta)) (xl_undolog_meta embeds UndoLogMetaData) and into the pg_undo checkpoint files via memcpy(&serialized[...], &log->meta, sizeof(UndoLogMetaData)). Enum storage size is implementation-defined in C, so the on-disk/WAL layout of these fields is not stable across compilers/ABIs and is not portable across the platforms PostgreSQL targets. Use fixed-width integer fields (e.g. uint8/int16) for on-disk/WAL-serialized enum values. [high confidence]
📄 src/include/access/perbackend/pbu_undolog_xlog.h (L30-L35)
xl_undolog_create embeds a C enum (UndoPersistence persistence) and is written to WAL by raw byte copy: pbu_undolog.c does XLogRegisterData((char *) &xlrec, sizeof(xlrec)) / XLogInsert(RM_UNDOLOG_ID, XLOG_UNDOLOG_CREATE). Enum storage width is implementation-defined in C, so this WAL record's on-disk layout is not stable across compilers/ABIs and is not portable across the platforms PostgreSQL supports. WAL-serialized enum fields must use a fixed-width integer type (e.g. uint8). [high confidence]
📄 src/include/access/perbackend/pbu_undoinsert.h (L62-L64)
This header references the type name RelFileNode in the UndoGetOneRecord() prototype, but RelFileNode no longer exists in master -- it is only made visible via the #define RelFileNode RelFileLocator shim in pbu_compat.h, which this header does not include (nor do any of its #included headers). As written, pbu_undoinsert.h is not self-contained: it compiles only when the includer happened to pull in pbu_compat.h first, and it will fail the tree's headerscheck/cpluspluscheck standalone-compile gate. Either #include "access/perbackend/pbu_compat.h" here or spell the parameter type as RelFileLocator directly. [high confidence]
📄 src/include/access/xact.h (L154-L154)
Missing PGDLLIMPORT on these new hook variables. Every other core hook in the tree is declared extern PGDLLIMPORT (see planner_hook, ExecutorStart_hook, ProcessUtility_hook, shmem_startup_hook, etc.), and the pre-existing extern vars in this same header (DefaultXactIsoLevel, MyXactFlags, ...) all carry it. These hooks are assigned/invoked cross-module (an AM module sets them, xact.c calls them); on Windows/MSVC an extern variable referenced from another module or extension requires PGDLLIMPORT or the module fails to link. This is a portability hard gate.
💡 Suggested change
Before:
extern void (*TableAMPrepare_hook) (void);
After:
extern PGDLLIMPORT void (*TableAMPrepare_hook) (void);
📄 src/include/access/undoworker.h (L41-L42)
shutdown_requested is written directly from the SIGTERM handler (undo_worker_sigterm in undoworker.c) and read in the worker's main loop (while (!UndoWorkerShmem->shutdown_requested)). A plain bool touched from a signal handler is not async-signal-safe and the compiler may cache the loop read. PostgreSQL's convention for a signal-set flag is volatile sig_atomic_t (cf. startup.c's static volatile sig_atomic_t shutdown_requested). Since this field is in shared memory and also set cross-process by UndoWorkerRequestShutdown(), a plain non-volatile bool is doubly unsafe.
📄 src/include/access/xact.h (L169-L172)
Same PGDLLIMPORT omission as TableAMPrepare_hook: these four hook variables must be extern PGDLLIMPORT to be usable cross-module on Windows/MSVC, consistent with every other core hook declaration in the tree.
💡 Suggested change
Before:
extern void (*PendingPhysOpsDo_hook) (bool isCommit);
extern void (*PendingPhysOpsPostPrepare_hook) (void);
extern void (*PendingPhysOpsAtSubCommit_hook) (void);
extern void (*PendingPhysOpsAtSubAbort_hook) (void);
After:
extern PGDLLIMPORT void (*PendingPhysOpsDo_hook) (bool isCommit);
extern PGDLLIMPORT void (*PendingPhysOpsPostPrepare_hook) (void);
extern PGDLLIMPORT void (*PendingPhysOpsAtSubCommit_hook) (void);
extern PGDLLIMPORT void (*PendingPhysOpsAtSubAbort_hook) (void);
📄 src/include/access/undoworker.h (L49-L51)
These extern GUC-style globals are read from other backend modules (undoworker.c/undolog.c) and are candidates for extension use; on Windows/MSVC extern variables referenced cross-module require PGDLLIMPORT. Note also that undo_worker_naptime/undo_retention_time are documented here and in the code as configurable but are never registered via guc_tables.c or DefineCustomIntVariable -- they are compile-time constants with hardcoded initializers, so the 'GUC parameters' comment is misleading.
💡 Suggested change
Before:
/* GUC parameters */
extern int undo_worker_naptime;
extern int undo_retention_time;
After:
/* GUC parameters */
extern PGDLLIMPORT int undo_worker_naptime;
extern PGDLLIMPORT int undo_retention_time;
📄 src/include/access/xact.h (L423-L424)
This StaticAssertDecl is a near-useless guard: offsetof(...) > 0 is trivially true for any non-first member and does NOT verify the array length equals NUndoPersistenceLevels -- which is the actual invariant the surrounding comments claim to protect. The real compile-time guard already exists in xactundo.c (StaticAssertDecl(NUndoPersistenceLevels == 3, ...)) where both headers are available. This assertion here adds no protection; consider removing it to avoid implying a guarantee it does not provide.
📄 src/include/access/xact.h (L415-L415)
SizeOfXactPrepare is defined but never used anywhere in the tree; the (de)serialization sites (e.g. xactdesc.c) use MAXALIGN(sizeof(xl_xact_prepare)) directly. This is dead code. Either drop it, or convert the existing call sites to use it consistently so there is a single source of truth for the on-disk header size.
📄 src/include/access/xactundo.h (L140-L140)
This comment describes undo_instant_abort_threshold as a GUC that transactions/users configure, but it is never registered as a GUC (no entry in guc_tables.c and no DefineCustomIntVariable call anywhere). It is a plain global int with a hardcoded initializer (65536) in xactundo.c, so it cannot actually be tuned at runtime. Either register it as a real GUC or drop the 'GUC:' wording so the header does not overstate configurability. Also, being referenced cross-module, the extern needs PGDLLIMPORT on Windows/MSVC.
💡 Suggested change
Before:
extern int undo_instant_abort_threshold;
After:
extern PGDLLIMPORT int undo_instant_abort_threshold;
📄 src/include/access/undodefs.h (L19-L23)
These typedefs (UndoRecPtr, UndoRecordSize, UndoLogOffset, UndoLogNumber) and the InvalidUndoRecPtr / UndoRecPtrIsValid macros are also defined verbatim by the parallel per-backend header access/perbackend/pbu_undolog.h (see its lines 28-32, 100-101, 143-144), with no cross-guard between the two hierarchies. Both headers are co-included in the same TU: e.g. src/backend/access/flux/flux_pbu.c pulls undodefs.h (via undormgr.h/flux_undo.h/flux_pbu.h) AND pbu_undolog.h. Redefining an identical typedef is a C99 constraint violation (the tree's baseline; only C11 relaxed it), and duplicate object-like macros trip -Werror. The tree already documents this collision in twophase_rmgr.c: "the per-backend engine headers cannot be included here (they redefine types that conflict with the cluster-wide UNDO engine)." The two UNDO type/macro namespaces need to be unified, or undodefs.h must guard its typedefs so it is safe to co-include. (high confidence)
📄 src/include/access/undo_bufmgr.h (L195-L196)
The entire buffer read/release/invalidation API declared here -- ReadUndoBuffer, ReadUndoBufferExtended, ReleaseUndoBuffer, UnlockReleaseUndoBuffer, MarkUndoBufferDirty, UndoMakeBufferTag, IsUndoBufferTag, InvalidateUndoBuffers, InvalidateUndoBufferRange -- plus the file-layout macros (UndoRecPtrGetFileOffset, UNDO_USABLE_BYTES_PER_PAGE, UndoRecPtrGetBlockNum, UndoRecPtrGetPageOffset, UndoLogicalToFileSize) has no caller anywhere outside undo_bufmgr.c itself. In the UNDO-in-WAL design (which this header's own preamble describes) UNDO never touches the buffer pool. This is speculative scaffolding for a path that is not wired up; per the minimalism/YAGNI discipline it should be dropped rather than shipped as an unused public header. (high confidence)
📄 src/include/access/undo_xlog.h (L291-L292)
These three externs have no definition and no registration anywhere in the tree. The actual two-phase UNDO path uses pbu_twophase_postcommit/pbu_twophase_postabort (pbu_recovery.c), wired into twophase_rmgr.c's callback arrays; undo_twophase_recover/postcommit/postabort are never defined nor referenced. They are dead declarations for functions that do not exist -- remove them (or the WAL 2PC path they belong to). (high confidence)
📄 src/include/access/undo_xlog.h (L279-L283)
xl_undo_chain_state is unused: the comment claims it is "Saved in the two-phase state file during PREPARE TRANSACTION," but no code reads or writes this struct. Dead scaffolding -- drop it. (high confidence)
📄 src/include/access/undolog.h (L143-L145)
Comment is inaccurate: MAX_UNDO_LOGS is not legacy. It is load-bearing -- it sizes the logs[MAX_UNDO_LOGS] array inside UndoLogSharedData (below) and is iterated throughout undolog.c, undo_xlog.c, undostats.c, undoworker.c. Only UNDO_LOG_SEGMENT_SIZE is truly unused (its sole reference is its own #define and this comment). Lumping MAX_UNDO_LOGS in as a "legacy define retained for compile-time compatibility" is misleading. (high confidence)
📄 src/include/access/undorecord.h (L107-L112)
UndoRecordSetChunkHeader / SizeOfUndoRecordSetChunkHeader, and the URST_MULTI / URST_EPHEMERAL enumerators, are never used anywhere in the tree (only URST_TRANSACTION and URST_INVALID appear, in undorecord.c). The "chunk-based organization" the comment describes never materialized in the WAL-integrated design. Dead scaffolding -- drop the unused chunk header and enum members. (high confidence)
📄 src/include/access/undorecord.h (L200-L202)
UndoWalBatchFlush/UndoWalBatchReset are documented in their definitions (undoinsert.c) as "Legacy no-op" and are still invoked from xactundo.c (with a "no-op, kept for safety" comment). Same dead-no-op-stub pattern as the undolog.h stubs: an exported API surface of functions that do nothing, plus live callers that only add noise. Remove the calls and the declarations. (high confidence)
📄 src/backend/access/common/reloptions.c (L157-L162)
index_undo is a new user-visible reloption (settable via CREATE TABLE / ALTER TABLE ... SET), but it is not documented in doc/src/sgml/ref/create_table.sgml where the other storage-parameter reloptions (e.g. vacuum_truncate, user_catalog_table) are described. A user-visible change without documentation is WIP, not commit-ready by pgsql-hackers standards. Add a <varlistentry> for index_undo to create_table.sgml. (high confidence)
📄 src/backend/access/Makefile (L25-L28)
The existing SUBDIRS list is alphabetically ordered (brin, common, ... transam). The three new entries are appended out of order (undo, flux, recno) and in a different order than meson.build (which lists flux, recno before nbtree and undo last). Recursion order is otherwise harmless here since each subdir builds an independent SUBSYS.o, but for readability/consistency prefer keeping SUBDIRS alphabetical: ... transam then flux, recno, undo interleaved alphabetically, or at minimum match one canonical ordering across Makefile and meson.build. (low confidence, style)
📄 src/include/access/xactundo.h (L93-L97)
This safety contract is inaccurate: AtPrepare_XactUndo() does NOT call XactUndoFlushPending() at its top. Its actual implementation (xactundo.c) calls UndoWalBatchFlush() and then Assert(!XactUndoHasPendingData()), relying on the flush having already happened earlier in PrepareTransaction(). Since this comment is the documented safety rationale for deferring records ("never the reason a rollback finds nothing to apply"), naming a non-flushing function here is misleading. Drop AtPrepare_XactUndo() from this list (or reword to reflect that PREPARE relies on an earlier flush plus an assertion).
💡 Suggested change
Before:
* Deferral is only safe because every path that can consume the chain flushes
* first: XactUndoFlushPending() is called at the top of AtAbort_XactUndo(),
* AtSubAbort_XactUndo(), AtCommit_XactUndo() and AtPrepare_XactUndo(), and on
* crossing the size/record thresholds below. A deferred record is therefore
* never the reason a rollback finds nothing to apply.
After:
* Deferral is only safe because every path that can consume the chain flushes
* first: XactUndoFlushPending() is called at the top of AtAbort_XactUndo(),
* AtSubAbort_XactUndo() and AtCommit_XactUndo(), and on crossing the
* size/record thresholds below. (PrepareTransaction() flushes before the 2PC
* state captures the chain-head LSN; AtPrepare_XactUndo() only asserts nothing
* is left pending.) A deferred record is therefore never the reason a
* rollback finds nothing to apply.
📄 src/include/access/undostats.h (L37-L49)
This struct and its field comments describe live cache instrumentation ("Aggregate statistics from the UNDO buffer cache", cache_hits/misses/evictions/writes), but with UNDO now living in shared_buffers the producing function GetUndoBufferStats() unconditionally zero-fills every field. The comment is aspirational/stale, and the whole struct plus its SQL wrapper is effectively dead code exposing a permanently-zero view to users (a POLA violation). Either drop UndoBufferStat/GetUndoBufferStats and the SQL function entirely, or document plainly that these counters are retained-but-always-zero because UNDO pages are tracked via pg_buffercache.
📄 src/include/access/slog.h (L239-L240)
These exported global/GUC variables lack PGDLLIMPORT. They are referenced cross-module: slog_dsa_max_size_mb and slog_num_partitions are GUCs (guc_tables.c includes slog.h), and the flux_*.c AMs consume this API. On Windows/MSVC any extern variable read from another module or extension must be annotated PGDLLIMPORT or it will fail to link. The sibling headers in this same PR (recno.h, flux.h) correctly annotate every exported var (e.g. recno_slog_entries_per_backend, TTSOpsRecnoTuple); do the same here.
💡 Suggested change
Before:
/* GUC: maximum DSA size (in MB) */
extern int slog_dsa_max_size_mb;
After:
/* GUC: maximum DSA size (in MB) */
extern PGDLLIMPORT int slog_dsa_max_size_mb;
📄 src/include/access/slog.h (L400-L401)
slog_num_partitions is a GUC variable read from guc_tables.c (a different TU) and must carry PGDLLIMPORT for MSVC/extension linkage, consistent with all other exported GUCs in the tree.
💡 Suggested change
Before:
/* GUC: number of sLog flat hash partitions (0 = auto based on CPU count) */
extern int slog_num_partitions;
After:
/* GUC: number of sLog flat hash partitions (0 = auto based on CPU count) */
extern PGDLLIMPORT int slog_num_partitions;
📄 src/include/access/slog_flathash.h (L237-L237)
SLogNumPartitions is used in the inline function SLogFlatHashPartitionIndex() in this header (compiled into every including module) and directly in slog_flathash.c, i.e. it is read across translation units and by any extension AM. Without PGDLLIMPORT this breaks the MSVC build. Add the annotation.
💡 Suggested change
Before:
extern int SLogNumPartitions;
After:
extern PGDLLIMPORT int SLogNumPartitions;
📄 src/include/access/slog_flathash.h (L124-L124)
Non-ASCII multiplication sign 'x' (U+00D7) in a source comment. PostgreSQL source must be ASCII-only (no smart quotes/em-dashes/multiplication signs). Use '4x' or '4 times'.
💡 Suggested change
Before:
* 4× the number of CPUs, clamped to [16, 256], rounded to next power of 2.
After:
* 4x the number of CPUs, clamped to [16, 256], rounded to next power of 2.
📄 src/include/access/slog_flathash.h (L234-L234)
Non-ASCII em-dash (U+2014) in a source comment. Keep source ASCII-only; use '--' or a comma/hyphen.
💡 Suggested change
Before:
* Runtime partition count — set during SLogShmemInit() based on
After:
* Runtime partition count -- set during SLogShmemInit() based on
📄 src/include/access/slog.h (L81-L83)
Memory-sizing footgun. SLogFlatBucket (slog_flathash.h) embeds a full SLogTupleEntry, which embeds SLogTupleOp ops[SLOG_MAX_TUPLE_OPS] (128 slots, ~40 bytes each -> ~5 KB per bucket). The flat-hash capacity auto-sizes to MaxBackends * 1024 entries (clamped to SLOG_TUPLE_MAX_ENTRIES=4194304, capacity clamp 2*1048576 buckets). At the upper end that is on the order of ~10 GB of static shared memory. Critically, SLogShmemRequest() calls SLogTupleShmemRequest() unconditionally and the sLog is a core PG_SHMEM_SUBSYSTEM, so this reservation is paid at every server startup even when no AM (flux/recno) is in use. Please bound the worst case (e.g. gate the tuple-hash reservation on an AM/GUC opt-in, or shrink SLOG_MAX_TUPLE_OPS / the per-backend slot count) and document the expected shmem cost.
📄 src/include/access/slog_flathash.h (L188-L191)
Footgun: SLOG_SEQ_READ_BEGIN acquires writer_lock in LW_SHARED and SLOG_SEQ_READ_END releases it, so any return, goto, or error thrown (ereport/elog/CHECK_FOR_INTERRUPTS) inside the block leaks the LWLock. This relies entirely on convention. Current call sites (slog_tuple.c) obey it, but this is easy to get wrong. Consider making the contract enforceable (e.g. wrap the body in a helper that guarantees release, or at minimum keep the loud warning) so future callers cannot introduce a leak silently.
📄 src/backend/access/common/reloptions.c (L158-L161)
index_undo is declared with AccessExclusiveLock. Like deduplicate_items (which uses ShareUpdateExclusiveLock "since it applies only to later inserts"), this option only changes the behavior of future DML/ROLLBACK on the table -- it does not rewrite existing data, so it does not obviously need to block concurrent readers/writers. Taking AccessExclusiveLock makes every ALTER TABLE ... SET (index_undo = ...) block the whole table, a heavier lock than functionally required. Unless there is a crash-recovery/in-flight-transaction reason for the stronger lock (please document it if so), ShareUpdateExclusiveLock is likely sufficient and less disruptive. (low confidence)
💡 Suggested change
Before:
"index_undo",
"Reverses this table's index entries during ROLLBACK instead of leaving them for VACUUM",
RELOPT_KIND_HEAP,
AccessExclusiveLock
After:
"index_undo",
"Reverses this table's index entries during ROLLBACK instead of leaving them for VACUUM",
RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
📄 src/include/access/slog_flathash.h (L208-L208)
Dead parameter. max_backends is unused in both SLogFlatHashShmemSize and SLogFlatHashPartitionedShmemSize -- the implementation casts it to (void) ("retained for call-site compatibility"). Per the minimalism/YAGNI discipline, drop the unused parameter from the signature and all call sites rather than carry dead API surface. Confidence: high.
💡 Suggested change
Before:
extern Size SLogFlatHashShmemSize(int capacity, int max_backends);
After:
extern Size SLogFlatHashShmemSize(int capacity);
📄 src/include/access/slog_flathash.h (L213-L214)
Same dead max_backends parameter here as in SLogFlatHashShmemSize; the implementation never reads it. Remove it from the signature and the callers in slog_tuple.c.
💡 Suggested change
Before:
extern Size SLogFlatHashPartitionedShmemSize(int total_capacity,
int max_backends);
After:
extern Size SLogFlatHashPartitionedShmemSize(int total_capacity);
📄 src/include/access/slog_flathash.h (L157-L161)
Stale naming/documentation. The header's own top comment states the seqlock reader/writer scheme was removed in favor of the per-partition writer_lock LWLock, yet the write-side helpers (SLogSeqWriteBegin/SLogSeqWriteEnd), the read-side macros (SLOG_SEQ_READ_BEGIN/SLOG_SEQ_READ_END), and their comments still describe a "seqlock" and a "sequence counter" that no longer exist. These names/comments will mislead future readers into thinking a lock-free sequence protocol is in play. Rename to reflect the LWLock reality (e.g. SLogReadBegin/SLogReadEnd) and drop the unused seqvar scaffolding, or at minimum fix the comments to stop describing a seqlock. Confidence: high.
📄 src/backend/access/index/indexam.c (L251-L252)
This comment is factually wrong. FLUX and RECNO (both added in this same changeset) set am_index_delete_marking = true and call index_delete_mark()/index_undo_mark() at runtime during in-place UPDATE and ROLLBACK (see flux_operations.c:2520/2545 and recno_operations.c:1469/1488). The "DORMANT ... not reached at runtime until ... Phase 8+" claim describes a state that no longer exists; comments must describe current behavior. Remove the stale DORMANT/Phase-8 note.
📄 src/include/access/amapi.h (L228-L229)
Contract drift: this says the clear_mark==false case does "kill it (LP_DEAD)", but the actual implementation (btundomark in nbtree.c) performs a physical, WAL-logged delete and explicitly documents that an LP_DEAD hint here would be a correctness bug (unbounded garbage that breaks the heapkeyspace ordering invariant, since LP_DEAD is only a hint and can be lost). Update the callback contract to say the entry is physically deleted, not LP_DEAD-marked, so implementers do not follow the wrong contract.
| flux_pbu.o \ | ||
| flux_pvs.o | ||
|
|
||
| include $(top_srcdir)/src/backend/common.mk No newline at end of file |
There was a problem hiding this comment.
Missing trailing newline at end of file. Every other backend Makefile (including the sibling recno/Makefile added in this same change) ends with a newline after the final include. POSIX defines a text file as a sequence of lines each terminated by a newline; a missing final newline is a patch-hygiene defect that shows up under git diff --check and pgindent conventions. Add a newline after the last line.
Confidence: high.
| include $(top_srcdir)/src/backend/common.mk | |
| +include $(top_srcdir)/src/backend/common.mk |
| (github.event_name == 'issue_comment' && github.event.issue.pull_request && | ||
| (startsWith(github.event.comment.body, '/open-code-review') || | ||
| startsWith(github.event.comment.body, '@open-code-review') || | ||
| startsWith(github.event.comment.body, '/pg-history'))) |
There was a problem hiding this comment.
The comment-trigger gate checks only startsWith(github.event.comment.body, ...) with no restriction on the commenter's author_association. Combined with the issue_comment trigger (which runs in the base-repo context with access to repo secrets and the AWS OIDC role), any user who comments the trigger keyword on a PR can invoke the reusable reviewer against arbitrary PR-head content. If gburd/ci-workflows/.../ocr-review.yml@v1 checks out and processes/executes PR-head code, this is a pull_request_target-style privilege-escalation footgun. Add an author-association gate, e.g. github.event.comment.author_association == 'OWNER' || ... == 'MEMBER' || ... == 'COLLABORATOR'. (Note: the reusable workflow lives in an external repo and could not be inspected here to confirm whether it checks out untrusted head code — high confidence on the missing author gate, moderate on the exploitability which depends on the callee.)
| on: | ||
| pull_request: |
There was a problem hiding this comment.
No top-level or job-level permissions: block is declared, so this caller inherits the repository's default token scope, which may be broad (including write). The review job only needs to post review comments. Declare least-privilege permissions explicitly, e.g. permissions: { contents: read, pull-requests: write }. Note: for reusable-workflow callers, permissions declared here bound (intersect with) what the called workflow's GITHUB_TOKEN can do.
| on: | |
| pull_request: | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| on: | |
| pull_request: |
| (startsWith(github.event.comment.body, '/open-code-review') || | ||
| startsWith(github.event.comment.body, '@open-code-review') || | ||
| startsWith(github.event.comment.body, '/pg-history'))) | ||
| uses: gburd/ci-workflows/.github/workflows/ocr-review.yml@v1 |
There was a problem hiding this comment.
The reusable workflow is pinned to a mutable tag @v1 rather than a commit SHA. Since this workflow runs with repository secrets and the AWS OIDC role, a re-pointed v1 tag on gburd/ci-workflows is a supply-chain risk. Pin to a full commit SHA (a comment can note the tag it corresponds to).
|
|
||
| on: | ||
| pull_request: | ||
| # Note: no draft filter — drafts are reviewed too. |
There was a problem hiding this comment.
ASCII-only is required in source per the project's contribution standards, but this comment uses a non-ASCII em-dash (U+2014). Replace with an ASCII hyphen/dash.
| # Note: no draft filter — drafts are reviewed too. | |
| # Note: no draft filter -- drafts are reviewed too. |
| UNDO substrate FLUX relies on, see src/backend/access/undo/README (the | ||
| cluster-wide UNDO engine) and src/include/access/relundo.h (the per-relation | ||
| UNDO fork). The zheap origin design notes are in DESIGN. |
There was a problem hiding this comment.
Stale/superseded design reference. There is no src/include/access/relundo.h in the tree, and no RELUNDO_FORKNUM or RelUndo symbol exists anywhere. The actual FLUX implementation stores before-images through the per-backend UNDO engine (src/backend/access/undo/perbackend, via PrepareUndoInsert/UndoFetchRecord, UndoRecPtr), as documented in flux_pbu.c and PHASE8_FLUX_PERBACKEND_DESIGN.md. This paragraph documents a design that was replaced; point at the real substrate and header.
| version is pushed to the relation's per-relation UNDO fork (RelUndo, | ||
| RELUNDO_FORKNUM) via the tuple header's version pointer (t_verptr). A |
There was a problem hiding this comment.
The "per-relation UNDO fork (RelUndo, RELUNDO_FORKNUM)" does not exist. No RELUNDO_FORKNUM fork or RelUndo type is present in the change set; the before-image is pushed to the per-backend UNDO engine and t_verptr holds an UndoRecPtr (see flux_pbu.c). Rewrite to reference the per-backend UNDO engine actually used.
| * UNDO-based rollback. ROLLBACK (and error/abort) replays the RelUndo chain | ||
| backward, restoring before-images in place (relundo_apply.c). There are no |
There was a problem hiding this comment.
relundo_apply.c does not exist in the change set. The FLUX UNDO-apply/rollback path is in flux_undo.c (flux_undo_apply, reached via GetUndoRmgr(UNDO_RMID_FLUX) on abort), per flux_pbu.c. This citation is inaccurate.
| flux_undo.c UNDO_RMID_FLUX resource manager (index-UNDO piggyback) | ||
| flux_relundo.c per-relation UNDO fork glue |
There was a problem hiding this comment.
Source-layout list is inaccurate: flux_relundo.c does not exist in src/backend/access/flux/. The actual per-backend UNDO shim file flux_pbu.c (present in the directory and Makefile/meson.build) is missing from this list. Replace the fictional entry with the real file.
| flux_undo.c UNDO_RMID_FLUX resource manager (index-UNDO piggyback) | |
| flux_relundo.c per-relation UNDO fork glue | |
| flux_undo.c UNDO_RMID_FLUX resource manager (index-UNDO piggyback) | |
| flux_pbu.c per-backend UNDO shim (insert / fetch / redo side) |
| flux_operations.c INSERT / DELETE / UPDATE (in-place + out-of-place), VACUUM | ||
| flux_tuple.c tuple form/deform, flux_toast_tuple / flux_toast_delete | ||
| flux_mvcc.c heap-shaped xmin/xmax visibility state, shmem | ||
| flux_pvs.c prior-version reconstruction from the UNDO fork |
There was a problem hiding this comment.
flux_pvs.c reconstructs prior versions from the per-backend UNDO chain (walking t_verptr), not from a "per-relation UNDO fork". Align this description with the actual per-backend UNDO substrate.
There was a problem hiding this comment.
🔍 OCR found 272 issue(s).
- 25 inline, 247 in summary (inline capped at 25)
⚠️ 1 warning(s) during review
📄 src/backend/access/flux/flux_operations.c
On-disk/cross-recovery format depends on struct padding and enum width. FluxTwoPhaseRecord is passed verbatim to RegisterTwoPhaseRecord, which save_state_data(data, len) writes byte-for-byte into the 2PC state file (persisted, survives crash/restart). The struct mixes Oid, ItemPointerData, bool, and SLogOpType -- and SLogOpType is a plain C enum (implementation-defined width) with implicit trailing/inter-field padding. Assert(len == sizeof(...)) validates nothing across compilers/ABIs. A prepared xact written by one build and resolved by another (recompiled server, different flags) can misread op_type. Serialize the fields explicitly with fixed-width integers, or store op_type as a fixed uint8/int32. Confidence: high.
📄 src/backend/access/flux/flux_fsm.c (L193-L193)
Performance: FreeSpaceMapVacuumRange() walks the FSM tree from the root on every single-page relation extension, i.e. once per new page during a bulk INSERT (the hot path). Heap deliberately avoids this in RelationGetBufferForTuple() by returning the just-extended buffer directly and letting the caller use it without a round-trip through the FSM upper levels. Here the extended block is returned and immediately re-read by the caller anyway, so the upper-level propagation is only needed for future inserts -- doing it synchronously on the extending backend's hot path is avoidable overhead. Consider dropping this or deferring FSM upper-level maintenance to vacuum, as heap does.
📄 src/backend/access/flux/flux_fsm.c (L133-L133)
Footgun: 'needed' is forwarded verbatim to GetPageWithFreeSpace(). If a caller ever passes a value > MaxFSMRequestSize (== MaxHeapTupleSize), fsm_space_needed_to_cat() throws elog(ERROR, "invalid FSM request size") and turns a legal INSERT into a hard ERROR. The FluxTargetFreeSpace clamp that prevents this lives entirely outside this function, so correctness depends on every caller pre-clamping -- and callers such as the speculative-insert path pass tuple_size directly. Clamp defensively here (e.g. Min(needed, (Size) FLUX_MAX_TUPLE_SIZE)) so the guard is co-located with the risk.
📄 src/backend/access/flux/flux_handler.c (L526-L528)
[medium confidence] rs_vistuples is allocated in TopMemoryContext (session lifetime) and freed only in flux_scan_end(). If the scan is torn down by an error (ereport/elog(ERROR)) before scan_end runs, this MaxOffsetNumber*sizeof(OffsetNumber) block is not reclaimed by any MemoryContext reset or ResourceOwner, so it leaks for the whole session -- repeated across every erroring scan. Per-scan state should live in a scan- or query-scoped context (e.g. CurrentMemoryContext at scan_begin, or a dedicated context deleted in scan_end) so it is reclaimed on error. The same applies to the bitmap-path allocation below.
📄 src/backend/access/flux/flux_handler.c (L1241-L1243)
[medium confidence] Same TopMemoryContext / error-leak concern as flux_prepare_pagescan: this per-scan bitmap vistuples buffer is freed only in flux_scan_end and leaks for the session if the scan errors out beforehand. Use a scan/query-scoped context.
📄 src/backend/access/flux/flux_handler.c (L2687-L2689)
[high confidence] The else branch writes tuple_hdr->t_flags directly (a bare non-atomic RMW into the shared buffer page) while only BUFFER_LOCK_SHARE is held. That is not torn-write / concurrency safe -- clearing a hint bit on a page under a shared lock must go through BufferSetHintBits16 (as the if branch does), which coordinates with concurrent I/O and marks the buffer dirty-hint. In flux_tuple_satisfies_snapshot 'buffer' is always valid here (freshly ReadBuffer'd and share-locked, never released above), so this else is dead code today, but it is a latent corruption hazard should the invariant ever change. Drop the unsafe fallback and always use BufferSetHintBits16.
📄 src/backend/access/flux/flux_handler.c (L3768-L3769)
[high confidence] Committed source must not reference an internal triage document. Remove the reference to flux_default_triage.md (and the 'KNOWN-OPEN note on fk-crosstype-recheck') -- the comment should state the invariant the code relies on, not point at an out-of-tree work document that will not exist in the tree.
📄 src/backend/access/flux/flux_handler.c (L689-L689)
[high confidence] PostgreSQL source must be ASCII-only. These comments contain non-ASCII characters (em-dash and right-arrow). Replace with ASCII ('--' or '->' / plain hyphen). Other occurrences exist at lines ~1984, 2056, 2083, 2100, 2104, 2402, 2867, 4218; run a tree-wide check and convert all of them.
📄 src/backend/access/flux/flux_lock.c (L53-L59)
PREPARE (2PC) leaks the serialization-lock tracking set. FluxXactCallback (flux_mvcc.c) handles XACT_EVENT_PREPARE by only doing MyFluxXactState = NULL; it never calls FluxForgetSerializeLocks(). At PREPARE, PrepareTransaction() transfers the LOCKTAG_TUPLE locks to the prepared xact's dummy PGPROC via PostPrepare_Locks() and then runs the XACT_EVENT_PREPARE callback, so the backend-local flux_serlocks[] entries survive into the backend's next transaction. Because the LOCALLOCK entries were moved to the dummy proc, LockHeldByMe() returns false on the next commit, so FluxReleaseSerializeLocks() silently skips them, but flux_serlock_n stays non-zero until an unrelated later COMMIT/ABORT clears it -- a cross-transaction state leak of freed lock references. The PREPARE path must clear the tracking set (e.g. call FluxForgetSerializeLocks() in the XACT_EVENT_PREPARE case) since the locks are now owned by the prepared xact, not this backend. (high confidence)
📄 src/backend/access/flux/flux_lock.c (L469-L482)
FluxHoldsTupleLock uses a LockTupleMode->LOCKMODE mapping that diverges from FluxLockTuple/FluxUnlockTuple. Acquisition maps KeyShare->AccessShareLock, Share->RowShareLock, NoKeyExclusive->ExclusiveLock, Exclusive->AccessExclusiveLock (matching heap's tupleLockExtraInfo), but here KeyShare/Share->ShareLock and NoKeyExclusive/Exclusive->ExclusiveLock. A lock taken as LockTupleExclusive (AccessExclusiveLock) or either share mode will never be reported held when queried, so this function returns wrong answers. It is currently dead code (no callers anywhere in the tree), so the safest fix is to delete it; if it is kept, it must use the exact same 4-way mapping as FluxLockTuple. (high confidence)
📄 src/backend/access/flux/flux_lock.c (L381-L384)
Dead code: FluxLockMultipleTuples, FluxLockPage, FluxUnlockPage, FluxLockRelationForDDL, and FluxHoldsTupleLock have no callers anywhere in the tree (only declarations in flux.h and a redundant re-extern in flux_operations.c). Per the minimalism/YAGNI discipline, remove speculative scaffolding not wired to any caller. If FluxLockPage/FluxUnlockPage were ever used, note that LOCKTAG_PAGE heavyweight locks introduce a second lock namespace whose ordering vs. buffer content locks and the LOCKTAG_TUPLE serialization locks is undocumented -- a deadlock hazard -- so they should not be added without a documented global lock ordering. (high confidence)
📄 src/backend/access/flux/flux_lock.c (L388-L391)
If this function is kept, the wait=true path is not error-safe: FluxLockTuple with wait=true calls LockAcquire(dontWait=false), which can ereport(ERROR) on deadlock or lock-timeout. On such an error the palloc0'd locked array leaks (no PG_TRY/PG_FINALLY) and any tuple locks already acquired in earlier loop iterations are not released here. It relies entirely on transaction-abort cleanup. Also the bubble sort is O(ntids^2) with no bound on ntids. (medium confidence)
📄 src/backend/access/flux/flux_lock.c (L154-L154)
Non-ASCII character in a comment: this line contains a U+200B zero-width space between "already/" and "will", violating the ASCII-only source rule. Remove it (and the stray slash reads awkwardly -- prefer "has already released or will release").
📄 src/backend/access/flux/flux_lock.c (L74-L75)
Redundant/aspirational comment text: "Returns nothing." adds nothing on a void function and should be dropped; comments should explain why, not restate the signature.
📄 src/backend/access/flux/flux_lock.c (L207-L223)
DRY: the LockTupleMode->LOCKMODE switch is duplicated verbatim in FluxLockTuple and FluxUnlockTuple, and a third divergent copy exists in FluxHoldsTupleLock. This copy-paste is the direct cause of the mapping-drift bug above. Extract a single static helper (e.g. fluxTupleLockmode(LockTupleMode)) so the mappings cannot diverge, mirroring how heap centralizes this via tupleLockExtraInfo. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L278-L286)
Dead code: FluxMvccShmemInit() has no caller. The live shmem-init path is FluxMvccShmemCallbacks (FluxMvccShmemRequest/FluxMvccShmemInit_cb), wired via PG_SHMEM_SUBSYSTEM(FluxMvccShmemCallbacks) in subsystemlist.h. This classic ShmemInitStruct + on_shmem_exit path is never invoked; keeping both is duplicated init logic and a footgun (if it ever were wired alongside the subsystem path, on_shmem_exit(FluxShmemExit, ...) would be registered twice). Remove FluxMvccShmemInit() and its prototype, and keep only the subsystem callback path. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L134-L134)
Dead state: serializable_horizon is only ever written (initialized to 1, then monotonically reduced via Min() in FluxCommitTransaction()); no code path ever reads it. It has no consumer, so the mvcc_lock section that maintains it in FluxCommitTransaction() and the field itself are dead code. Remove the field, its two initializations, and the update block in FluxCommitTransaction() (which also lets you drop mvcc_lock if it has no other user). Note also that the update logic is a monotonic-shrink toward 1 that never recovers, so were it ever consumed it would be a bug. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L909-L911)
Dead code: FluxGetMvccStats() has no caller anywhere in the tree (only its definition and prototype exist). Per the minimalism/YAGNI discipline, remove it and its prototype in flux.h unless a caller is added in this patch. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L832-L836)
Dead code: FluxUpdateOldestActiveTimestamp() has no caller in the tree (only definition and prototype). The self-documenting "Also callable from VACUUM or any code that needs to force a refresh" is aspirational. Remove it and its prototype unless wired in this patch. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L149-L149)
Non-ASCII punctuation: this comment (and several others in this file, e.g. the -- just a write barrier and report — analogous lines) uses a literal em-dash (U+2014). PostgreSQL source must be ASCII-only. Replace em-dashes with -- or -. Run git diff --check / grep for non-ASCII before submitting. (high confidence)
💡 Suggested change
Before:
* no lock is needed — just a compiler barrier via volatile access.
After:
* no lock is needed -- just a compiler barrier via volatile access.
📄 src/backend/access/flux/flux_mvcc.c (L898-L902)
Cache-invalidation TOCTOU: current_gen is read (line 873) before the slot rescan (884-890). A concurrent backend that starts/ends a transaction between this read and the end of the scan writes a slot and bumps oldest_active_generation after current_gen was sampled. This function then stamps my_oldest_active_gen = current_gen with the now-stale value, so the next call takes the fast path and returns a stale horizon that missed the concurrent change. Sample the generation after the scan and cache that, or re-check it and retry on mismatch (cf. GetSnapshotData recheck patterns). Impact is bounded here because the consumers are a cosmetic WAL commit-ts word and the rebuildable VM all-visible hint rather than the XID reclamation gate, but the cache is still incorrect. (moderate confidence)
📄 src/backend/access/flux/flux_lock.c (L82-L87)
The dedup scan makes serialization-lock tracking O(n^2). FluxTrackSerializeLock is called once per updated row (flux_operations.c:3143), and each call does a linear scan over all previously tracked entries. A single transaction updating N distinct rows therefore costs O(N^2) on the update hot path -- heap avoids this entirely by carrying the lost-update signal in the tuple xmax. Consider a hashed lookup (simplehash/dynahash) keyed by (dbid, relnumber, blkno, offnum), or reconsider whether per-row deduplication is needed at all given the lock manager already refcounts duplicate acquisitions. Confidence: high.
📄 src/backend/access/flux/flux_lock.c (L226-L232)
FLUX keys LOCKTAG_TUPLE by rd_locator.relNumber (the RelFileNumber / relfilenode), whereas the rest of the system (lmgr.c LockTuple, heap via tupleLockExtraInfo) keys tuple locks by rd_lockInfo.lockRelId.relId (the stable relation OID). The relfilenumber changes on rewrite (VACUUM FULL, CLUSTER, TRUNCATE, ALTER TABLE ... SET TABLESPACE), so a transaction-duration lock keyed by relfilenumber is a footgun and diverges from the shared LOCKTAG_TUPLE convention. Use the relation OID here (and consistently in FluxUnlockTuple/FluxHoldsTupleLock/FluxReleaseSerializeLocks) to match the rest of the tree. Confidence: moderate.
📄 src/backend/access/flux/flux_mvcc.c (L1289-L1291)
Asymmetry with the xmin-self path: subtransaction-abort of our own DELETE/UPDATE is not detected here. The xmin branch (self-insert) consults the sLog for SLOG_OP_ABORTED so a tuple inserted by a rolled-back subxact is correctly hidden. This xmax branch does not. When our own transaction deletes/updates a tuple inside a savepoint and then ROLLBACK TO that savepoint, the sLog op is flipped to SLOG_OP_ABORTED but t_xmax remains our TOP xid on the page (UNCOMMITTED still set). TransactionIdIsCurrentTransactionId(xmax) is true and the function returns false (row gone) even though the delete was undone -- the row should become visible again. Consult the sLog for SLOG_OP_ABORTED on the tuple's TID here, mirroring the xmin-self path. (moderate-high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L787-L789)
Dead code: FluxGetSnapshotTimestamp() has no caller anywhere in the tree (only this definition and its prototype in flux.h). Under the YAGNI/minimalism discipline, remove it and its prototype unless a caller is added in this patch. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L818-L821)
Dead code: FluxTupleVisibleToSnapshot() has no caller anywhere in the tree (only this definition and its prototype in flux.h); it is a thin one-line forwarder to FluxTupleVisibleToSnapshotDual(), which every read site already calls directly. Remove it and its prototype. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L527-L532)
The write barrier is on the wrong side of the store. pg_write_barrier() before the slot write orders prior writes ahead of this store; to publish the timestamp to a concurrent reader (VACUUM) the barrier must come after the store (write value, then write-barrier), matching the read-barrier-then-read pattern in FluxGetOldestActiveTimestamp. As written, the barrier here does not guarantee the slot value is observed before the subsequent active_xact_count increment. (moderate confidence)
💡 Suggested change
Before:
if (my_slot >= 0 && my_slot < FluxMvccShmem->num_xact_slots)
{
pg_write_barrier();
FluxMvccShmem->xact_start_ts_slots[my_slot] =
MyFluxXactState->xact_start_ts;
}
After:
if (my_slot >= 0 && my_slot < FluxMvccShmem->num_xact_slots)
{
FluxMvccShmem->xact_start_ts_slots[my_slot] =
MyFluxXactState->xact_start_ts;
pg_write_barrier();
}
📄 src/backend/access/flux/flux_mvcc.c (L717-L728)
SSI conflict-out misses concurrently-COMMITTED writers, breaking SERIALIZABLE. flux_header_dirty_xid() deliberately returns InvalidTransactionId for any writer that is not TransactionIdIsInProgress (see its definition in flux_operations.c: it filters on TransactionIdIsInProgress(xid)). But the heap equivalent, HeapCheckForSerializableConflictOut, derives the conflict xid from the tuple header for LIVE / RECENTLY_DEAD / DELETE_IN_PROGRESS / INSERT_IN_PROGRESS tuples, bounded only by TransactionXmin -- it explicitly reports conflicts against writers that already COMMITTED but committed after (are still invisible to) the reader's snapshot. That committed-concurrent-writer case is precisely what rw-antidependency detection exists for. Here, such a writer yields xid == Invalid, the function returns at the !TransactionIdIsValid(xid) branch claiming the tx "committed so long ago," and no conflict is registered -- a missed SIREAD conflict that can let a serialization anomaly commit. Derive xid from t_xmin / t_xmax directly (like heap) and gate only on TransactionIdPrecedes(xid, TransactionXmin), not on in-progress-ness. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L185-L185)
Dead field: is_read_only is only ever initialized to true (line 515) and never read, and never set to false despite the comment "Until first write." No write path flips it and no reader consults it. Remove the field and its initialization. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L775-L782)
Dead code: FluxAbortTransaction() has no caller in the tree (only this definition and its prototype in flux.h). The abort path in FluxXactCallback calls FluxCleanupTransactionState() directly, not this wrapper. Remove it and its prototype. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L1103-L1103)
Dead field: epq_reconcile_verptr is write-only -- assigned here but never read. FluxEpqReconcileMatches() explicitly ignores head_verptr (matching on relid/tid/cid only, by design). Remove this field and the assignment; the head_verptr parameter of FluxEpqReconcileMark is then also unused for storage. (high confidence)
📄 src/backend/access/flux/flux_handler.c (L3593-L3593)
WAL/crash-consistency hazard: tuple_hdr->t_flags |= FLUX_TUPLE_LOCKED; modifies the shared buffer page BEFORE START_CRIT_SECTION(), and SLogTupleInsert() (which runs between them) can ereport(ERROR) (e.g. palloc/lappend OOM in SLogTupleTrackKey/SLogTupleTrackLocalOnly). If it throws, the page carries the LOCKED bit but was never MarkBufferDirty()'d nor WAL-logged, so redo/a later flush of the same page can diverge from WAL. Per PostgreSQL discipline, a shared-page modification must happen inside the critical section, atomically with MarkBufferDirty + XLogInsert. Move the flag write into the critical section (after START_CRIT_SECTION, before MarkBufferDirty); the sLog marker registration correctly stays before the crit section. The nearby comment claiming the flag and marker are set "atomically" is inaccurate.
📄 src/backend/access/flux/flux_handler.c (L1505-L1507)
Hot-path performance regression: flux_index_entry_key_mismatch() is called for EVERY tuple returned by an index scan on a FLUX table (all FLUX tables set am_index_delete_marking and xs_want_itup_recheck, see flux_index_scan_begin / flux_index_getnext_slot). It rebuilds IndexInfo via BuildIndexInfo() and allocates/frees a full EState (CreateExecutorState/FreeExecutorState) per tuple. BuildIndexInfo does catalog work (index expressions/predicate). Build the IndexInfo and per-scan EState once (cache them in IndexScanFluxData, tear down in flux_index_scan_end) instead of per tuple, and reset the per-tuple memory context between rows.
📄 src/backend/access/flux/flux_operations.c (L392-L393)
Return-type/contract mismatch. FluxPagePruneOpt is declared int (flux.h:742) and this header states "Returns the number of tuples pruned", but the success path returns the boolean literal true (== 1) after pruning ndead tuples, discarding the real count, while other paths return 0. Any future caller relying on the documented "count" semantics will misbehave; the current callers only use it as a bool. Either return ndead and keep the int/count contract, or change the declared type to bool and fix the comment. Confidence: high.
📄 src/backend/access/flux/flux_operations.c (L891-L891)
Double-release hazard on the error paths. Both cleanup loops here iterate overflow_buffers.buffers[] and unconditionally UnlockReleaseBuffer() each entry, unlike the success-path cleanup which explicitly dedups because "an overflow buffer might be the SAME ... as another overflow buffer (when overflow data is placed on the same page)". flux_tuple_insert calls FluxFormTuple(relation, &overflow_buffers), so the overflow path is reachable and two overflow columns can land on one page, putting the same Buffer twice in the array. On these ereport(ERROR) paths that duplicate is released twice ("refcount going negative"/double content-unlock). Apply the same dedup used on the success path. Confidence: high.
📄 src/backend/access/flux/flux_operations.c (L7129-L7134)
VACUUM never bounds the dead-item store against maintenance_work_mem. dead_items_info->max_bytes is set from maintenance_work_mem, but the Phase I scan accumulates every dead TID into a single dead_items TidStore for the whole relation and never checks TidStoreMemoryUsage(dead_items) to trigger an intermediate index-vacuum flush (as heap lazy vacuum does). On a large table with many dead tuples the store grows unbounded past the configured budget, defeating the purpose of the max_bytes setting. Add a size check that flushes indexes (Phase II) and resets the store when it fills. Confidence: high.
📄 src/backend/access/flux/flux_operations.c (L6319-L6323)
Dead code: the early if (nindexes > 0) return 0; above makes this and every other if (nindexes > 0) block in this function unreachable -- the executor-state/IndexInfo setup here, the per-tuple deferred-move copy, and the deferred index-insert replay can never run when nindexes>0, and the nindexes/indrels parameters are effectively unused. This is substantial YAGNI scaffolding (a full tuple-relocation-with-index-maintenance path that is compiled but never executes). Either wire the relocation path (and drop the early return) or delete the dead index machinery and the unused parameters. Confidence: high.
📄 src/backend/access/flux/flux_operations.c (L344-L344)
Dead state exposed as a public metric. flux_stat_out_of_place_updates is never incremented anywhere in the tree (the out-of-place path flux_update_out_of_place does not bump it), so FluxGetUpdateStats always reports 0 for it. Moreover FluxGetUpdateStats itself has no caller, so both the getter and this counter are dead flexibility. Increment it on the out-of-place path or remove the counter (and the getter) per YAGNI. Confidence: high.
📄 src/backend/access/flux/flux_operations.c (L1503-L1503)
Non-ASCII characters in comments. PostgreSQL source must be ASCII-only, but this file contains U+2192 arrows (e.g. "Own LOCK entry -> proceed", the TM_Invisible/TM_Updated comment block, the EPQ livelock numbered list) and em-dashes/typographic apostrophes throughout (e.g. "FLUXs ceiling"). Replace ->-glyphs with ASCII ->, em-dashes with --, and smart quotes with ASCII. git diff --check / a non-ASCII scan should be clean. Confidence: high.
📄 src/backend/access/flux/flux_pvs.c (L179-L184)
Visibility rule asymmetry: aborted intermediate updaters may be served as visible.
The terminal (no-verptr / INSERT) branch correctly combines commit status with the snapshot: TransactionIdDidCommit(ins_xid) && !XidInMVCCSnapshot(ins_xid, snapshot). But the intermediate FLUX_UNDO_UPDATE / FLUX_UNDO_UPDATE_DELTA steps decide "updater visible, stop" using ONLY !XidInMVCCSnapshot(urec_xid, snapshot).
XidInMVCCSnapshot returns false for BOTH committed-before-snapshot xids AND aborted xids (an aborted xid is not in the running set). So if a chain link's producing urec_xid aborted (and was not yet physically reverted by the logical-revert worker), this branch treats it as visible and serves the rolled-back image -- exposing data from an aborted transaction. The outer callers guard the on-page (newest) image via FluxTupleSatisfiesMVCC's TransactionIdDidCommit check, and this function's terminal case guards the base image, but the intermediate steps do not. The guard should be symmetric with the terminal case (add a TransactionIdDidCommit(urec_xid) component) so an aborted intermediate version is stepped past rather than served. Please confirm whether an aborted, not-yet-reverted intermediate chain link is reachable here; if it is, this is a wrong-result / uncommitted-data-exposure bug. (moderate-high confidence)
📄 src/backend/access/flux/flux_pvs.c (L96-L96)
Comment is inaccurate and a NULL-deref footgun: tid is documented as "currently unused but reserved for future diagnostics" and immediately discarded with (void) tid, but it IS dereferenced later in the depth-cap WARNING (ItemPointerGetBlockNumber/OffsetNumber(tid)) and passed to RelationGetRelid. All current callers pass a non-NULL tid, so this is not a live crash, but the "unused" claim invites a future caller to pass NULL, which would segfault on the depth-cap path. Drop the misleading (void) tid; and the "currently unused" wording, and document that tid must be non-NULL. (high confidence)
📄 src/backend/access/flux/flux_pvs.c (L203-L204)
Convention: UndoRecPtr is uint64 and this subsystem already provides UndoRecPtrFormat (see pbu_undolog.c) and uses UINT64_FORMAT (flux_pbu.c line 453). Printing verptr as (unsigned long long) verptr with %llu is inconsistent with that convention; prefer UINT64_FORMAT (or UndoRecPtrFormat) here and in the other WARNING messages for portability/consistency. (low confidence)
📄 src/backend/access/flux/flux_pbu.c (L84-L88)
[high confidence] The whole write protocol is guarded only by flux_pbu_prepared plus Assert(!flux_pbu_prepared), and there is no transaction-abort hook that resets it. FluxPbuPrepareInsert() runs BEFORE the caller's critical section, and the callers can ereport(ERROR) between prepare and finish:
- UPDATE (flux_operations.c ~5540) calls
FluxPbuPrepareInsert()inside aPG_TRY, but itsPG_CATCHonly callsflux_release_update_overflow_buffers()+PG_RE_THROW()-- it does NOT cancel the prepared undo, and the followingFluxXLogPrepareLogicalImage()can throw on OOM. - DELETE (flux_operations.c ~1860) prepares, then calls
GetCurrentTransactionId()(which can palloc / take locks / throw) beforeSTART_CRIT_SECTION().
On such an ERROR, PbuAtAbort_ApplyUndo() handles the undo chain and the buffer manager's ResourceOwner releases the pinned undo buffers, but flux_pbu_prepared stays true and flux_pbu_uur.uur_payload is never freed. The next DML op then either fires Assert(!flux_pbu_prepared) (assert build) or, in production where the assert is compiled out, initStringInfo() overwrites the stale payload pointer and the module state is silently corrupted. Register an xact/subxact abort callback (or reset within PbuAtAbort_ApplyUndo) that clears flux_pbu_prepared and frees the payload, so an ERROR between prepare and finish/cancel cannot poison the next operation.
📄 src/backend/access/flux/flux_pbu.c (L80-L82)
[high confidence] This "runs to completion with no nesting, no re-entrancy" claim ignores the error path: FluxPbuPrepareInsert() runs before the critical section and its callers can throw before FluxPbuFinishUndo()/FluxPbuCancelUndo() are reached (see the UPDATE PG_CATCH and the DELETE GetCurrentTransactionId() call). The single-static design therefore does NOT run to completion on error, and nothing resets the state on abort. The comment overstates the invariant the code actually maintains.
📄 src/backend/access/flux/flux_pbu.c (L290-L293)
[high confidence] FluxPbuCancelUndo() is dead code: code_search finds no caller anywhere in src/backend/access/flux. Its header comment claims it is "Used when the DML op decides not to commit the reserved record (e.g. a same-size CAS update that finds no byte actually changed)", but the CAS path in flux_operations.c (~3592) explicitly documents the opposite: "We prepare the UNDO record exactly once ... so there is no cancel + re-prepare (that would double-advance the undo log's insert pointer)." Per YAGNI, either wire this into the actual error-recovery path (it is exactly what the abort cleanup in the previous comment needs) or remove it along with its misleading documentation.
📄 src/backend/access/flux/flux_pbu.c (L8-L8)
[high confidence] fileops_pbu.c does not exist (the sibling file is src/backend/storage/file/fileops_undo.c). This stale name appears three times (here, and "mirroring the FILEOPS crash-safe choreography" / "identical to the FILEOPS no-op path" below). The comparison is also technically wrong: FILEOPS forces full-page images via RegisterUndoLogBuffersForImage(), whereas this file re-runs the undo insert via RegisterUndoLogBuffersWithData() -- the opposite redo strategy. Fix the file name and drop the "identical/mirroring FILEOPS" framing so it does not mislead maintainers.
💡 Suggested change
Before:
* translation unit is the small "pbu shim" (mirroring fileops_pbu.c): it is
After:
* translation unit is the small "pbu shim" (compare fileops_undo.c): it is
📄 src/backend/access/flux/flux_pbu.c (L711-L716)
[medium confidence] During a SAVEPOINT rollback IsAbortedTransactionBlockState() is false, so flux_undo_apply() does NOT early-skip and proceeds to try_relation_open() / ReadBuffer() / buffer locking, any of which can ereport(ERROR). If rm_undo throws here, urec (palloc'd by UndoFetchRecord) and its pinned uur_buffer are only reclaimed later by memory-context/ResourceOwner cleanup -- the PG_FINALLY block resets flux_pbu_in_subxact_apply but does not UndoRecordRelease(urec). The normal loop path releases them explicitly; the error path should be consistent (release the fetched record/buffer in the FINALLY, or track them so the FINALLY can). Not a hard leak given ResourceOwner cleanup on the resulting abort, but the asymmetric cleanup is fragile.
📄 src/backend/access/flux/flux_pbu.c (L228-L228)
[low confidence] uur_payload.len is an int and is narrowed to the uint16 payload_len. Today FLUX before-images are on-page tuples (ItemIdGetLength() / on-page t_len), bounded well under 64 KB, so this cannot overflow in practice. But there is no guard: if a future before-image ever exceeds 65535-SizeOfFluxUndoPayloadHeader bytes, this silently truncates and FluxPbuRedoUndo()'s datalen != SizeOfFluxUndo + payload_len check will PANIC (or worse, reconstruct a wrong-length payload). Add an Assert(flux_pbu_uur.uur_payload.len <= PG_UINT16_MAX) (or an explicit ereport) to make the bound enforced rather than assumed.
📄 src/backend/access/flux/flux_operations.c (L1999-L2000)
Stale comment: "The tracked key was just created by SLogTupleInsert above" no longer matches the code. The BUG 5 refactor (see the two comments immediately above) removed the per-tuple sLog INSERT for UPDATE/DELETE ("UPDATE/DELETE keep NO per-tuple tracking at all"), so there is no SLogTupleInsert call above this point. The comment describes behavior that was deleted; drop the second sentence.
📄 src/backend/access/flux/flux_operations.c (L5970-L5970)
100%% in a plain C comment. The doubled percent is only meaningful inside a printf-style format string; in an ordinary comment it renders as a stray extra %. Use 100%.
📄 src/backend/access/flux/flux_pvs.c (L226-L231)
The reconstructed prior image (next_image) is re-interpreted as a FluxTupleHeader on the next loop iteration (hdr = (const FluxTupleHeader *) candidate), and its t_verptr (offset 8) and t_xmin (offset 20) are read via FluxTupleGetVersionPtr/FluxTupleGetXmin. Neither the FLUX_UNDO_UPDATE path (next_len = phdr.tuple_len) nor the FLUX_UNDO_UPDATE_DELTA path (next_len = delta->old_len) validates that next_len >= sizeof(FluxTupleHeader). A corrupted/truncated undo record with a small tuple_len/old_len (payload bounds are still satisfied) yields a candidate shorter than the header, and the next iteration reads past the palloc'd buffer. This directly undermines the stated goal of defending against corrupted records (the depth cap only guards against infinite loops). Add a next_len >= sizeof(FluxTupleHeader) guard before accepting the reconstructed image.
💡 Suggested change
Before:
next_len = phdr.tuple_len;
next_image = (char *) palloc(next_len);
memcpy(next_image,
payload + SizeOfFluxUndoPayloadHeader,
next_len);
stepped = true;
After:
if (phdr.tuple_len < sizeof(FluxTupleHeader))
{
elog(WARNING,
"FLUX PVS: FLUX_UNDO_UPDATE tuple_len %u smaller than tuple header at " UINT64_FORMAT,
phdr.tuple_len, verptr);
break;
}
next_len = phdr.tuple_len;
next_image = (char *) palloc(next_len);
memcpy(next_image,
payload + SizeOfFluxUndoPayloadHeader,
next_len);
stepped = true;
📄 src/backend/access/flux/flux_pbu.c (L378-L380)
[high confidence] The redo search window is too small and silently misses the undo region for records with many overflow pages. All FLUX redo callers invoke FluxPbuRedoUndo(record, 1) with first_block_id == 1 (flux_xlog.c lines 1455, 1981, 2048, 2561), so this loop scans only block ids 1..4 (first_block_id + FLUX_PBU_MAX_UNDO_BLOCKS, with FLUX_PBU_MAX_UNDO_BLOCKS == 4).
But on the write side the undo pages are folded in after all distinct overflow blocks: flux_undo_block_id = registered_block_id where registered_block_id starts at 1 and is incremented once per distinct overflow page (flux_xlog.c:210,263,296), and MAX_OVERFLOW_BUFFERS == 32. So an INSERT/UPDATE that spills to 4 or more distinct overflow pages registers its undo block at id >= 5, outside this window. The overflow block ids 1..N are contiguous (no gap that would trip the break), so the loop continues past all overflow blocks, hits the window limit at id 4, and returns with data == NULL -- the undo insert is never re-run during redo.
Consequence: the undo log's insert pointer is not advanced during recovery, so every later record in that log lands at the wrong offset (the exact failure the PANIC guards on urec_ptr mismatch describe), and the crashed transaction's before-image is lost -- rollback/recovery cannot undo it. Either pass the true flux_undo_block_id into FluxPbuRedoUndo() (mirroring the DO side) instead of the hard-coded 1, or scan up to XLR_MAX_BLOCK_ID rather than a fixed 4-wide window.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L54-L60)
These frontend-local record copies do not match the authoritative xl_flux_insert in flux_xlog.h, which is {OffsetNumber offnum; uint16 flags; uint32 tuple_len; uint64 commit_ts} and whose write path (FluxXLogInsert) never emits an xact_ts field. xl_flux_insert_fe drops tuple_len and invents a trailing xact_ts: the memcpy in flux_desc() reads commit_ts from the right offset by luck (bytes 8-15), but then reads the first 8 bytes of the WAL-logged tuple body as a bogus xact_ts. Same problem for xl_flux_delete_fe and xl_flux_update_fe (both invent xact_ts and drop tuple_len). Result: pg_waldump prints garbage timestamps for every FLUX INSERT/DELETE/UPDATE record. Mirror the real structs (including tuple_len) and drop xact_ts.
💡 Suggested change
Before:
typedef struct xl_flux_insert_fe
{
uint16 offnum;
uint16 flags;
uint64 commit_ts;
uint64 xact_ts;
} xl_flux_insert_fe;
After:
typedef struct xl_flux_insert_fe
{
uint16 offnum;
uint16 flags;
uint32 tuple_len;
uint64 commit_ts;
} xl_flux_insert_fe;
📄 src/backend/access/rmgrdesc/fluxdesc.c (L140-L148)
xl_flux_lock_fe diverges from the authoritative xl_flux_lock in flux_xlog.h, which is {OffsetNumber offnum; uint16 flags; uint8 infomask; uint8 lock_mode} (no xmax, no infomask2, and infomask is uint8 not uint16). flux_desc() memcpy's the raw WAL bytes into this larger struct and then prints xmax/infomask2, so the LOCK case reads past the real record and mislabels every field. The sizeof(xl_flux_lock_fe) length check will also spuriously report lock (truncated) for a correctly-sized record. Mirror the real 6-byte layout.
💡 Suggested change
Before:
typedef struct xl_flux_lock_fe
{
uint16 offnum;
uint16 flags;
uint32 xmax;
uint16 infomask;
uint16 infomask2;
uint8 lock_mode;
} xl_flux_lock_fe;
After:
typedef struct xl_flux_lock_fe
{
uint16 offnum;
uint16 flags;
uint8 infomask;
uint8 lock_mode;
} xl_flux_lock_fe;
📄 src/backend/access/rmgrdesc/fluxdesc.c (L196-L202)
xact_ts does not exist in any FLUX WAL record emitted by flux_xlog.c; printing it here dumps the first bytes of the tuple body as a fake timestamp. Once the struct is corrected to include tuple_len and drop xact_ts, print tuple_len instead (the actual field). Same for the DELETE and UPDATE branches.
💡 Suggested change
Before:
memcpy(&xlrec, data, sizeof(xl_flux_insert_fe));
flags = xlrec.flags;
appendStringInfo(buf, "off: %u, flags: 0x%04X, "
"commit_ts: " UINT64_FORMAT ", "
"xact_ts: " UINT64_FORMAT,
xlrec.offnum, xlrec.flags,
xlrec.commit_ts, xlrec.xact_ts);
After:
memcpy(&xlrec, data, sizeof(xl_flux_insert_fe));
flags = xlrec.flags;
appendStringInfo(buf, "off: %u, flags: 0x%04X, "
"tuple_len: %u, "
"commit_ts: " UINT64_FORMAT,
xlrec.offnum, xlrec.flags,
xlrec.tuple_len, xlrec.commit_ts);
📄 src/backend/access/rmgrdesc/fluxdesc.c (L48-L53)
flux_xlog.h already provides a full #ifdef FRONTEND block with frontend-safe versions of these exact structs (xl_flux_insert, xl_flux_delete, xl_flux_update, xl_flux_multi_insert, xl_flux_compress, ...), which is the convention other rmgrdesc files follow (this file already #includes access/xlog headers). Re-declaring a second, divergent private copy here is what produced the field mismatches above; the "keep in sync" comments concede the fragility. Include flux_xlog.h and use the shared FRONTEND structs instead of maintaining a parallel _fe set.
📄 src/include/access/flux.h (L226-L228)
Casting the address of a plain uint32 disk field to pg_atomic_uint32 * and running pg_atomic_compare_exchange_u32 on it relies on pg_atomic_uint32 being layout-identical to a bare uint32. This holds on all currently-supported platforms (PostgreSQL mandates native 32-bit atomics, so pg_atomic_uint32 is { volatile uint32 value; }), and t_writer at header offset 16 is 4-byte aligned on a MAXALIGN'd tuple, so the CAS is well-defined in practice. It is nonetheless strict-aliasing UB and contrary to PG's own guidance (embed a pg_atomic_uint32 field rather than alias an int). Low risk given the constraints, but worth a note; consider declaring t_writer as pg_atomic_uint32 (it is still zero-initialized by palloc0/memset, and its on-disk footprint is unchanged).
📄 src/include/access/flux.h (L16-L18)
Header files must never #include "postgres.h" -- that is the .c file's job (it must be the first include in every translation unit). No other src/include/access/*.h does this (heapam.h, nbtree.h, etc. do not). Including postgres.h from a header breaks the convention and can cause double/ordering issues when the header is pulled into a FRONTEND or extension build. Drop this include and rely on the .c files to include postgres.h first.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/heapam.h"
After:
#include "access/heapam.h"
📄 src/include/access/flux_xlog.h (L16-L18)
Same convention violation as flux.h: a header must not #include "postgres.h". flux_xlog.h is also compiled in the FRONTEND (pg_waldump) build where postgres.h is not the right prologue (postgres_fe.h is). Remove this include; the including .c/frontend TU already establishes the correct prologue.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/xlogreader.h"
After:
#include "access/xlogreader.h"
📄 src/include/access/flux_pbu.h (L88-L88)
The comment documents the second argument as stop_exclusive, but the prototype names it stop_inclusive. For a subtransaction undo-apply boundary the exclusive/inclusive distinction determines exactly which records get reverse-applied, so this contradiction is a correctness footgun for callers. Reconcile the comment with the actual parameter semantics (name the parameter and the comment identically, and state clearly whether the stop bound is applied).
💡 Suggested change
Before:
* FluxPbuApplySubxactUndo(from, stop_exclusive) is run at
After:
* FluxPbuApplySubxactUndo(from, stop_inclusive) is run at
📄 src/backend/access/flux/flux_tuple.c (L789-L791)
FluxPageUpdateTuple mutates page contents (memcpy into the tuple slot, ItemIdSetNormal, FluxPageIndexTupleDelete, PageAddItem) but performs no WAL logging and no MarkBufferDirty, and has zero callers in the tree. As written it is dead code (YAGNI); if it were ever wired onto a live path it would be a crash-safety / replica-divergence hole because the in-place mutation is never replayed. Remove it, or if it is intended for future use, it must not ship until it WAL-logs and is exercised.
📄 src/backend/access/flux/flux_tuple.c (L326-L332)
flux_form_tuple_internal carries force_shrink, old_ovptrs and old_ovpresent parameters that are never referenced in the body -- the body comment states "FLUX has no on-page overflow" and "work_values therefore mirrors values exactly". Consequently the wrappers built on top of them are misleading: FluxFormTupleForceShrink documents that it "forces every inline varlena off-page" but produces a tuple identical to FluxFormTuple, and its only call site (flux_operations.c) is inside an if (false) block, so it is unreachable. FluxFormTupleUpdate has no callers at all. This is speculative scaffolding / dead flexibility; the unused parameters and the two unused wrappers should be dropped to match what the code actually does.
📄 src/backend/access/flux/flux_tuple.c (L518-L519)
is_compressed[] and is_overflowed[] are initialized to false and read here at the pfree loop, but are never assigned true anywhere in the function (the comment above confirms FLUX neither compresses nor overflows attributes). This loop therefore never frees anything, and the parallel has_compressed/has_overflow locals stay false too. This is dead code left over from a removed feature; remove the tracking arrays and the loop.
📄 src/backend/access/flux/flux_tuple.c (L496-L497)
Stale comment: FLUX no longer compresses attributes (see the "FLUX does not compress attributes" notes in this file and the deform path). This "(possibly compressed)" annotation describes removed behavior and should be corrected to reflect that varlena values are stored verbatim.
💡 Suggested change
Before:
/* Variable-length attribute (possibly compressed) */
Size attr_len = VARSIZE_ANY(DatumGetPointer(work_values[i]));
After:
/* Variable-length attribute (stored verbatim) */
Size attr_len = VARSIZE_ANY(data_ptr);
📄 src/backend/access/flux/flux_tuple.c (L350-L350)
Stale comment: same as above -- FLUX does not compress or overflow attributes any more, so "(possibly compressed/overflowed)" is inaccurate. Update to reflect that work_values mirrors the input values.
📄 src/backend/access/flux/flux_tuple.c (L859-L861)
FluxPageGetLiveTuples has no callers in the tree (the comment even admits "no current callers"). Dead code -- remove it rather than shipping an unexercised helper.
📄 src/backend/access/flux/flux_stats.c (L50-L52)
This entire module (FluxCollectRelationStats + FluxLogRelationStats) has no callers anywhere in the tree -- it is not wired into the ANALYZE path despite the header comment claiming it is "called during ANALYZE". As dead code it should not ship (YAGNI). Separately, if it is later wired up: it does a full O(nblocks) sequential scan with a share lock on every page on top of ANALYZE's sampling, which is a real I/O regression on large FLUX tables and duplicates work the sampling framework already does; that design needs to be justified against a benchmark before it lands.
📄 src/backend/access/flux/flux_slot.c (L858-L858)
Non-ASCII em-dash (U+2014) in source comments violates the ASCII-only rule for the tree. Replace with a plain ASCII hyphen/double-hyphen.
💡 Suggested change
Before:
/* Same buffer — just free any materialized data */
After:
/* Same buffer -- just free any materialized data */
📄 src/backend/access/flux/flux_slot.c (L868-L868)
Non-ASCII em-dash (U+2014) in source comment; use a plain ASCII hyphen/double-hyphen.
💡 Suggested change
Before:
/* Different buffer — full clear (releases old pin) and acquire new */
After:
/* Different buffer -- full clear (releases old pin) and acquire new */
📄 src/backend/access/flux/flux_xlog.c (L2291-L2291)
Non-ASCII em-dash (U+2014) in source comment violates the tree's ASCII-only rule; use a plain ASCII hyphen/double-hyphen.
💡 Suggested change
Before:
* unreachable, but if it ever fires we must not PANIC —
After:
* unreachable, but if it ever fires we must not PANIC --
📄 src/backend/access/flux/flux_stats.c (L249-L253)
PostgreSQL convention is to use INT64_FORMAT/UINT64_FORMAT for int64/uint64 rather than casting to (long long)/(unsigned long long) with %lld/%llu. The explicit long long casts here are technically portable, but the tree standard is the format macros. (Minor; also note this whole file is currently unreferenced -- see the FluxCollectRelationStats comment.)
📄 src/backend/access/recno/recno_operations.c (L3132-L3136)
Dangling if / logic defect: if (update_indexes) has no braces and only conditionally executes the following pfree(new_tuple). When update_indexes == NULL, new_tuple is leaked; when it is non-NULL, pfree(new_tuple) runs but no *update_indexes = ... is ever assigned on this path. The comment block just above claims TU_Summarizing is returned, yet no assignment exists here (the only *update_indexes writes are in the earlier hot_changed/sum_changed block, which is not always reached). A mis-set/unset update_indexes causes either missing index maintenance (index corruption / wrong results) or duplicate-key errors. This is almost certainly a missing braced block that should both assign the verdict and free the tuple.
💡 Suggested change
Before:
if (update_indexes)
pfree(new_tuple);
return TM_Ok;
}
After:
pfree(new_tuple);
return TM_Ok;
}
📄 src/backend/access/recno/recno_compress.c (L781-L784)
Buffer over-read: on-disk bytes_stored (from input[1]) is used directly as the memcpy length into a fixed 8-byte uint64 with no validation that bytes_stored <= sizeof(uint64). Corrupt or crafted compressed data (bytes_stored up to 255) overflows abs_val on the stack and also reads past the compressed buffer. Validate the length before the memcpy.
💡 Suggested change
Before:
int bytes_stored = input[1];
uint64 abs_val = 0;
memcpy(&abs_val, input + 2, bytes_stored);
After:
int bytes_stored = input[1];
uint64 abs_val = 0;
if (bytes_stored < 1 || bytes_stored > (int) sizeof(uint64))
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("invalid delta compression length %d", bytes_stored)));
memcpy(&abs_val, input + 2, bytes_stored);
📄 src/backend/access/recno/recno_compress.c (L825-L828)
Buffer overflow: stored_size is read from on-disk data (input[1]) and used as the memcpy length into VARDATA(output), which was allocated for orig_size bytes only. If stored_size > orig_size (corrupt/crafted data), this writes past the palloc'd output buffer (heap overflow). Bound stored_size against orig_size before copying.
💡 Suggested change
Before:
Size stored_size = input[1];
memcpy(VARDATA(output), input + 2, stored_size);
return PointerGetDatum(output);
After:
Size stored_size = input[1];
if (stored_size > orig_size)
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("invalid delta compression length %zu", stored_size)));
memcpy(VARDATA(output), input + 2, stored_size);
return PointerGetDatum(output);
📄 src/backend/access/recno/recno_clock.c (L367-L369)
Non-portable format specifier: error_ms is uint64 but is printed with %lu, which is wrong on platforms where unsigned long is 32-bit (e.g. Windows/MSVC LLP64) and triggers -Wformat elsewhere. Use UINT64_FORMAT. Both the FATAL and the WARNING message below have the same defect. Separately, comparing a uint64 against recno_max_clock_offset_ms * 0.8 promotes to floating point and risks surprising rounding at the threshold; prefer integer arithmetic (e.g. error_ms * 5 > recno_max_clock_offset_ms * 4).
💡 Suggested change
Before:
ereport(FATAL,
(errmsg("recno clock error bound %lu ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
After:
ereport(FATAL,
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
📄 src/backend/access/recno/recno_clock.c (L377-L379)
Same non-portable %lu on a uint64 in the WARNING path; use UINT64_FORMAT.
💡 Suggested change
Before:
ereport(WARNING,
(errmsg("recno clock error bound %lu ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
After:
ereport(WARNING,
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
📄 src/backend/access/recno/recno_clock.c (L61-L68)
Security / availability footgun: this module mmaps an external, world-writable path (/dev/shm/clockbound) read-only and then trusts its contents. RecnoReadClockBound memcpy's sizeof(ClockBoundData) and only checks segment_id/tv_sec ordering — there is no magic/version and the layout depends on struct timespec padding (non-portable). Any local user can populate this file to inject arbitrary bounds; worse, a crafted error_bound drives RecnoCheckClockHealth into ereport(FATAL), taking the whole cluster down (self-DoS) when recno_fatal_on_clock_drift is on. At minimum, add a magic/version header, validate the mapped size, verify ownership/permissions of the path (reject world-writable), and do not escalate untrusted input straight to FATAL.
📄 src/backend/access/recno/recno_compress.c (L957-L962)
Data-corruption hazard: RecnoCompressDictionary writes only a 4-byte, backend-local dict_id while the dictionary itself is process-local and NOT persisted (see this file's own NOTE/TODO). RecnoDecompressDictionary reads that dict_id from on-disk data and indexes compression_dict->entries[], so a different backend or a post-restart read gets a stale/empty dictionary and returns wrong bytes or raises ERRCODE_DATA_CORRUPTED. RecnoChooseCompressionType never selects RECNO_COMP_DICTIONARY, but RecnoCompressAttribute still honors an explicit comp_type == RECNO_COMP_DICTIONARY. If any persisted path can pass DICTIONARY, this is silent data corruption. Either remove the dictionary path entirely (YAGNI) or make it reject persisted use.
📄 src/backend/access/recno/recno_operations.c (L309-L312)
Return-type/semantics mismatch: RecnoPagePruneOpt is declared to return int and the header comment says "Returns the number of tuples pruned", but every success path returns a boolean (return true; / return 0;) independent of ndead. Callers in recno_handler.c treat it as a bool. Pick one contract: either return the actual pruned count and update the comment/callers, or change the declared type to bool. As written a caller relying on the documented count would misbehave.
💡 Suggested change
Before:
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
return true;
}
After:
LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
return ndead;
}
📄 src/backend/access/recno/recno_handler.c (L1751-L1761)
Dead CLOG-fallback branch: hint_xid is initialized to InvalidTransactionId and then immediately tested with TransactionIdIsValid(hint_xid), which is always false, so this "clear stale DELETED via CLOG when the UNDO worker removed sLog entries" fallback can never execute — the tuple is always treated as committed-deleted. This looks like a leftover from the t_xid_hint removal: the intended source is the tuple's t_xmin, which is not read here. As written, visibility can be misclassified once sLog entries are discarded.
📄 src/backend/access/recno/recno_handler.c (L1823-L1830)
Same dead-branch pattern in the RECNO_TUPLE_UPDATED CLOG-fallback: hint_xid is hard-coded to InvalidTransactionId, so all the TransactionIdIsValid(hint_xid) arms are unreachable and the else ("updater committed -> old version is dead") always wins. The intended deciding XID (t_xmin) is never consulted, so an aborted out-of-place update whose sLog entries were discarded is misclassified.
📄 src/backend/access/recno/recno_fsm.c (L327-L329)
Performance + reentrancy hazard: RecnoGetFSMState allocates a fresh RecnoFSMState (with its own MemoryContext and defrag queue) on every call, and RecnoRecordFreeSpace/RecnoMarkPageForDefrag call it per page. The comment even concedes "In practice, this would be cached per relation." Because the queue is per-call it can fill immediately, and RecnoScheduleDefrag then synchronously calls RecnoDefragmentPage (which takes a cleanup lock on another buffer and writes WAL) from inside RecnoRecordFreeSpace — which is invoked on hot insert/update/delete paths, sometimes while other buffers are held. This is both palloc/context churn on hot paths and a lock-ordering/self-deadlock hazard. Cache the state per relation and make scheduling non-reentrant, or drop the queue entirely.
📄 src/backend/access/recno/recno_lock.c (L386-L391)
Dead placeholder API: RecnoCheckDeadlock always returns false ("Simplified deadlock check") after suppressing unused-variable warnings on the tag/lockmode it computes. If it has no functional caller it is dead scaffolding that should be removed (YAGNI) rather than shipped as a no-op that implies deadlock detection exists.
📄 src/backend/access/recno/recno_clock.c (L619-L623)
Empty GUC assign hooks (assign_recno_enable_clock_bound, assign_recno_fatal_on_clock_drift, assign_recno_clock_check_interval) whose only content is a comment add nothing over not registering an assign hook. Remove them and drop the hook registration; a no-op assign hook is unnecessary scaffolding.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L91-L94)
The XLOG_FLUX_VACUUM (0x30) case parses the record as xl_flux_vacuum_fe {uint32 ntuples}, but opcode 0x30 is emitted by FluxXLogDefrag() as xl_flux_defrag, whose backend layout in flux_xlog.h is {uint16 ntuples; uint64 commit_ts} (plus a trailing FluxOffsetMapping array). Reading a uint32 ntuples from a record whose first field is a uint16 (followed by padding and a uint64 commit_ts) yields a garbled tuple count in pg_waldump output. Since fluxdesc.c is the sole desc implementation for both frontend and backend (flux_xlog.c defines no flux_desc), this misparse ships to production WAL dumps. Make the frontend struct match xl_flux_defrag (uint16 ntuples; uint64 commit_ts), or reuse the FRONTEND xl_flux_vacuum/xl_flux_defrag definitions from flux_xlog.h. (high confidence)
📄 src/backend/access/rmgrdesc/fluxdesc.c (L6-L7)
This header comment is inaccurate and dangerous: flux_xlog.c defines no flux_desc/flux_identify (verified: only fluxdesc.c defines them). This rmgrdesc file is the SOLE implementation of the FLUX desc/identify routines and is linked into BOTH the backend (RM_FLUX_ID's desc callback) and frontend (pg_waldump). Calling it "frontend version" / "minimal" and claiming "full implementations are in flux_xlog.c" invites the exact struct-divergence bugs flagged elsewhere in this file, because it implies these copies are throwaway when they in fact drive production WAL description. Fix the comment to state this is the authoritative desc implementation and that its record layouts must stay byte-identical to flux_xlog.h. (high confidence)
💡 Suggested change
Before:
* This provides minimal desc/identify functions for frontend tools like pg_waldump.
* The full implementations are in flux_xlog.c for backend use.
After:
* This provides the desc/identify routines for the FLUX resource manager,
* used by both the backend (RM_FLUX_ID) and frontend tools like pg_waldump.
* The record layouts below must stay byte-identical to flux_xlog.h.
📄 src/include/access/flux_xlog.h (L437-L439)
The FRONTEND copy of xl_flux_update is not byte-compatible with the backend definition above it. The backend struct has two extra trailing fields (uint8 dst_block_id; uint8 pad[3]) after new_tuple_len; this FRONTEND copy omits them, making it 4 bytes shorter. Any frontend consumer that validates a record via sizeof(xl_flux_update) (the pattern the confirmed fix for fluxdesc.c suggests -- reuse these FRONTEND structs) would compute the wrong minimum length. Add the same trailing dst_block_id/pad fields so the FRONTEND and backend layouts are identical. (medium confidence)
💡 Suggested change
Before:
uint16 old_tuple_len; /* Length of old tuple */
uint16 new_tuple_len; /* Length of new tuple */
} xl_flux_update;
After:
uint16 old_tuple_len; /* Length of old tuple */
uint16 new_tuple_len; /* Length of new tuple */
uint8 dst_block_id; /* dst page block id for cross-page updates */
uint8 pad[3]; /* padding for alignment */
} xl_flux_update;
📄 src/include/access/flux.h (L865-L868)
These three dirty-map symbols are already declared in flux_dirtymap.h (its dedicated header). Declaring them a second time here duplicates the interface across two public headers; if either signature changes, the two copies silently drift and a TU that includes both will fail to compile. Drop these three lines from flux.h and #include "access/flux_dirtymap.h" instead (or vice-versa) so there is a single source of truth. (medium confidence)
📄 src/include/access/flux.h (L270-L272)
This comment is stale and contradicts the FluxTupleHeader documentation earlier in this same file, which states t_verptr is now "a plain aligned header field ... no trailing-byte arithmetic" and no longer a trailer located by item_len - 8. The version-chain head no longer "follows column data". Update the comment to describe the flag's current meaning (e.g. that the tuple has an active version chain via the t_verptr header field). (high confidence)
📄 src/backend/access/flux/flux_tuple.c (L1125-L1126)
This varlena path applies an EXTRA post-read alignment (att_align_nominal(data_ptr + attr_len, att->attalign)) that no other FLUX (de)form routine performs. The form path (flux_form_tuple_internal, data_ptr += attr_len), tts_flux_deform (data_ptr += attr_len), and FluxDeformTupleUpTo (data_ptr += attr_len) all advance by exactly attr_len with NO trailing alignment; the next attribute is aligned to ITS OWN alignment at the top of the loop.
When a varlena is followed by an attribute of smaller alignment (e.g. text -> "char"/bool), rounding data_ptr + attr_len up to the varlena's attalign here over-advances the cursor, so every following attribute is read from the wrong offset -> garbage/corrupt reads. FluxTupleToSlotWithOverflow is on live paths (index fetch in flux_handler.c and the UPDATE old/new slots in flux_operations.c), so this is a real data-correctness bug, not a corner case.
Drop the post-read alignment to match the other deform routines.
💡 Suggested change
Before:
slot->tts_values[i] = PointerGetDatum(data_ptr);
data_ptr = (char *) att_align_nominal(data_ptr + attr_len, att->attalign);
After:
slot->tts_values[i] = PointerGetDatum(data_ptr);
data_ptr += attr_len;
📄 src/backend/access/recno/recno_operations.c (L3307-L3317)
WAL/redo data-loss bug: this batch loop adds batch_count tuples to the page, but the WAL logging below calls RecnoXLogInsert exactly once, logging only formed_tuples[batch_start] at slots[batch_start]'s offset. RecnoXLogInsert registers the main buffer with REGBUF_STANDARD (no forced FPI) and encodes a single-tuple insert; recno_xlog_insert_redo replays exactly one tuple at xlrec->offnum. When the page modification does NOT capture a full-page image (the normal case after the first post-checkpoint write), crash recovery restores only the first tuple and the remaining batch_count - 1 tuples are permanently lost. The batch must either WAL-log every tuple it added (e.g. a multi-tuple insert record or one RecnoXLogInsert per tuple) or force a full-page image (REGBUF_FORCE_IMAGE) for the batched page.
📄 src/backend/access/flux/flux_vm.c (L72-L79)
FluxVMInit is an empty stub (body is a comment only) with no callers anywhere in the tree. Ship dead code violates YAGNI; remove it (and its prototype in flux.h).
📄 src/backend/access/flux/flux_vm.c (L384-L387)
FluxVMExtend has no callers anywhere in the tree. Dead code (YAGNI) -- remove it (and its prototype in flux.h) unless it is wired onto a live path.
📄 src/backend/access/flux/flux_vm.c (L407-L411)
FluxVMTruncate has no callers anywhere in the tree. As dead code (YAGNI) it should not ship. Separately, if it is ever wired up: it calls FlushRelationBuffers(rel) (flushes ALL forks, not just the VM fork) and smgrtruncate with no WAL logging of the truncation -- a standby's VM fork would then diverge from the primary after a heap truncation. Confirm both before enabling.
📄 src/backend/access/flux/flux_vm.c (L362-L365)
FluxVMPinBuffer has no callers anywhere in the tree. Dead code (YAGNI) -- remove it (and its prototype in flux.h).
📄 src/backend/access/flux/flux_vm.c (L436-L440)
FluxVMGetPageSize and FluxVMMapHeapToVM have no callers anywhere in the tree. Dead code (YAGNI) -- remove both (and their prototypes in flux.h).
📄 src/backend/access/recno/recno_clock.c (L205-L206)
SIGTERM handler bug (high confidence). pqsignal(SIGTERM, PG_SIG_DFL) installs the OS default disposition, which for SIGTERM terminates the process immediately, bypassing all backend cleanup (shared-memory detach via on_shmem_exit, buffer pins, LWLocks). A background worker must instead route SIGTERM to a handler that sets a flag and SetLatch()s MyLatch (the standard die/SignalHandlerForShutdownRequest), so the WaitLatch loop can exit cleanly. As written, the loop's shutdown_pending guard is the only intended exit, but nothing ever sets shutdown_pending on SIGTERM, so a normal pg_ctl stop / postmaster shutdown SIGTERM hard-kills this worker with no cleanup. Use SignalHandlerForShutdownRequest and check ShutdownRequestPending in the loop.
📄 src/backend/access/flux/flux_xlog.c (L815-L817)
FluxXLogOverflowWrite has no callers in the tree. FLUX no longer uses on-page overflow (the deform/form paths and header comments state "FLUX uses TOAST, not on-page overflow"), so this WAL emitter and its redo handler (flux_xlog_overflow_write_redo) are dead scaffolding for a removed feature. Since this is net-new code with no released WAL to stay compatible with, remove the emitter, the redo handler, and the XLOG_FLUX_OVERFLOW_WRITE dispatch (YAGNI).
📄 src/backend/access/flux/flux_xlog.c (L846-L849)
FluxXLogCompress has no callers in the tree. FLUX no longer compresses attributes (see the repeated "FLUX does not compress attributes" notes in flux_tuple.c/flux_slot.c). This emitter and its redo handler (flux_xlog_compress_redo) are dead scaffolding for a removed feature -- remove them and the XLOG_FLUX_COMPRESS dispatch (YAGNI).
📄 src/backend/access/flux/flux_xlog.c (L1034-L1036)
FluxXLogWriteDict has no callers in the tree. It WAL-logs a "compression-dictionary fork page", but FLUX no longer compresses attributes, so the dictionary fork and this emitter (plus its redo handler flux_xlog_write_dict_redo) are dead scaffolding. Remove them and the XLOG_FLUX_WRITE_DICT dispatch (YAGNI).
📄 src/backend/access/flux/flux_tuple.c (L461-L465)
has_overflow is initialized to false and never assigned true anywhere in flux_form_tuple_internal (FLUX has no on-page overflow), so this block is dead: FLUX_TUPLE_HAS_OVERFLOW / FLUX_INFOMASK_HASOVERFLOW are never set here. Same class of dead branch as has_compressed. Remove the variable and this block.
📄 src/include/access/recno.h (L102-L102)
Stray doubled percent in a plain C comment. %% is a printf escape; this text is a comment (not a format string), so it renders literally as 100%%. Use a single %.
Confidence: high.
💡 Suggested change
Before:
+ * A page packed to 100%% therefore makes every row on it permanently
After:
+ * A page packed to 100% therefore makes every row on it permanently
📄 src/include/access/recno.h (L24-L26)
Convention violation (portability/maintainability): a PostgreSQL header must not #include "postgres.h". postgres.h is included first by every .c file and pulls in the full backend environment; including it from a widely-included header is against the tree's include conventions and makes the header unusable from any frontend TU (postgres.h is only frontend-safe when the includer defines FRONTEND first). The same applies to recno_xlog.h, recno_slog.h, and recno_diff.h. Drop the postgres.h include from these headers and let the including .c file provide it (as recno_*.c already do).
Confidence: high (convention); note the sibling flux.h/flux_xlog.h repeat the same mistake, so this is not new-to-recno, but it should not be propagated.
📄 src/backend/access/rmgrdesc/recnodesc.c (L28-L29)
DRY/maintainability footgun: these opcode macros are hand-copied from recno_xlog.h and kept in agreement only by a "keep in sync" comment. If the values in recno_xlog.h ever change, pg_waldump silently misidentifies RECNO WAL records with no compile-time error. Direct inclusion is genuinely blocked here (recno_xlog.h pulls in access/recno.h -> heapam.h/tableam.h/utils/rel.h, which are not frontend-safe), so a code-sharing fix is non-trivial; at minimum consider factoring the opcode defines into a small frontend-safe header included by both, rather than duplicating. Note this mirrors the existing fluxdesc.c pattern.
Confidence: high (duplication confirmed; frontend inclusion of recno_xlog.h confirmed infeasible).
📄 src/backend/access/recno/recno_overflow.c (L395-L401)
Uninitialized page used on the write path. When the FSM has no space, this extends the relation with ReadBufferExtended(..., P_NEW, ...) and the fresh page is never passed through RecnoInitPage(). The buffer is then locked and PageGetFreeSpace(page) (line 416) and PageAddItem(page, ...) (line 499) run against a page whose header is all zeros (pd_lower == pd_upper == 0). Every other RECNO write/redo site guards new pages with if (PageIsNew(page)) RecnoInitPage(...); this one does not. Operating on an uninitialized page is a corruption/crash hazard. Call RecnoInitPage(BufferGetPage(buffer), BufferGetPageSize(buffer)) on the newly extended page before touching it.
Confidence: high.
📄 src/backend/access/recno/recno_overflow.c (L951-L952)
On-disk page mutation without WAL. RecnoDeleteOverflowChain retires items with RecnoPageIndexTupleDelete + MarkBufferDirty but emits no WAL record and sets no page LSN (the comment openly states "We do NOT WAL-log individual overflow deletions"). This diverges primary from standby and is not crash-safe: after recovery/on a replica the deleted overflow records reappear while the parent tuple no longer references them, and the FSM disagrees. Overflow-record deletion changes durable shared state and must be WAL-logged and replayed. This is unlike TOAST, whose deletions are ordinary WAL-logged heap deletes.
Confidence: high.
📄 src/backend/access/recno/recno_overflow.c (L1522-L1524)
Wrong WAL record for a selective delete. Pass 2 selectively removes only orphaned overflow records (keeping referenced ones), then WAL-logs RecnoXLogInitPage. But recno_xlog_init_page_redo replays with RBM_ZERO_AND_LOCK + RecnoInitPage, which zeroes the entire page and drops ALL items. On redo/standby this destroys the still-referenced overflow records that the primary preserved, corrupting live data and diverging the replica. The WAL record must describe exactly which offsets were deleted (as recno_xlog_defrag_redo does via a logged unused-offset list), not a page-init.
Confidence: high.
📄 src/backend/access/recno/recno_slog.c (L1461-L1463)
Durability / replica-divergence bug: setting RECNO_TUPLE_DELETED here is a durable visibility change but is applied with MarkBufferDirty() and no WAL record (no XLogInsert, no PageSetLSN). On the primary the row becomes dead; on a standby (or after crash recovery) it stays live, because nothing replays this mutation. This is not a hint bit -- it flips MVCC visibility. It must be WAL-logged and replayed. (Compare RecnoSLogClearUncommittedFlags just below, which at least uses MarkBufferDirtyHint.) Separately, mutating the page here while holding only AccessShareLock on the relation is inconsistent with the RowExclusiveLock the DML paths use.
Confidence: high.
📄 src/backend/access/recno/recno_slog.c (L527-L531)
No interrupt check in the busy-wait retry loop. This loop sleeps pg_usleep(1000) up to 3 times waiting for other backends' cleanup, but contains no CHECK_FOR_INTERRUPTS(). A backend spinning here cannot respond to query cancel or fast SIGTERM. Add CHECK_FOR_INTERRUPTS() at the top of each iteration. More broadly, sleeping/looping on a shared-memory hot path under table-full pressure is a latent livelock; this deserves a design note in the -hackers thread.
Confidence: high.
📄 src/backend/access/recno/recno_slog.c (L980-L986)
Holding all 128 partition LWLocks simultaneously and then palloc()ing under them is dangerous. max_keys = RecnoSLogNumEntries() can be up to 262144, so this allocates several MB while every sLog partition is locked SHARED, freezing all sLog activity cluster-wide for the duration. If the palloc hits OOM, the ereport unwinds with all 128 locks held (relying entirely on LWLock cleanup at abort). Acquiring every partition lock at once also creates a global lock-ordering point that any two-partition path must never contend with. Prefer per-partition scans that lock one partition at a time, or at minimum allocate the buffer BEFORE taking the locks.
Confidence: high.
📄 src/backend/access/recno/recno_pbu.c (L69-L69)
recno_pbu_prepared (and the associated statics recno_pbu_uur, recno_pbu_undometa) are only reset by RecnoPbuFinishUndo/RecnoPbuCancelUndo. If any error is raised between RecnoPbuPrepareInsert() and the matching finish (e.g. during the forward-op page work or XLogInsert), nothing resets this flag: there is no xact-abort callback that calls RecnoPbuCancelUndo, and grep shows RecnoPbuCancelUndo has NO callers anywhere in the tree. The next DML on the same backend then trips Assert(!recno_pbu_prepared) in RecnoPbuPrepareInsert (assert builds) or silently reuses stale prepared state (production). Register an AtAbort/xact-callback reset, and either wire RecnoPbuCancelUndo into the abort path or remove it as dead code.
Confidence: high.
📄 src/backend/access/recno/recno_slot.c (L523-L527)
GetCurrentTransactionId() forces XID assignment inside an attribute accessor. Reading xmin/xmax from a RECNO slot (e.g. selecting the system column, or executor paths that inspect it) will now consume an XID even for an otherwise read-only statement, and will error on a hot standby / read-only transaction. Use GetCurrentTransactionIdIfAny() (as the no-tuple path and tts_recno_is_current_xact_tuple already do) and return InvalidTransactionId when none is assigned, rather than forcing one.
Confidence: medium.
📄 src/backend/access/recno/recno_overflow.c (L1552-L1553)
Dead scaffolding (YAGNI). RecnoStoreOverflow, RecnoFetchOverflow (which unconditionally ereport(ERROR, FEATURE_NOT_SUPPORTED)) and RecnoDeleteOverflow are a "legacy interface (kept for compatibility during transition)" but grep shows RecnoStoreOverflow/RecnoFetchOverflow have no live callers. A fork slated for pgsql-hackers should not carry a transition-era shim with no callers and an aspirational "during transition" comment. Remove these (and their declarations in recno.h) unless a caller is added.
Confidence: high (no-callers verified for Store/Fetch); the "during transition" comment is stale.
📄 src/backend/access/recno/recno_xlog.c (L888-L888)
RecnoXLogCompress is defined and exported but has no callers anywhere in the tree (only this definition and the header prototype); the XLOG_RECNO_COMPRESS redo handler likewise only reacts to records nothing emits. This is speculative WAL scaffolding for a compression-logging path that is not wired up. Per minimalism, remove the unused logger + opcode (and header prototype), or justify it against a caller in the -hackers thread.
Confidence: high (no callers found for RecnoXLogCompress).
📄 src/backend/access/recno/recno_xlog.c (L157-L163)
Footgun: RecnoReplicaHandleUncertainty busy-waits with pg_usleep during WAL redo on a standby to stall until the physical clock passes an HLC uncertainty window. Sleeping inside the redo/apply path stalls recovery and delays hot-standby feedback, and the loop only checks CHECK_FOR_INTERRUPTS() -- it will happily block replay for uncertainty_ms per record. Spinning on wall-clock skew during recovery is a serious hazard; this needs a design justification (and a Message-Id reference to the prior discussion) or removal. At minimum it should be gated so it can never run on the standby apply path by default.
Confidence: medium.
📄 src/backend/access/recno/recno_overflow.c (L324-L329)
Lock-ordering / deadlock risk. This store loop keeps every prior overflow buffer EXCLUSIVE-locked (they stay in overflow_buffers for atomic WAL) while, per iteration, it additionally SHARE-locks an arbitrary prev_block (here) and then EXCLUSIVE-locks an arbitrary target_block returned by the FSM. Because target/prev block numbers are not acquired in any defined order, two concurrent inserters holding different overflow pages can deadlock. RECNO relies on BufferIsLockedByMeInMode to avoid self-deadlock, but that does not address cross-backend ordering. Establish and document a consistent buffer-lock acquisition order (e.g. ascending block number) for the overflow chain, or hold only one overflow buffer lock at a time.
Confidence: medium.
📄 src/include/access/recno_diff.h (L18-L18)
Convention violation (portability/maintainability, high confidence): a PostgreSQL header must not #include "postgres.h". postgres.h is included first by every .c file and pulls in the full backend environment; including it from a widely-included header is against the tree's include conventions and prevents this header from being used by frontend/extension code. This struct-only header does not need it -- it only uses uint16/Size/char which come from c.h/postgres.h at the .c level already. Drop the include (same issue as recno.h).
📄 src/include/access/recno_slog.h (L19-L21)
Convention violation (portability/maintainability, high confidence): a PostgreSQL header must not #include "postgres.h". postgres.h is included first by every .c file; including it from a header is against the tree's include conventions. Remove it and rely on the including .c file having already pulled in postgres.h; keep only the specific access/xact.h and storage/itemptr.h includes this header actually needs (same issue as recno.h).
💡 Suggested change
Before:
#include "postgres.h"
#include "access/xact.h"
After:
#include "access/xact.h"
📄 src/include/access/recno_xlog.h (L18-L20)
Convention violation (portability/maintainability, high confidence): a PostgreSQL header must not #include "postgres.h". Remove it and rely on the including .c file; the subsequent access/recno.h include already pulls in what is needed (same issue as recno.h).
💡 Suggested change
Before:
#include "postgres.h"
#include "access/recno.h"
After:
#include "access/recno.h"
📄 src/include/access/recno.h (L221-L221)
Wrong/misleading comment and dead constant (high confidence). recno_overflow.c always sets and validates or_magic against RECNO_OVERFLOW_RECORD_MAGIC (0x524F5652 "ROVR"), never RECNO_OVERFLOW_MAGIC (0x524F5646 "ROVF"). The field comment here cites the wrong constant, and RECNO_OVERFLOW_MAGIC defined below is dead code with no users. Fix the comment to reference RECNO_OVERFLOW_RECORD_MAGIC and drop the unused RECNO_OVERFLOW_MAGIC define.
💡 Suggested change
Before:
uint32 or_magic; /* RECNO_OVERFLOW_MAGIC */
After:
uint32 or_magic; /* RECNO_OVERFLOW_RECORD_MAGIC */
📄 src/backend/access/undo/undo_xlog.c (L50-L50)
This RM_HEAP_ID branch (and the matching one in UndoReadBatchFromWAL) is dead code that contradicts the stated design. The README asserts heap does not participate in UNDO (leaves am_supports_undo=false, no UNDO code in heapam), and grep confirms nothing in src/backend/access/heap/ ever sets XLH_INSERT_HAS_UNDO / XLH_DELETE_HAS_UNDO / XLH_UPDATE_HAS_UNDO. So these tests can never be true; the branch is unreachable, and the layering breach it introduces (the AM-agnostic UNDO core #including heapam_xlog.h and hard-coding heap opcode/flag knowledge) has no compensating benefit. Either wire the flag emission into heapam (and drop the README claim that heap has no UNDO), or remove the heap branches, the heapam_xlog.h include, and the self-flagged FIXME. A shipping patch should not carry a FIXME admitting an unresolved agnosticism breach around dead branches. (high confidence)
📄 src/backend/access/undo/undo_xlog.c (L379-L381)
Buffer overflow in the delta-CLR redo path. The validation checks prefix/suffix/changed against cur_len and that prefix+changed+suffix == old_tuple_len, but never checks that old_tuple_len fits the item's on-page storage. ItemIdSetNormal() only rewrites lp_len (it does NOT reallocate page space, keeping the same lp_off), so when reversing an in-place UPDATE that grew the tuple (old_tuple_len > cur_len), memcpy(cur_htup, restored, old_tuple_len) writes past the item's storage into adjacent tuples on the page. This is heap-page corruption inside a critical WAL redo section, i.e. it corrupts during crash recovery and cannot be caught. Add a guard that old_tuple_len <= cur_len (or, if in-place growth is legitimately possible, this whole delta-restore approach needs a page-relocation path, not an in-place memcpy). (high confidence)
📄 src/backend/access/undo/undo_xlog.c (L102-L102)
Stale constant in comment: MAX_UNDO_LOGS is defined as 100 in access/undolog.h, not 64. The README (sections 6 and 13) also says 100. Fix the comment to avoid misleading capacity reasoning during recovery. (high confidence)
📄 src/backend/access/undo/undo_xlog.c (L653-L661)
O(n^2) work during redo: UndoRecoveryTrackBatch linear-scans undo_recovery_entries for every XLOG_UNDO_BATCH record, and UndoRecoveryRemoveXid also linear-scans on every commit/abort record replayed. With the safety cap at UNDO_RECOVERY_MAX_ENTRIES (1,048,576) distinct in-flight XIDs, this degrades to quadratic recovery time. Normal in-flight cardinality at crash is small, but a crash under a very large fan-out of concurrent UNDO-writing transactions hits this cliff on the recovery hot path. A HTAB/simplehash keyed by xid removes the quadratic term with negligible cost. At minimum, document the assumed cardinality bound. (medium confidence)
📄 src/backend/access/undo/undo_xlog.c (L1472-L1479)
This function's own doc-comment states callers 'MUST' wrap chain application in a PG_CATCH that calls UndoResetBatchReader after an error, because the cached static reader can be left with a stale segment FD or partial read buffer. The logical revert worker does this, but the inline-abort caller (AtAbort_XactUndo in xactundo.c) does NOT call UndoResetBatchReader in its PG_CATCH -- it only calls BufferLockReleaseAll/FlushErrorState. If ApplyUndoChainFromWAL errors mid-read on the inline path, the next abort in the same backend reuses the corrupted cached reader and can SIGSEGV, exactly the failure this reset guards against. The contract enforced by comment alone is a footgun; consider resetting the cached reader from within UndoReadBatchFromWAL's own error handling (or making it non-static / caller-owned) so correctness does not depend on every caller remembering the PG_CATCH. (medium confidence)
📄 src/backend/access/undo/undoapply.c (L304-L306)
Per-batch payload memory accumulates for the whole chain walk and is never released between batches. UndoReadBatchFromWAL palloc's result and result->payload in CurrentMemoryContext, and UndoFreeBatchData is a documented no-op. The record_starts array is freed per batch (its own batchctx is deleted), but the batch->payload allocations are not. For a long UNDO chain (large aborted transaction), CurrentMemoryContext grows proportional to total UNDO volume until the caller resets its context. The inline-abort path deletes its undo_ctx afterward so it is bounded per-abort, but the logical revert worker path runs in the transaction context for the whole chain. Consider a per-chain transient context reset every N batches, or actually freeing the batch payload once applied. At minimum, document that peak memory is O(total UNDO volume of the chain), not O(one batch). (medium confidence)
📄 src/backend/access/undo/slog_flathash.c (L261-L261)
Non-ASCII punctuation (em-dash U+2014) in comments violates the ASCII-only source rule. Replace with '--' or a plain hyphen. Same defect recurs at the other 'No free slot -- ...' / 'Safe to reclaim -- ...' comments in this file. (high confidence)
📄 src/backend/access/undo/slog_tuple.c (L137-L137)
Non-ASCII punctuation violates the ASCII-only source rule: em-dash U+2014 ('---'/'--') and arrow U+2192 ('->') appear here and in several other comments in this file (e.g. the 'key->partition routing', 'no savepoint -> use the hash', 'Found it -- store local copy', 'Tracked key not found -- ...' comments). Replace em-dashes with '--' and arrows with '->'. (high confidence)
📄 src/backend/access/undo/README (L1108-L1108)
Non-ASCII punctuation violates the ASCII-only source rule. The em-dash (U+2014) here and the arrow (U+2192) in '2→5' on the line below should be plain ASCII: use '--' for the em-dash and '2->5' for the arrow. (high confidence)
📄 src/backend/access/undo/README (L5-L5)
This README (1130 lines) documents behavior that has not shipped and admits incomplete integration, which per patch-hygiene should describe only current behavior. Concrete examples in the diff: monitoring functions 'are NOT SQL-callable ... Wiring them into the catalog is outstanding work'; 'ProcArray integration for oldest XID is simplified' / 'Proper ProcArray scan ... needed for production'; a full 'Standby-Side Before-Image Regeneration (designed, rejected)' section; 'Planned Future Work'; and 'UNDO-Based MVCC (In Progress ...)'. Trim to shipped behavior; move rejected-design rationale and future work to the -hackers thread / commit message rather than the in-tree README. (medium confidence)
📄 src/backend/access/recno/recno_vm.c (L122-L132)
WAL/crash-consistency bug: the VM page is modified and MarkBufferDirty(vmBuf) is called, then WAL is inserted, all OUTSIDE any critical section. heap's visibilitymap_set wraps the page mutation + XLogInsert in START_CRIT_SECTION()/END_CRIT_SECTION(). Without it, an error between MarkBufferDirty and a successful XLogInsert (or during buffer/WAL work) leaves the buffer dirty with no WAL record, so the change can reach disk without being logged, diverging primary from standby / crash recovery. Wrap the modification and WAL insert in a critical section.
📄 src/backend/access/recno/recno_tuple.c (L460-L467)
RecnoDeformTuple has the dropped-attribute desync bug that tts_recno_deform and RecnoTupleToSlotWithOverflow explicitly fixed. Tuples written before ALTER TABLE ... DROP COLUMN stored real bytes for the (then-live) column and are never rewritten, so their null-bitmap bit is still set (NOT NULL) and bytes are present on-disk. This branch returns early without advancing data_ptr, desynchronizing every following attribute (garbage varlena/TOAST pointer -> SIGSEGV in detoast_attr). RecnoDeformTuple is used by the delta-update WAL path (recno_operations.c) and the logical-decoding image path (recno_xlog.c), so a dropped-then-selected column can corrupt WAL/decode output. Consult the null bitmap first, and when bytes are present skip exactly attlen (or VARSIZE_ANY for attlen==-1) bytes, matching the sibling functions.
💡 Suggested change
Before:
if (att->attisdropped)
{
values[i] = (Datum) 0;
isnull[i] = true;
continue;
}
/* Check null bitmap: bit=0 means NULL (bit cleared in RecnoFormTuple) */
After:
if (att->attisdropped)
{
/*
* A dropped attribute may still occupy bytes in tuples written
* before the DROP COLUMN; skip exactly what the writer emitted so
* following attributes stay in sync.
*/
if (!((header->t_infomask & RECNO_INFOMASK_HASNULL) &&
att_isnull(i, nulls_bitmap)))
{
data_ptr = (char *) att_align_nominal(data_ptr, att->attalign);
if (att->attlen > 0)
data_ptr += att->attlen;
else
data_ptr += VARSIZE_ANY(data_ptr);
}
values[i] = (Datum) 0;
isnull[i] = true;
continue;
}
/* Check null bitmap: bit=0 means NULL (bit cleared in RecnoFormTuple) */
📄 src/backend/access/recno/recno_vm.c (L212-L220)
Same WAL/crash-consistency defect as RecnoVMSet: the VM page modification and MarkBufferDirty(vmBuf) are not inside a critical section around the XLogInsert. Follow heap's visibilitymap.c and wrap the map mutation + WAL insert in START_CRIT_SECTION()/END_CRIT_SECTION() so a dirtied buffer cannot escape without its WAL record.
📄 src/backend/access/recno/recno_overflow.c (L588-L590)
ReadBuffer()/LockBuffer() are called inside the critical section opened at START_CRIT_SECTION() (before PageAddItem) and closed only at the matching END_CRIT_SECTION() after the buffer-tracking block. Any error raised by ReadBuffer (buffer I/O failure, eviction of a dirty victim that fails to write, etc.) inside a critical section escalates to PANIC (crashing the whole cluster). Buffer reads and locks must be done before entering the critical section. Move this prev-block read/lock (and the FSM pfree at the end of the loop) outside the crit section, or open the crit section only around the actual page mutation + WAL insert as elsewhere in the tree.
📄 src/backend/access/recno/recno_slog.c (L1609-L1619)
This PG_CATCH swallows the error with only EmitErrorReport()/FlushErrorState(), but that does NOT release resources acquired inside the PG_TRY. If any call between ReadBuffer() and UnlockReleaseBuffer() throws (e.g. LockBuffer, a corrupt-page ereport, MarkBufferDirtyHint), the buffer's EXCLUSIVE content lock and pin remain held and rel stays open. PG_TRY/PG_CATCH does not unwind buffer content locks or LWLocks -- only a (sub)transaction abort does. Since this runs at PRE_COMMIT and the error is being discarded rather than aborting the transaction, the backend continues holding a stuck buffer content lock, which can self-deadlock the very next buffer access. Either wrap this in a subtransaction (BeginInternalSubTransaction / RollbackAndReleaseCurrentSubTransaction) so resources are properly released, or drop the catch-and-continue entirely and let the error propagate.
📄 src/backend/access/undo/undobuffer.c (L190-L192)
The per-backend undo_t2buf is only cleared by UndoBufferEnd(), which is invoked from the table AM's finish_bulk_insert callback (flux_handler.c). On an aborted DML operation that callback is not guaranteed to run, so undo_t2buf stays active with a stale xid and chain_prev across the transaction boundary. There is no AtAbort/AtProcExit reset hook analogous to xactundo.c's AtAbort_XactUndo/AtProcExit_XactUndo. A subsequent transaction that adds records without a fresh UndoBufferBegin (or an overflow flush during a partially-torn-down abort) would embed the previous transaction's xid/chain_prev into WAL, corrupting the UNDO chain. Add a reset entry point called from the abort and proc-exit paths in xact.c (deactivate and clear xid/chain_prev), mirroring the xactundo cleanup.
📄 src/backend/access/undo/xactundo.c (L922-L923)
The goto inline_undo_done jumps over MemoryContextSwitchTo(old_ctx) and MemoryContextDelete(undo_ctx) (both only reached on the fall-through/PG_CATCH path). When UndoValidateBatchLSN() returns false, control lands at inline_undo_done with CurrentMemoryContext still equal to undo_ctx. Everything downstream then runs in that context and it is never deleted: ATMAddAborted(), the UndoRecordSetFree loop, UndoClearBatchLSN, and ResetXactUndo all execute in undo_ctx, which is leaked (it lives in TopMemoryContext, so it is not freed by transaction teardown). This is the can't-fail post-abort path, where a leak plus a doomed CurrentMemoryContext is especially hazardous. Switch back and delete the context before the goto (or restructure so the switch/delete run unconditionally, e.g. via PG_FINALLY).
📄 src/backend/access/recno/recno_tuple.c (L926-L931)
Memory leak: values and isnull are palloc'd but never freed (the function returns immediately after RecnoFormTuple). The sibling RecnoComputeSlotSize below frees both. RecnoFormTuple does not retain the arrays, so they should be freed before returning. (They also duplicate slot->tts_values/tts_isnull unnecessarily -- you could pass those directly and drop the two palloc+memcpy entirely.) In a tuple-formation hot path this leaks two allocations per call into the surrounding context until it is reset.
💡 Suggested change
Before:
slot_getallattrs(slot);
memcpy(values, slot->tts_values, tupdesc->natts * sizeof(Datum));
memcpy(isnull, slot->tts_isnull, tupdesc->natts * sizeof(bool));
return RecnoFormTuple(tupdesc, values, isnull, NULL, NULL);
}
After:
slot_getallattrs(slot);
return RecnoFormTuple(tupdesc, slot->tts_values, slot->tts_isnull,
NULL, NULL);
}
📄 src/backend/access/undo/xactundo.c (L889-L889)
AtAbort_XactUndo runs on the transaction abort path (called from AbortTransaction() with no wrapping PG_TRY). Several calls here can ereport(ERROR) but are outside the PG_TRY that guards ApplyUndoChainFromWAL: AllocSetContextCreate, XLogFlush(XactLastRecEnd) (can PANIC/ERROR), and ATMAddAborted (both the inline-fallback call and the large-transaction call). An error escaping AtAbort_XactUndo re-enters the abort machinery and risks recursive abort or PANIC. The XLogFlush at line 889 is particularly exposed since it runs before any PG_TRY. Either guard these with PG_TRY/PG_CATCH (as done for the inline apply) or document/verify they cannot throw in this context.
📄 src/backend/access/undo/undoworker.c (L127-L129)
shutdown_requested is a plain bool in shared memory, but undo_worker_sigterm writes it from signal-handler context (UndoWorkerShmem->shutdown_requested = true;). A signal handler may only write objects of type volatile sig_atomic_t; writing a plain bool is not async-signal-safe and the main-loop read while (!UndoWorkerShmem->shutdown_requested) has no guaranteed visibility of the write. Use the standard shutdown-request interrupt handler (SignalHandlerForShutdownRequest / ShutdownRequestPending) as other background processes do, or make this a volatile sig_atomic_t local flag. Note also that UndoWorkerRequestShutdown writes the same field under LWLock while the signal handler writes it lock-free — inconsistent synchronization on the same field.
📄 src/backend/access/undo/undoworker.c (L395-L396)
retained_mb is uint64 but is formatted with %lu and cast to (unsigned long). On LLP64 platforms (64-bit Windows/MSVC) and all 32-bit platforms, unsigned long is 32 bits, so this truncates the value. Use UINT64_FORMAT (and no cast) for the uint64 argument, per the tree's portability rules for 64-bit integer formatting.
📄 src/backend/access/undo/logical_revert_worker.c (L377-L378)
The returned handle is discarded and per-db workers use bgw_restart_time = 60, so a worker is never explicitly stopped when its database is dropped. When a database disappears the launcher merely forgets its OID (see LogicalRevertLauncherMain); the orphaned worker keeps running/auto-restarting against the dropped DB until it exits on its own, and the launcher may concurrently spawn a new worker for a reused OID. Combined with the list_length(next_known) >= max_logical_revert_workers cap in the launcher, this means the GUC is not a true concurrency ceiling and the bgworker slot pool can be exhausted. Retain the handle (and/or notify pid) and TerminateBackgroundWorker on DB drop, or otherwise enforce the ceiling against actually-live workers.
📄 src/backend/access/undo/undostats.c (L39-L41)
These functions are exposed via PG_FUNCTION_INFO_V1 and invoked from SQL (e.g. examples/05-undo-monitoring.sql calls SELECT * FROM pg_stat_get_undo_logs() and SELECT pg_undo_force_discard()), but there is no corresponding pg_proc.dat catalog entry (a search of src/include/catalog/pg_proc.dat finds no undo functions). Without a pg_proc.dat entry the functions are not SQL-callable and those queries fail with "function does not exist". Add pg_proc.dat entries (with OIDs from the 8000-9999 developer range) for each of pg_stat_get_undo_logs, pg_stat_get_undo_buffers, and pg_undo_force_discard — do not hand-edit the generated fmgroids.h/fmgrprotos.h.
📄 src/backend/access/undo/undo_xlog.c (L273-L276)
Buffer overflow in the UNDO_CLR_HAS_TUPLE redo branch, same class as the delta branch below. ItemIdSetNormal(lp, ItemIdGetOffset(lp), xlrec->tuple_len) only rewrites lp_len; it does not reallocate on-page storage. The code validates datalen >= xlrec->tuple_len (WAL payload size) but never validates that tuple_len fits the item's original on-page storage slot. A corrupt or WAL-recycled CLR with tuple_len larger than the slot makes memcpy(htup, data, xlrec->tuple_len) write past the item into adjacent page data, corrupting the page during redo. Validate tuple_len against the item's storage (and page free space) before ItemIdSetNormal/memcpy, matching the guard the delta branch needs. This is the DELETE/UPDATE/INPLACE undo path, so it is hit far more often than the delta path.
📄 src/backend/access/undo/undoapply.c (L304-L306)
Missing circular/forward-chain guard: the walk follows batch->header.chain_prev without verifying it is strictly less than the current batch_lsn. A corrupt or circular UNDO chain (chain_prev >= batch_lsn) causes an infinite loop, repeatedly re-reading the same or a forward batch and re-applying records. PerformUndoRecovery() in undo_xlog.c guards this exact case with a PANIC; this runtime abort / logical-revert path (the common path) has no such guard and is the more dangerous place to lack it. Add the same check before following the chain.
📄 src/backend/access/undo/undo_xlog.c (L1007-L1009)
During crash recovery PerformUndoRecovery() runs in the startup process, where MyDatabaseId is InvalidOid. Recording deferred->dboid = MyDatabaseId therefore stamps the deferred aborted transaction with an invalid database OID. xl_undo_batch carries no dboid, so the real database is unavailable here, but the consequence is that FlushDeferredUndoXacts() adds an ATM entry with dboid=InvalidOid; the logical-revert launcher (ATMCollectUnrevertedDatabases) then cannot spawn a per-database worker to apply the undo, so the deferred rollback is silently never performed. This needs the real dboid propagated through the UNDO batch (or another resolution), not MyDatabaseId.
📄 src/backend/access/undo/undo_xlog.c (L617-L618)
UndoRecoveryEntry.status is declared (comment "in use") but never read or written anywhere; entries are marked removed by setting xid = InvalidTransactionId, not via status. Dead field -- remove it (YAGNI).
📄 src/backend/access/undo/undo_bufmgr.c (L234-L236)
Footgun: the name and doc claim this drops "a range of blocks" bounded by last_block, but DropRelationBuffers drops ALL buffers with blockNum >= first_block, so last_block is ignored entirely. A future caller passing (first=10, last=20) would silently drop every buffer from block 10 to the end of the log, discarding still-live undo data past block 20 -- silent data loss. The function also currently has no callers (dead code). Either remove it, or make the semantics match the name (and drop the unused last_block parameter / assert). The confusing Assert(first_block <= last_block) on an ignored parameter compounds the trap.
📄 src/backend/access/undo/README (L795-L797)
Internal contradiction: this line says an in-place-update AM's old versions "come from the per-relation UNDO fork," but Section 1 states the per-relation UNDO fork (RELUNDO_FORKNUM/relundo.c) was "removed entirely ... do not add documentation describing it as available," and the version-reconstruction section says prior versions come from the per-backend undo log (FluxPbuFetchVersion). This resurrects the removed subsystem the README forbids documenting. Also contradicts Section 17 "Delta Compression: Implemented for the in-place AMs" versus the adjacent line here "No delta compression for UPDATE records." Reconcile these.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1053-L1054)
info.keysize is set to sizeof(TransactionId) (4 bytes), but every hash_search() call site in this file passes a RollbackHashKey {FullTransactionId full_xid; UndoRecPtr start_urec_ptr;} (16 bytes) as the key, and the entry (RollbackHashEntry) is also keyed on the full struct. With HASH_BLOBS, dynahash hashes and compares only the first keysize bytes, so only 4 of the 16 key bytes participate: distinct (full_xid, start_urec_ptr) pairs collide/alias, and lookups can retrieve or mutate the wrong rollback request. This corrupts rollback scheduling. keysize must be sizeof(RollbackHashKey).
💡 Suggested change
Before:
info.keysize = sizeof(TransactionId);
info.entrysize = sizeof(RollbackHashEntry);
After:
info.keysize = sizeof(RollbackHashKey);
info.entrysize = sizeof(RollbackHashEntry);
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1198-L1200)
Two bugs here on the error path. (1) The lookup key is &urinfo->full_xid (a FullTransactionId), but the table is keyed on RollbackHashKey {full_xid, start_urec_ptr}; passing only the xid pointer is a malformed key (missing start_urec_ptr) and will not reliably find the entry. (2) The returned rh is dereferenced (rh->in_progress = false) without a NULL check; hash_search(..., HASH_FIND, ...) returns NULL when not found, so this can crash. Build a proper RollbackHashKey and guard against NULL.
💡 Suggested change
Before:
rh = (RollbackHashEntry *) hash_search(RollbackHT, (void *) &urinfo->full_xid,
HASH_FIND, NULL);
rh->in_progress = false;
After:
{
RollbackHashKey hkey;
hkey.full_xid = urinfo->full_xid;
hkey.start_urec_ptr = urinfo->start_urec_ptr;
rh = (RollbackHashEntry *) hash_search(RollbackHT, (void *) &hkey,
HASH_FIND, NULL);
}
if (rh != NULL)
rh->in_progress = false;
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1111-L1113)
These memset calls zero only a single element (sizeof(UndoXidQueue)), but the arrays are ROLLBACK_REQUEST_QUEUE_SIZE elements long, and UndoXidQueueGetFreeElem()/UndoSizeQueueGetFreeElem()/UndoErrorQueueGetFreeElem() scan all ROLLBACK_REQUEST_QUEUE_SIZE slots for full_xid == InvalidFullTransactionId. The code only works because fresh shmem happens to be OS zero-filled; the explicit memset here does not match its stated intent and would leave the tail uninitialized on any reuse. Use the array-sized helper (e.g. UndoXidQueueElemsShmSize()) or ROLLBACK_REQUEST_QUEUE_SIZE * sizeof(UndoXidQueue).
💡 Suggested change
Before:
binaryheap_init_shm(UndoXidBinaryHeap, ROLLBACK_REQUEST_QUEUE_SIZE,
undo_age_comparator, NULL);
memset(UndoXidQueueElems, 0, sizeof(UndoXidQueue));
After:
binaryheap_init_shm(UndoXidBinaryHeap, ROLLBACK_REQUEST_QUEUE_SIZE,
undo_age_comparator, NULL);
memset(UndoXidQueueElems, 0, UndoXidQueueElemsShmSize());
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1117-L1117)
Same undersized memset: only one UndoSizeQueue/UndoErrorQueue element is zeroed rather than the full ROLLBACK_REQUEST_QUEUE_SIZE-element array that the free-slot scans traverse.
💡 Suggested change
Before:
memset(UndoSizeQueueElems, 0, sizeof(UndoSizeQueue));
After:
memset(UndoSizeQueueElems, 0, UndoSizeQueueElemsShmSize());
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1121-L1121)
Undersized memset for the error queue element array (only one element zeroed vs. ROLLBACK_REQUEST_QUEUE_SIZE).
💡 Suggested change
Before:
memset(UndoErrorQueueElems, 0, sizeof(UndoErrorQueue));
After:
memset(UndoErrorQueueElems, 0, UndoErrorQueueElemsShmSize());
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L63-L66)
max_undo_workers and undo_worker_quantum_ms are plain global variables, not GUCs -- there is no entry for either in guc_parameters.dat (only pbu_undo_workers_enabled and undo_rollback_overflow_size are registered). Labeling this block "GUC parameters" is misleading, and the WARNING hint below ("Raise max_undo_workers and restart") tells the user to change something that cannot be set via configuration. Either register these as real GUCs or fix the comment/hint to reflect that they are compile-time constants.
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L9-L10)
Stale IDENTIFICATION path carried over from zheap: this file is src/backend/access/undo/perbackend/pbu_undoworker.c, not src/backend/postmaster/undoworker.c. Same issue in pbu_undorequest.c's header (src/backend/access/undo/undorequest.c). Correct the path to match the real location.
💡 Suggested change
Before:
* IDENTIFICATION
* src/backend/postmaster/undoworker.c
After:
* IDENTIFICATION
* src/backend/access/undo/perbackend/pbu_undoworker.c
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L9-L9)
Stale file-header path from zheap: this file lives at src/backend/access/undo/perbackend/pbu_undorequest.c, not src/backend/access/undo/undorequest.c. Also fix the "oldesdXidHavingUndo" typo (should be "oldestXidHavingUndo") in the header prose below.
💡 Suggested change
Before:
* src/backend/access/undo/undorequest.c
After:
* src/backend/access/undo/perbackend/pbu_undorequest.c
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L157-L157)
Typo: "rollabck" -> "rollback".
💡 Suggested change
Before:
/* This is the hash table to store all the rollabck requests. */
After:
/* This is the hash table to store all the rollback requests. */
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L355-L355)
Typo: "accomodate" -> "accommodate".
💡 Suggested change
Before:
* backends and discard worker. The table must be able to accomodate all
After:
* backends and discard worker. The table must be able to accommodate all
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L947-L947)
Typo: "wrokers" -> "workers".
💡 Suggested change
Before:
* Returns true, if we can push the rollback request to undo wrokers, false,
After:
* Returns true, if we can push the rollback request to undo workers, false,
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1602-L1602)
Typo: "resuests" -> "requests".
💡 Suggested change
Before:
* that we push the resuests only when there is some space in
After:
* that we push the requests only when there is some space in
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L740-L740)
Typo: "wroker" -> "worker".
💡 Suggested change
Before:
* Get the dbid where the wroker should connect to and get the worker
After:
* Get the dbid where the worker should connect to and get the worker
📄 src/backend/access/undo/logical_revert_worker.c (L324-L329)
On the error path the transaction is never aborted before CommitTransactionCommand() runs. When process_revert_entry() (or ATMMarkReverted/ATMForget) throws, control lands in PG_CATCH, which only does EmitErrorReport()/FlushErrorState() and the retention check — it does NOT abort the transaction. After PG_END_TRY() the code unconditionally calls CommitTransactionCommand() on a transaction whose internal state was left inconsistent by the longjmp. The established pattern in this same tree (see UndoWorkerPerformRequest() in pbu_undoworker.c) is to AbortOutOfAnyTransaction() inside the catch under HOLD_INTERRUPTS(), and only commit on the success path. As written, an error during revert corrupts transaction state and can crash/PANIC. (high confidence)
💡 Suggested change
Before:
PG_END_TRY();
CommitTransactionCommand();
/* Immediately look for more work instead of sleeping */
continue;
After:
PG_END_TRY();
if (IsTransactionState())
CommitTransactionCommand();
/* Immediately look for more work instead of sleeping */
continue;
📄 src/backend/access/undo/logical_revert_worker.c (L241-L242)
Head-of-line blocking / starvation: ATMGetNextUnreverted() has no database filter and (via SLogTxnGetNextUnreverted) returns the globally-first unreverted entry in ascending-xid order regardless of database. When that first entry belongs to another database this worker does goto sleep and repeats the exact same lookup on every wake, so it never advances to the later entries that DO belong to MyDatabaseId. If a lower-xid entry for a different database is stuck at the head of the map (e.g. its worker crashed, or its database was dropped), this worker will indefinitely starve its own database's aborted-transaction UNDO. The comment 'Skip entries that belong to a different database' is not achievable without a db-filtered scan or an advancing cursor. Either add a dboid argument to ATMGetNextUnreverted or iterate past mismatched entries. (high confidence)
📄 src/backend/access/undo/undoworker.c (L114-L119)
These signal handlers do not save/restore errno. SetLatch() may call kill(), which can clobber errno, corrupting an in-progress errno value in the interrupted mainline code. Per the standard convention (see SignalHandlerForConfigReload / SignalHandlerForShutdownRequest in postmaster/interrupt.h, which is already included), save errno on entry and restore on exit. Simplest fix: reuse the shared handlers instead of open-coding these. (high confidence)
💡 Suggested change
Before:
undo_worker_sighup(SIGNAL_ARGS)
{
(void) postgres_signal_arg; /* unused */
ConfigReloadPending = true;
SetLatch(MyLatch);
}
After:
undo_worker_sighup(SIGNAL_ARGS)
{
int save_errno = errno;
(void) postgres_signal_arg; /* unused */
ConfigReloadPending = true;
SetLatch(MyLatch);
errno = save_errno;
}
📄 src/backend/access/undo/perbackend/pbu_undoaction.c (L360-L360)
Integer overflow/truncation: maintenance_work_mem is an int in KB and can be set as high as MAX_KILOBYTES (INT_MAX, ~2TB). maintenance_work_mem * 1024L is computed in long, but the result is stored into a 32-bit int. Once maintenance_work_mem exceeds ~2GB in KB the value overflows INT_MAX and truncates to a garbage/negative value. undo_apply_size is then compared against total_size (a Size, i.e. unsigned) in UndoRecordBulkFetch(): a negative int promotes to a huge unsigned, so the total_size >= undo_apply_size memory bound never trips and the bulk fetch accumulates unbounded memory. Use a 64-bit type end to end (the UndoRecordBulkFetch parameter is also int).
💡 Suggested change
Before:
int undo_apply_size = maintenance_work_mem * 1024L;
After:
Size undo_apply_size = (Size) maintenance_work_mem * 1024;
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L1275-L1276)
Non-portable and inconsistent LSN format specifier. checkPointRedo is an XLogRecPtr (uint64); %llX expects unsigned long long, which is not guaranteed to match uint64 on all platforms. Every other site that formats this same pg_undo/ filename uses "%016" INT64_MODIFIER "X" (see CheckPointUndoLogs and StartupUndoLogs). On a platform where uint64 != unsigned long long the comparison string built here will not match the filenames actually created, so CleanUpUndoCheckPointFiles silently fails to prune stale snapshots (and could misclassify files). Use the same INT64_MODIFIER form.
💡 Suggested change
Before:
snprintf(oldest_path, MAXPGPATH, "%016llX",
checkPointRedo);
After:
snprintf(oldest_path, MAXPGPATH, "%016" INT64_MODIFIER "X",
checkPointRedo);
📄 src/backend/access/undo/perbackend/pbu_undofile.c (L567-L575)
Stale-vfd hazard: the backend-local MRU caches a File keyed by (logno, spcNode, segno), but nothing invalidates it when a segment is unlinked or renamed. The discard/recycle path (PbuUndoLogDiscard and undolog_xlog_discard) rename or unlink segment files and call undofile_forget_sync(), yet undofile_forget_sync/undofile_close/undofile_unlink never clear undofile_mru. A subsequent undofile_get_segment_file() for the same key returns the cached vfd, which now refers (via the OS inode) to a recycled/renamed file - exactly the "would read entirely the wrong data" case the discard comment in pbu_undolog.c warns about. undofile_forget_sync() (and the close/unlink stubs) should drop the MRU entry when its (logno, tablespace, segno) matches.
📄 src/backend/access/undo/perbackend/pbu_discardworker.c (L9-L10)
Wrong IDENTIFICATION path (copy-paste from the original postmaster/discardworker.c). This file lives at src/backend/access/undo/perbackend/pbu_discardworker.c. Also the header banner still says "discardworker.c". Fix both to match the real path/name.
💡 Suggested change
Before:
* IDENTIFICATION
* src/backend/postmaster/discardworker.c
After:
* IDENTIFICATION
* src/backend/access/undo/perbackend/pbu_discardworker.c
📄 src/backend/access/undo/perbackend/pbu_undoaction.c (L9-L9)
Wrong IDENTIFICATION path (stale zheap path). The file is src/backend/access/undo/perbackend/pbu_undoaction.c; the banner also still reads "undoaction.c". Correct both.
💡 Suggested change
Before:
* src/backend/access/undo/undoaction.c
After:
* src/backend/access/undo/perbackend/pbu_undoaction.c
📄 src/backend/access/undo/perbackend/pbu_undoactionxlog.c (L10-L11)
Wrong IDENTIFICATION path (stale zheap path). The file is pbu_undoactionxlog.c under perbackend/; the banner also still reads "undoactionxlog.c". Correct both.
💡 Suggested change
Before:
* IDENTIFICATION
* src/backend/access/undo/undoactionxlog.c
After:
* IDENTIFICATION
* src/backend/access/undo/perbackend/pbu_undoactionxlog.c
📄 src/backend/access/undo/perbackend/pbu_undodiscard.c (L9-L9)
Wrong IDENTIFICATION path (stale zheap path). The file is pbu_undodiscard.c under perbackend/; the banner also still reads "undodiscard.c". Correct both.
💡 Suggested change
Before:
* src/backend/access/undo/undodiscard.c
After:
* src/backend/access/undo/perbackend/pbu_undodiscard.c
📄 src/backend/access/undo/perbackend/pbu_undoinsert.c (L9-L9)
Wrong IDENTIFICATION path (stale zheap path). The file is pbu_undoinsert.c under perbackend/; the banner also still reads "undoinsert.c". Correct both.
💡 Suggested change
Before:
* src/backend/access/undo/undoinsert.c
After:
* src/backend/access/undo/perbackend/pbu_undoinsert.c
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L15-L15)
Wrong IDENTIFICATION path (stale zheap path). The file is pbu_undolog.c under perbackend/; the banner also still reads "undolog.c". Correct both.
💡 Suggested change
Before:
* src/backend/access/undo/undolog.c
After:
* src/backend/access/undo/perbackend/pbu_undolog.c
📄 src/backend/access/undo/perbackend/pbu_undorecord.c (L9-L9)
Wrong IDENTIFICATION path (stale zheap path). The file is pbu_undorecord.c under perbackend/; the banner also still reads "undorecord.c". Correct both.
💡 Suggested change
Before:
* src/backend/access/undo/undorecord.c
After:
* src/backend/access/undo/perbackend/pbu_undorecord.c
📄 src/backend/access/undo/perbackend/pbu_recovery.c (L344-L345)
Misleading name/cast. MakeUndoRecPtr(logno, offset) returns a full UndoRecPtr (logno in the high bits), yet it is cast to UndoLogOffset and stored in a variable named next_insert. It happens to work because UndoLogOffset is a uint64, UndoRecPtrGetPageOffset() masks off the log bits, and end_ptr is later consumed as a full UndoRecPtr - so the value is a full pointer throughout, not an offset. The name and cast wrongly suggest a plain offset and invite a future off-by-a-log-number bug. Drop the cast and rename, e.g. UndoRecPtr next_insert = MakeUndoRecPtr(logno, log->meta.insert);.
📄 src/backend/access/undo/perbackend/pbu_undofile.c (L495-L495)
PathNameOpenFile() returns -1 (a negative File) on failure; the PostgreSQL convention is to test file < 0. Testing file <= 0 works only incidentally (VfdCache[0] is the reserved free-list header, so AllocateVfd never returns File 0), but it is non-idiomatic and diverges from md.c/fd.c usage. The same pattern recurs at undofile_get_segment_file() (undofile_mru.file <= 0 / > 0, Assert(... > 0)) and undofile_syncfiletag() (file <= 0). Use < 0 / >= 0 for consistency.
💡 Suggested change
Before:
if (file <= 0 && (!missing_ok || errno != ENOENT))
After:
if (file < 0 && (!missing_ok || errno != ENOENT))
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L717-L722)
GetRollbackHashKeyFromQueue() mutates the shared round-robin cursor cur_undo_queue only in the ERROR_QUEUE empty branch, but not in the XID_QUEUE/SIZE_QUEUE branches. This is a stray side-effect in what is documented as a pure "get the hash key" accessor. Two concrete consequences:
- In
UndoGetWork()'s main round-robin loop, when the current queue is ERROR_QUEUE and empty,cur_undo_queueis incremented here AND again by the caller (cur_undo_queue++; continue;), so the cursor advances by two and one queue is skipped on the next dispatch cycle. - In the peek loop,
cur_queueis a local iterator and the peek path must not touch the shared cursor at all, yet this branch still bumpscur_undo_queue, corrupting the launcher/worker round-robin state.
Remove the cur_undo_queue++ from this helper; let the callers own the cursor.
💡 Suggested change
Before:
/* check if there is a work in the next queue */
if (GetErrorQueueSize() <= n)
{
cur_undo_queue++;
return false;
}
After:
/* check if there is a work in the next queue */
if (GetErrorQueueSize() <= n)
return false;
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1631-L1634)
RollbackHTCleanup() acquires RollbackRequestLock in LW_SHARED mode but then calls hash_search(RollbackHT, &hkey, HASH_REMOVE, NULL), which mutates the shared dynahash. Every other mutator of RollbackHT in this file (RegisterRollbackReq, RollbackHTRemoveEntry, InsertRequestIntoErrorUndoQueue) holds LW_EXCLUSIVE. Removing entries under a shared lock races with concurrent readers/writers and can corrupt the hash table. This must be LW_EXCLUSIVE.
💡 Suggested change
Before:
/* Fetch the rollback requests */
LWLockAcquire(RollbackRequestLock, LW_SHARED);
Assert(hash_get_num_entries(RollbackHT) <= UndoRollbackHashTableSize());
After:
/* Fetch the rollback requests */
LWLockAcquire(RollbackRequestLock, LW_EXCLUSIVE);
Assert(hash_get_num_entries(RollbackHT) <= UndoRollbackHashTableSize());
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1292-L1294)
In the !remove_from_queue (launcher) path, StartTransactionCommand()/dbid_exists()/CommitTransactionCommand() are executed while RollbackRequestLock is held in LW_EXCLUSIVE mode (acquired at the top of UndoGetWork). Running a full transaction start/commit and a catalog lookup under a held LWLock acquires many other LWLocks (WAL insert, ProcArrayLock, buffer locks, etc.), which is a lock-ordering/deadlock hazard and violates the principle that LWLocks are held only for short, bounded critical sections. The dbid existence check should be done after releasing RollbackRequestLock (e.g. capture the dbid/key, drop the lock, check, then re-evaluate).
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L732-L734)
PbuUndoWorkerMain() installs only a SIGTERM handler (pqsignal(SIGTERM, die)) and never installs a SIGHUP handler, yet the main loop checks if (got_SIGHUP) { got_SIGHUP = false; ProcessConfigFile(PGC_SIGHUP); }. Because no handler sets got_SIGHUP in this process, the config-reload branch is dead code and the undo apply worker can never reload GUCs on pg_ctl reload. Install a SIGHUP handler (e.g. pqsignal(SIGHUP, UndoLauncherSighup) / SignalHandlerForConfigReload) so the reload actually happens.
💡 Suggested change
Before:
/* Setup signal handling */
pqsignal(SIGTERM, die);
BackgroundWorkerUnblockSignals();
After:
/* Setup signal handling */
pqsignal(SIGHUP, UndoLauncherSighup);
pqsignal(SIGTERM, die);
BackgroundWorkerUnblockSignals();
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L68-L71)
binaryheap_allocate_shm() (and its use of the ShmemInitStruct-based allocation) is defined but never called: shmem heaps are reserved via PendingUndoShmemRequest() and initialized in-place by binaryheap_init_shm(). This is dead scaffolding from the API-drift shim. Remove binaryheap_allocate_shm() to keep the diff minimal (YAGNI).
📄 src/backend/access/undo/perbackend/pbu_discardworker.c (L60-L60)
got_SIGTERM is assigned from the SIGTERM signal handler (undoworker_sigterm_handler) yet declared as a plain bool. A value written in a signal handler and read in the main loop must be volatile sig_atomic_t: otherwise the compiler is free to cache the read in the while (!got_SIGTERM) loop and the write from the handler is not guaranteed atomic/visible. Both sibling workers in this same change (logical_revert_worker.c and pbu_undoworker.c) correctly use static volatile sig_atomic_t got_SIGTERM.
💡 Suggested change
Before:
static bool got_SIGTERM = false;
After:
static volatile sig_atomic_t got_SIGTERM = false;
📄 src/backend/access/undo/perbackend/pbu_undofile.c (L432-L433)
The 1 + overshoots the current segment by one block. The number of blocks remaining in the current segment starting at blocknum is UNDOSEG_SIZE - (blocknum % UNDOSEG_SIZE) (compare md.c's mdwriteback, which uses RELSEG_SIZE - (blocknum % RELSEG_SIZE)). With the extra +1, a large nblocks yields an nflush that runs one block past the end of the segment file, so FileWriteback(file, (blocknum % UNDOSEG_SIZE) * BLCKSZ, nflush * BLCKSZ, ...) requests writeback of a byte range extending beyond the segment, and the loop then advances blocknum/nblocks incorrectly across the boundary.
💡 Suggested change
Before:
nflush = Min(nblocks,
1 + UNDOSEG_SIZE - (blocknum % UNDOSEG_SIZE));
After:
nflush = Min(nblocks,
UNDOSEG_SIZE - (blocknum % UNDOSEG_SIZE));
📄 src/include/access/perbackend/pbu_compat.h (L47-L49)
high confidence, portability hard-gate: hard-coding INT64_MODIFIER "ll" is wrong. In current master int64 is typedef int64_t int64 and the tree deliberately provides INT64_FORMAT/UINT64_FORMAT (defined as "%" PRId64 / "%" PRIu64) precisely because the length modifier is platform-dependent: on LP64 platforms (typical 64-bit Linux/BSD/Solaris) int64_t is long, so PRIu64 is "lu", not "llu". UndoRecPtrFormat "%016" INT64_MODIFIER "X" in pbu_undolog.h will therefore be "%016llX" against a uint64 argument that is unsigned long -- a format/argument width mismatch that is a defect on those platforms. Define UndoRecPtrFormat in terms of the sanctioned macros (e.g. "%016" INT64_MODIFIER "X" -> build it from UINT64_FORMAT/PRIx64) rather than reintroducing the removed INT64_MODIFIER with a fixed "ll".
📄 src/include/access/perbackend/pbu_compat.h (L161-L163)
medium confidence, maintainability/dead code: these three tranche-ID macros are never used. The engine registers its tranche dynamically via LWLockNewTrancheId("UndoLogPBU") in pbu_undolog.c (PbuUndoLogTrancheId) and initializes every pbu LWLock with that id; a code search for LWTRANCHE_UNDOLOG/LWTRANCHE_UNDODISCARD/LWTRANCHE_DISCARD_UPDATE in the pbu sources finds no callers. Beyond being dead scaffolding (YAGNI), deriving fixed offsets from LWTRANCHE_FIRST_USER_DEFINED is not the canonical pattern and would collide with any other subsystem doing the same. Remove them.
📄 src/include/access/perbackend/pbu_compat.h (L116-L119)
medium confidence, maintainability: the GetOldestXmin(rel, flags) shim silently discards flags. The discard worker calls GetOldestXmin(NULL, PROCARRAY_FLAGS_AUTOVACUUM | PROCARRAY_FLAGS_VACUUM) (pbu_discardworker.c:140) intending to IGNORE (auto)vacuum backends so undo can be discarded more aggressively; the shim instead returns the fully conservative horizon. As the comment argues, this only over-retains undo, so it is safe, but it is a real behavior change hidden behind a macro that looks like a passthrough. Also note the guard #ifndef PROCARRAY_FLAGS_DEFAULT bundles the GetOldestXmin redefinition with the flag defaults, so if any of these names ever reappears in a core header the redefinition silently drops. A static inline wrapper with an explicit (void)-cast of flags would be clearer and less fragile than a function-like macro.
📄 src/include/access/perbackend/pbu_compat.h (L66-L66)
medium confidence, comment accuracy: these Phase-2a/2b roadmap narratives describe process, not current behavior, and several are already stale. For example the header (and pbu_helpers.h) describe SetCurrentUndoLocation as a no-op wired "in Phase 2b", but it is already fully wired -- it calls SetCurrentTransactionPbuUndoLocation() which writes pbuUndoStartPtr/pbuUndoLatestPtr on the xact.c TransactionState. Per the project convention, comments must describe what the code does now; drop the phased-rollout prose or replace it with a factual statement of the current mapping.
📄 src/include/access/perbackend/pbu_undoinsert.h (L63-L63)
medium confidence, portability/fragility: this prototype uses the type name RelFileNode, which no longer exists in master -- it is only made valid by the #define RelFileNode RelFileLocator in pbu_compat.h, which is itself guarded by #ifndef FRONTEND. This header does not include pbu_compat.h, so it silently depends on every includer having defined that macro first, and it will not compile in any FRONTEND translation unit (where the alias is absent). It happens to work today only because no frontend/rmgrdesc TU includes this header. Use RelFileLocator directly in the prototype to remove the include-ordering footgun.
💡 Suggested change
Before:
RelFileNode rnode, UndoPersistence persistence,
After:
RelFileLocator rnode, UndoPersistence persistence,
📄 src/include/access/perbackend/pbu_undorecord.h (L45-L46)
medium confidence, portability (on-disk/WAL format): the [XXX:TODO: this creates an alignment hole?] marker is on the on-disk undo-record header. UndoRecordHeader is serialized to the undo page by a raw struct copy -- pbu_undorecord.c:193 does InsertUndoBytes((char *) &work_hdr, SizeOfUndoRecordHeader, ...) and reads it back the same way -- so any compiler padding between urec_rmid (RmgrId/uint8) and the following fields becomes part of the persisted format, making it dependent on struct layout/ABI and thus non-portable across compilers/architectures. This must be resolved (either serialize field-by-field or pack/reorder the header explicitly and document the fixed layout) before this format ships; an unresolved TODO on a persistent format is not commit-ready.
📄 src/include/access/perbackend/pbu_helpers.h (L9-L10)
high confidence, comment accuracy: this description is stale/false. SetCurrentUndoLocation is NOT a no-op and is already wired into xact.c: its implementation (pbu_helpers.c) calls SetCurrentTransactionPbuUndoLocation(), which stores pbuUndoStartPtr/pbuUndoLatestPtr on the current TransactionState, and it has a live caller in pbu_undoinsert.c (InsertPreparedUndo path). Remove the "Phase 2a: ... no-op ... wired ... in Phase 2b" claim; describe the current behavior.
📄 src/include/access/perbackend/pbu_helpers.h (L48-L50)
medium confidence, comment accuracy (same file): this trailing comment repeats the false "wired to xact.c in Phase 2b" claim for SetCurrentUndoLocation, which is already wired and in use. Update it to describe the current behavior.
💡 Suggested change
Before:
/* Record the transaction's current undo insertion location (wired to xact.c
* in Phase 2b). */
extern void SetCurrentUndoLocation(UndoRecPtr urec_ptr);
After:
/* Record the transaction's current per-backend undo insertion location on the
* current TransactionState (see SetCurrentTransactionPbuUndoLocation). */
extern void SetCurrentUndoLocation(UndoRecPtr urec_ptr);
📄 src/include/access/perbackend/pbu_undoaction_xlog.h (L3-L4)
low confidence, comment accuracy: the file banner still carries the pre-rename identity -- title undoaction_xlog.h and path src/include/access/undoaction_xlog.h -- but the actual file is pbu_undoaction_xlog.h under src/include/access/perbackend/. Stale identity headers mislead maintainers; fix the title and IDENTIFICATION path.
💡 Suggested change
Before:
* undoaction_xlog.h
* undo action XLOG definitions
After:
* pbu_undoaction_xlog.h
* undo action XLOG definitions
📄 src/include/access/perbackend/pbu_undoaction_xlog.h (L9-L9)
low confidence, comment accuracy: the IDENTIFICATION path is the old pre-rename path; it should match the real location.
💡 Suggested change
Before:
* src/include/access/undoaction_xlog.h
After:
* src/include/access/perbackend/pbu_undoaction_xlog.h
📄 src/include/access/perbackend/pbu_undodiscard.h (L3-L4)
low confidence, comment accuracy: the banner title and path are copy-pasted from undoinsert -- title says undoinsert.h and path src/include/access/undodiscard.h, but this file is pbu_undodiscard.h under perbackend/. Correct both.
💡 Suggested change
Before:
* undoinsert.h
* undo discard definitions
After:
* pbu_undodiscard.h
* undo discard definitions
📄 src/include/access/perbackend/pbu_undoinsert.h (L3-L4)
low confidence, comment accuracy: stale banner identity -- title undoinsert.h and path src/include/access/undoinsert.h do not match the real pbu_undoinsert.h under perbackend/.
💡 Suggested change
Before:
* undoinsert.h
* entry points for inserting undo records
After:
* pbu_undoinsert.h
* entry points for inserting undo records
📄 src/include/access/perbackend/pbu_undolog.h (L3-L4)
low confidence, comment accuracy: stale banner identity -- title undolog.h and path src/include/access/undolog.h do not match the real pbu_undolog.h under perbackend/ (and there is a separate access/undolog.h in this same patch, so the mismatch is actively confusing).
💡 Suggested change
Before:
* undolog.h
*
After:
* pbu_undolog.h
*
📄 src/include/access/perbackend/pbu_undolog_xlog.h (L3-L4)
low confidence, comment accuracy: stale banner identity -- title undolog_xlog.h and path src/include/access/undolog_xlog.h do not match the real pbu_undolog_xlog.h under perbackend/.
💡 Suggested change
Before:
* undolog_xlog.h
* undo log access XLOG definitions.
After:
* pbu_undolog_xlog.h
* undo log access XLOG definitions.
📄 src/include/access/perbackend/pbu_undorecord.h (L3-L4)
low confidence, comment accuracy: stale banner identity -- title undorecord.h and path src/include/access/undorecord.h do not match the real pbu_undorecord.h under perbackend/ (a separate access/undorecord.h also exists in this patch).
💡 Suggested change
Before:
* undorecord.h
* encode and decode undo records
After:
* pbu_undorecord.h
* encode and decode undo records
📄 src/include/access/perbackend/pbu_undoworker.h (L16-L18)
Dead declaration: UndoWorkerDelay has no definition, GUC registration, or reader anywhere in the tree (verified via code_search across all *.c). This is unused scaffolding (YAGNI) that the community rejects. Either wire up the GUC or drop the declaration.
📄 src/include/access/perbackend/pbu_undoworker.h (L3-L9)
File banner is stale/copy-pasted: title says undoworker.h and path says src/include/access/undoworker.h, but this file is undoworker.h under perbackend/ with guard PBU_UNDOWORKER_H. Note a real src/include/access/undoworker.h also exists in this change, so the misleading path is actively confusing. The 1996-2019 copyright also predates this new file. Fix the banner path/name.
📄 src/include/access/perbackend/pbu_undorequest.h (L3-L8)
File banner is stale/copy-pasted: title says undorequest.h and path says src/include/access/undorequest.h, but this file lives at src/include/access/perbackend/pbu_undorequest.h (guard PBU_UNDOREQUEST_H). The 1996-2019 copyright also predates this new file. Correct the banner path/name so it matches the actual file.
📄 src/include/access/perbackend/pbu_undorequest.h (L111-L118)
SetUndoRequestInfoFromRHEntry does not parenthesize its urinfo/rh arguments, unlike the sibling ResetUndoRequestInfo above which correctly wraps (urinfo). This is a macro footgun if any caller ever passes an expression. It also silently leaves request_size uninitialized in the OUT struct (the sibling ResetUndoRequestInfo zeroes it), so a caller relying on request_size after this macro gets whatever ResetUndoRequestInfo last set. Parenthesize the arguments and confirm the partial initialization is intentional.
💡 Suggested change
Before:
#define SetUndoRequestInfoFromRHEntry(urinfo, rh, cur_queue) \
( \
urinfo->full_xid = rh->full_xid, \
urinfo->start_urec_ptr = rh->start_urec_ptr, \
urinfo->end_urec_ptr = rh->end_urec_ptr, \
urinfo->dbid = rh->dbid, \
urinfo->undo_worker_queue = cur_queue \
)
After:
#define SetUndoRequestInfoFromRHEntry(urinfo, rh, cur_queue) \
( \
(urinfo)->full_xid = (rh)->full_xid, \
(urinfo)->start_urec_ptr = (rh)->start_urec_ptr, \
(urinfo)->end_urec_ptr = (rh)->end_urec_ptr, \
(urinfo)->dbid = (rh)->dbid, \
(urinfo)->undo_worker_queue = (cur_queue) \
)
📄 src/include/access/perbackend/pbu_undorequest.h (L28-L28)
Portability: InvalidUndoWorkerQueue is a bare -1 but is stored into the enum-typed field UndoRequestInfo.undo_worker_queue (and UndoApplyWorker.undo_worker_queue in pbu_undoworker.c: worker->undo_worker_queue = InvalidUndoWorkerQueue;). The underlying type of an enum with only non-negative enumerators is implementation-defined and may be unsigned; on such a compiler -1 becomes a large positive value and any equality check against InvalidUndoWorkerQueue (or signed compare) can misbehave. Add a negative enumerator to UndoWorkerQueueType (or make the sentinel an enum value) rather than relying on a -1 macro assigned to an enum field.
📄 src/include/access/perbackend/pbu_undorequest.h (L128-L129)
Header/implementation parameter-name mismatch: this prototype names the second parameter is_undo_launcher, but the definition in pbu_undorequest.c uses remove_from_queue (and its doc comment describes remove_from_queue). is_undo_launcher appears nowhere in the tree. Align the header parameter name with the implementation to avoid misleading callers.
💡 Suggested change
Before:
extern bool UndoGetWork(bool allow_peek, bool is_undo_launcher,
UndoRequestInfo *urinfo, bool *in_other_db);
After:
extern bool UndoGetWork(bool allow_peek, bool remove_from_queue,
UndoRequestInfo *urinfo, bool *in_other_db);
📄 src/backend/access/undo/perbackend/pbu_undoinsert.c (L500-L503)
This assert checks the wrong variable. idx is the function parameter (the xact_urec_info[] slot index) and is invariant across this loop; the loop variable that actually indexes xact_urec_info[idx].idx_undo_buffers[i] (a MAX_BUFFER_PER_UNDO-sized array) is i, which increments each iteration. The guard against overrunning idx_undo_buffers[] must bound i, not idx. As written it can never fire on a runaway loop, and the analogous loops in PrepareUndoInsert/InsertPreparedUndo correctly assert the incrementing buffer index.
💡 Suggested change
Before:
starting_byte = UndoLogBlockHeaderSize;
i++;
Assert(idx < MAX_BUFFER_PER_UNDO);
After:
starting_byte = UndoLogBlockHeaderSize;
i++;
Assert(i < MAX_BUFFER_PER_UNDO);
📄 src/include/access/perbackend/pbu_undorequest.h (L121-L121)
PendingUndoShmemSize() is declared to return int, but its definition (pbu_undorequest.c:1024) computes a Size via hash_estimate_size/add_size/mul_size and return size;, and the sole caller (undo.c:115) feeds the result into add_size(size, PendingUndoShmemSize()) where size is Size. Returning through int truncates/overflows the shared-memory estimate on 64-bit builds when the sizing exceeds INT_MAX (large hash table / queue element arrays), silently under-reserving shmem. Every sibling ShmemSize function returns Size (e.g. UndoLauncherShmemSize in pbu_undoworker.h). Change the return type to Size here and at the definition. (high confidence)
💡 Suggested change
Before:
extern int PendingUndoShmemSize(void);
After:
extern Size PendingUndoShmemSize(void);
📄 src/backend/access/undo/perbackend/pbu_recovery.c (L393-L395)
This PG_CATCH does not release resources held at the point of ERROR. execute_undo_actions() -> UndoRecordBulkFetch/UndoFetchRecord/UndoGetOneRecord acquire LWLocks (log->discard_lock, log->mutex) and pin+content-lock undo buffers, and UnlockReleaseUndoBuffers() only runs on the success path. When one of them throws, this handler merely EmitErrorReport()/FlushErrorState() and continues the chain walk, so those LWLocks and buffer pins/content locks leak; the next iteration that re-acquires log->discard_lock will self-deadlock. The sibling undo apply worker (undoworker.c) shows the required cleanup on its error path: LWLockReleaseAll(), UnlockBuffers(), AtEOXact_Buffers(false), etc. Add equivalent resource release here before deferring.
💡 Suggested change
Before:
MemoryContextSwitchTo(workctx);
EmitErrorReport();
FlushErrorState();
After:
MemoryContextSwitchTo(workctx);
EmitErrorReport();
FlushErrorState();
/* Release locks/buffers the failed apply may still hold. */
LWLockReleaseAll();
UnlockBuffers();
UnlockReleaseUndoBuffers();
📄 src/include/access/perbackend/pbu_undoinsert.h (L48-L48)
Dead declaration: UndoRecordSetPrevUndoLen has no definition and no caller anywhere in the tree (a code search finds only this extern). A dangling extern for an undefined symbol is dead scaffolding -- if a future caller ever appears it will link-error. Drop it (YAGNI/minimalism).
📄 src/include/access/perbackend/pbu_undolog.h (L327-L328)
Dead declarations: UndoLogSetPrevLen and UndoLogGetPrevLen are declared here but have no definition and no caller anywhere in the tree. These dangling externs are dead scaffolding (and would produce a link error if ever called). Remove them.
📄 src/include/access/perbackend/pbu_helpers.h (L44-L46)
Stale/contradictory comment. "provided as a no-op for now" reads as deferred work, but the actual implementation (pbu_helpers.c) documents it as "intentionally a no-op rather than deferred work" because master's elog machinery has no per-error client-toggle. Drop the "for now" and state that it is a deliberate no-op, matching the .c comment.
💡 Suggested change
Before:
/* zheap error-handling helper: control whether the current error reaches the
* client. Master has no equivalent; provided as a no-op for now. */
extern void err_out_to_client(bool status);
After:
/* zheap error-handling helper: control whether the current error reaches the
* client. Master has no equivalent; this is intentionally a no-op. */
extern void err_out_to_client(bool status);
📄 src/include/access/perbackend/pbu_helpers.h (L32-L32)
Style: hex literal uses an uppercase X (0XFFFFFFFF); the tree consistently writes hex constants with lowercase 0x. Also the & 0xFFFFFFFF is redundant since the (uint32) cast already truncates to 32 bits. Note GetEpochFromEpochXid below has no caller (dead macro) -- consider dropping it too.
💡 Suggested change
Before:
#define GetXidFromEpochXid(epochxid) ((uint32) (epochxid) & 0XFFFFFFFF)
After:
#define GetXidFromEpochXid(epochxid) ((uint32) (epochxid))
📄 src/backend/access/rmgrdesc/undodesc.c (L71-L94)
The operation-name mapping here is wrong and will mislabel records in pg_waldump. xl_undo_apply.operation_type is set to the AM-specific info code at the CLR generation sites (nbtree_undo.c/hash_undo.c both do xlrec.operation_type = info;). For nbtree those codes are NBTREE_UNDO_* (nbtree.h): 0x0001=INSERT_LEAF, 0x0002=INSERT_UPPER, 0x0004=INSERT_POST, 0x0005=DELETE, 0x0006=SPLIT_L, 0x0009=DEDUP. This descriptor instead prints 0x0002="DELETE", 0x0004="PRUNE", 0x0005="INPLACE", 0x0006="HOT_UPDATE", and 0x0009="UNKNOWN" -- none of which match the actual value written. The heap-style INSERT/DELETE/UPDATE/PRUNE/INPLACE/HOT_UPDATE names appear to be aspirational and do not correspond to any emitter in this change. This produces actively misleading WAL dumps. (high confidence)
📄 src/backend/access/rmgrdesc/undodesc.c (L69-L71)
Hardcoded magic numbers for operation_type. Because operation_type carries per-RM info codes (there is no shared canonical operation-type enum), a single hardcoded table here cannot be correct for all emitters and will silently drift. Either drop the name translation and print the raw operation_type value, or dispatch on the emitting RM and use its named constants (e.g. NBTREE_UNDO_*). (high confidence)
📄 src/backend/access/rmgrdesc/undodesc.c (L37-L39)
Tree convention prints uint64/UndoRecPtr with UINT64_FORMAT (or LSN-style %X/%X via LSN_FORMAT_ARGS, as XLOG_UNDO_BATCH does for chain_prev just below). The (unsigned long long) + %llu idiom used here is portable but is not used by any existing rmgrdesc file and is inconsistent with the batch descriptor in this same file. Prefer UINT64_FORMAT for these UndoRecPtr fields for consistency. (moderate confidence)
📄 src/include/access/logical_revert_worker.h (L25-L25)
Header files must not include "postgres.h"; that is the job of each .c file (which must include it first). The only tree headers that include postgres.h are special cases (PCH, regex/snowball self-contained runtimes) -- the other occurrences are all new files from this same change. Drop this include and pull in only the specific headers actually needed for the declarations (Datum, Size, Oid come from postgres.h/c.h transitively at the .c include site). (high confidence)
📄 src/include/access/logical_revert_worker.h (L40-L41)
Cross-module extern GUC variables must be annotated PGDLLIMPORT so they are readable from other modules/extensions on Windows/MSVC (compare xact.h, which marks every exported GUC extern PGDLLIMPORT int). Mark these extern PGDLLIMPORT int. (moderate confidence)
📄 src/include/access/undoworker.h (L50-L51)
These exported GUCs are declared as plain extern int. Per tree convention (see xact.h) cross-module extern variables need PGDLLIMPORT for Windows/MSVC. Mark them extern PGDLLIMPORT int. (moderate confidence)
📄 src/include/access/xactundo.h (L140-L140)
undo_instant_abort_threshold is an exported GUC (defined in xactundo.c, documented for reading elsewhere). Declare it extern PGDLLIMPORT int to keep it readable from other modules/extensions on Windows/MSVC, matching the convention in xact.h. (moderate confidence)
📄 src/include/access/undostats.h (L56-L57)
This comment states pg_undo_force_discard is reached via pg_proc by name, but there is no pg_proc.dat entry for it (nor for the stats function) anywhere in this change -- only PG_FUNCTION_INFO_V1 in undostats.c. Without a pg_proc registration the function is not SQL-callable, yet examples/05-undo-monitoring.sql does SELECT pg_undo_force_discard();. Add the function(s) to pg_proc.dat with developer-range OIDs (8000-9999). (high confidence)
📄 src/include/access/undormgr.h (L45-L48)
This comment is inaccurate and actively misleading. In this same change, FLUX (UNDO_RMID_FLUX=5, flux_undo.h) and FILEOPS (UNDO_RMID_FILEOPS=2, fileops.h) are compiled in-core and registered unconditionally via undormgrlist.h, and RECNO uses UNDO_RMID_RECNO=7 (recno_undo.h) which this comment does not mention at all. So of "2, 4, and 5", two of the three are in-core here, not out-of-core, and the actual assignment is: 1=NBTREE, 2=FILEOPS, 3=HASH, 5=FLUX, 7=RECNO. Since these IDs are stamped into WAL-durable UNDO records and dispatch on rollback/redo, an accurate, single-source-of-truth allocation table is important to prevent a future consumer from reusing an in-use value. Update this comment to reflect the real allocations (or centralize them here). Confidence: high.
💡 Suggested change
Before:
#define UNDO_RMID_INVALID 0
#define UNDO_RMID_NBTREE 1
#define UNDO_RMID_HASH 3
/* 2, 4, and 5 are reserved by out-of-core consumers (see their own headers) */
After:
#define UNDO_RMID_INVALID 0
#define UNDO_RMID_NBTREE 1
#define UNDO_RMID_HASH 3
/*
* Other IDs are defined by their consumers in their own headers:
* 2 = FILEOPS (storage/fileops.h), 5 = FLUX (access/flux_undo.h),
* 7 = RECNO (access/recno_undo.h). 4 and 6 are currently unused.
* IDs are WAL-durable: values must be unique and never reused.
*/
📄 src/include/access/undormgrlist.h (L20-L23)
This paragraph directly contradicts the file it heads: the list below is not empty, it registers five built-in resource managers (Nbtree, Hash, Flux, Recno, Fileops). The "starts empty / no built-in resource managers exist yet" narrative is stale aspirational text describing a state that never shipped. Remove or rewrite it to describe the current contents. Confidence: high.
📄 src/include/access/undolog.h (L147-L154)
These exported globals lack PGDLLIMPORT. undo_batch_size_kb / undo_batch_record_limit are already read from other translation units (e.g. undobuffer.c), and the GUC convention across the tree (see miscadmin.h: work_mem, NBuffers, maintenance_work_mem, etc.) is that every extern variable in a header that is referenced cross-module or by an extension must be declared PGDLLIMPORT. Without it the Windows/MSVC build fails to link these symbols from other modules or extensions. Same applies to extern UndoLogSharedData *UndoLogShared; below, and to extern MemoryContext UndoContext; (undo.h) and extern const UndoRmgrData *UndoRmgrs[MAX_UNDO_RMGRS]; (undormgr.h). Confidence: high.
💡 Suggested change
Before:
extern int undo_retention_time;
extern int undo_worker_naptime;
extern int undo_buffer_size;
extern bool pbu_undo_workers_enabled;
extern int undo_rollback_overflow_size;
extern int undo_max_wal_retention_size;
extern int undo_batch_size_kb;
extern int undo_batch_record_limit;
After:
extern PGDLLIMPORT int undo_retention_time;
extern PGDLLIMPORT int undo_worker_naptime;
extern PGDLLIMPORT int undo_buffer_size;
extern PGDLLIMPORT bool pbu_undo_workers_enabled;
extern PGDLLIMPORT int undo_rollback_overflow_size;
extern PGDLLIMPORT int undo_max_wal_retention_size;
extern PGDLLIMPORT int undo_batch_size_kb;
extern PGDLLIMPORT int undo_batch_record_limit;
📄 src/include/access/undolog.h (L138-L139)
UndoLogShared is dereferenced from many translation units (undo_xlog.c, undostats.c, undoworker.c, etc.) and must be PGDLLIMPORT for the MSVC/cross-module build. See the same PGDLLIMPORT requirement applied to the GUCs above. Confidence: high.
💡 Suggested change
Before:
/* Global shared memory pointer (set during startup) */
extern UndoLogSharedData *UndoLogShared;
After:
/* Global shared memory pointer (set during startup) */
extern PGDLLIMPORT UndoLogSharedData *UndoLogShared;
📄 src/include/access/undo.h (L49-L50)
UndoContext is an exported global; if it is (or will be) referenced outside undo.c or by an extension it needs PGDLLIMPORT for the Windows/MSVC build, matching the tree convention (see MemoryContext exports elsewhere). Confidence: medium.
💡 Suggested change
Before:
/* Memory context for undo-related allocations */
extern MemoryContext UndoContext;
After:
/* Memory context for undo-related allocations */
extern PGDLLIMPORT MemoryContext UndoContext;
📄 src/include/access/undormgr.h (L106-L107)
This exported dispatch table needs PGDLLIMPORT if it is read cross-module or by an extension on Windows/MSVC. Confidence: medium (currently accessed only via GetUndoRmgr(), but the array is exported in a public header).
💡 Suggested change
Before:
/* Global registration table */
extern const UndoRmgrData *UndoRmgrs[MAX_UNDO_RMGRS];
After:
/* Global registration table */
extern PGDLLIMPORT const UndoRmgrData *UndoRmgrs[MAX_UNDO_RMGRS];
📄 src/include/access/undo_xlog.h (L267-L271)
xl_undo_batch is a WAL-durable record that is written and read as a raw struct: undoinsert.c/undobuffer.c do XLogRegisterData((char *) &xlrec, SizeOfUndoBatch) and undo_xlog.c reads it back via (xl_undo_batch *) record_data. That embeds the C layout -- including inter-field padding and the width of the UndoPersistenceLevel enum -- directly into WAL. Enum storage width is implementation-defined, so this on-disk format is not portable across compilers/ABIs and is a WAL-compatibility hazard per the portability rules. Use a fixed-width integer field (e.g. uint8) for the persistence level in the wire struct and pack fields to avoid padding, or serialize field-by-field rather than casting the raw struct. Confidence: high.
💡 Suggested change
Before:
UndoPersistenceLevel persistence; /* Persistence level of this batch */
/* Followed by total_len bytes of serialized UndoRecordHeader+payload */
} xl_undo_batch;
#define SizeOfUndoBatch (offsetof(xl_undo_batch, persistence) + sizeof(UndoPersistenceLevel))
After:
uint8 persistence; /* UndoPersistenceLevel of this batch */
/* Followed by total_len bytes of serialized UndoRecordHeader+payload */
} xl_undo_batch;
#define SizeOfUndoBatch (offsetof(xl_undo_batch, persistence) + sizeof(uint8))
📄 src/include/access/undorecord.h (L75-L75)
By the field's own comment, urec_prev is written by the record builders (undorecord.c, undobuffer.c) but never read by the apply path -- the real chain is between batches via xl_undo_batch.chain_prev, and order within a batch is positional. This is a deliberately dead field that costs 8 bytes in every UNDO record header on the DML hot write path (header documented as 40 bytes -> ~20% is this unused field). Per YAGNI/minimal-diff, drop the field (and its writes) unless there is a concrete near-term reader; "format stability" for a field no reader consumes is not sufficient justification for a permanent per-record hot-path cost. Confidence: high.
📄 src/include/access/undolog.h (L185-L197)
This is a block of exported functions that are documented as unconditional no-ops, yet they still have live callers (e.g. UndoLogCloseFiles/UndoFlushResetMaxWritePtr from xactundo.c, ExtendUndoLogFile from the undo_xlog.c redo path, UndoLogDeleteSegmentFile/UndoLogSealAndRotate from undoworker.c/undostats.c). Real-sounding names that silently do nothing are a footgun: a reader of the redo path will believe ExtendUndoLogFile extends a file when it does nothing. Per the minimalism discipline, remove the no-op API and its call sites rather than shipping a permanent dead-stub layer. At minimum, do not ship this as new public API. Confidence: high.
📄 src/include/access/undo_bufmgr.h (L43-L47)
The claim "value 9 is reserved for this purpose" is not backed by anything in the catalog: pg_database.dat only pins OID 1 (template1); template0/postgres get OIDs from the normal counter at initdb time. Nothing prevents a real database from being assigned dbOid 9, at which point undo BufferTags (dbOid==9) alias real relation buffers -- wrong-page reads / corruption. Since IsUndoRelFileLocator()/IsUndoBufferTag() distinguish undo buffers solely by dbOid==9, this is a correctness dependency on an unenforced assumption. Either reserve OID 9 properly (a pinned pseudo-database entry / documented reserved OID) or add a StaticAssert / runtime guard, and fix the comment to state where the reservation is actually enforced. Confidence: medium.
📄 src/include/access/undo_bufmgr.h (L251-L253)
Gating a public extern declaration behind #ifdef BUFMGR_INTERNALS_H makes the declaration silently disappear depending on header include order: a .c file that includes undo_bufmgr.h before buf_internals.h loses the prototype and gets an implicit-declaration warning / link error. This ordering-dependent conditional declaration is unusual for the tree. Prefer including buf_internals.h from this header (or moving these two declarations into buf_internals.h alongside BufferTag), so the declaration is unconditional. Note also UndoMakeBufferTag/IsUndoBufferTag currently have no callers -- consider dropping them entirely (YAGNI). Confidence: medium.
📄 src/include/access/undo_bufmgr.h (L128-L131)
This narrative contradicts the stated UNDO-in-WAL architecture at the top of this very header ("UNDO records are embedded in the WAL stream ... There are no separate UNDO segment files"). There is no pwrite()/pread() segment-file write path anymore, and the retained buffer-pool API (ReadUndoBuffer/ReadUndoBufferExtended/InvalidateUndoBuffers) has no in-tree callers. Stale/aspirational design text like this misleads maintainers about which path is live. Remove the pwrite/pread/"pre-allocated file" description, and either wire up or delete the now-dead ReadUndoBuffer* API. Confidence: high.
📄 src/include/access/undo_bufmgr.h (L73-L76)
Comments must describe present behavior and must not reference internal task trackers. "reserved for future use ... (Task #5)" is aspirational and an internal reference that should not ship. Same class of issue as "Eventually per-tablespace undo logs may be supported, but for now ..." a few lines above, and "reserved for future pre-sizing but not used currently" in undobuffer.h (the unused nrows param). Remove the future-tense/task-tracker text; if UNDO_FORKNUM is genuinely unused, drop it. Confidence: high.
📄 src/include/access/xactundo.h (L128-L129)
Naming inconsistency: this is the only function in the undo subsystem that uses the XAct prefix (capital A). Every other exported symbol in this header and in xactundo.c uses Xact -- XactUndoFlushPending, GetCurrentXactUndoRecPtr, AtCommit_XactUndo, etc. Rename to XactUndoUpdateLastBatchLSN for consistency (and update the definition in xactundo.c and the call site in undobuffer.c). (high confidence)
📄 src/include/access/undostats.h (L19-L20)
#include "fmgr.h" is unnecessary here. The comment below explicitly states the SQL-callable function is declared via PG_FUNCTION_INFO_V1 in undostats.c, not in this header, and neither GetUndoLogStats nor GetUndoBufferStats uses any fmgr type (Datum/PG_FUNCTION_ARGS). Drop the include to keep the header minimal and avoid pulling fmgr into every consumer. (moderate confidence)
📄 src/backend/access/rmgrdesc/undodesc.c (L168-L171)
The outer switch (info) has no default case: for any unrecognized info code the function returns without appending anything, so pg_waldump prints an empty desc for that record. Add a default: branch that appends something like "unknown info %02X" so unexpected records are still identifiable in WAL dumps. (moderate confidence)
📄 src/backend/access/hash/hash_undo.c (L359-L365)
[medium confidence] The overflow-chain walk violates the hash AM bucket-scan locking protocol. Per src/backend/access/hash/README (lines 258-285), a reader/scanner must retain the pin on the primary bucket page for the entire traversal precisely to protect its current position against concurrent bucket splits and overflow compaction (_hash_squeezebucket). This apply path instead reads each block independently, releasing the previous buffer before locking the next, and never holds the primary-bucket pin. A concurrent squeeze/split can therefore move the target entry off the page being examined, or rewrite hasho_nextblkno underneath the walk. Because verification is positive (heap_tid + digest + length), the worst case here is a spurious miss (entry left for VACUUM) rather than killing a committed entry, so it is fail-safe -- but it does not follow the documented protocol and can silently fail to reverse aborted entries under concurrency. At minimum document this deviation and its fail-safe rationale; ideally pin the primary bucket for the walk.
📄 src/backend/access/hash/hash_undo.c (L431-L432)
[medium confidence] The CLR redo path does not re-run the verification that the forward-apply path relies on. On the primary, hash_undo_apply() only marks a slot LP_DEAD after hash_undo_verify_entry() confirms heap_tid + key_digest + itup_sz all match, precisely because a relocated slot may hold a COMMITTED entry. The CLR is logged with REGBUF_STANDARD (not REGBUF_FORCE_IMAGE), so on a standby the common redo case is BLK_NEEDS_REDO, where undo_redo() (undo_xlog.c, XLOG_UNDO_APPLY_RECORD) blindly does ItemIdMarkDead(PageGetItemId(page, xlrec->target_offset)) guarded only by ItemIdIsNormal(). If the standby page diverged (different insert order after the recorded insert), replay can mark a committed entry LP_DEAD and lose data. This mirrors nbtree_undo.c and the actual redo logic lives outside this file, but the hazard is real for the hash INSERT undo written here. Confirm the design guarantees the standby offset cannot diverge, or carry the discriminator into the CLR and re-verify in redo.
📄 src/backend/access/hash/hash_undo.c (L422-L425)
[high confidence] xl_undo_apply has a 2-byte internal padding hole (after the three adjacent uint16 fields target_offset/operation_type/clr_flags, before the 4-byte-aligned tuple_len), and this stack variable is only field-assigned, never zeroed. XLogRegisterData((char *) &xlrec, SizeOfUndoApply) then writes the uninitialized pad bytes into WAL. This is nondeterministic content in the WAL stream and will trip wal_consistency_checking (FPI mismatch) between primary and standby. Add memset(&xlrec, 0, sizeof(xlrec)) before assigning fields. (Same defect exists in nbtree_undo.c; fixing both is warranted.)
💡 Suggested change
Before:
XLogRecPtr clr_lsn;
xl_undo_apply xlrec;
xlrec.urec_ptr = urec_ptr;
After:
XLogRecPtr clr_lsn;
xl_undo_apply xlrec;
MemSet(&xlrec, 0, sizeof(xlrec));
xlrec.urec_ptr = urec_ptr;
📄 src/backend/access/hash/hash_undo.c (L368-L371)
[low confidence] After the flags check, HashPageGetOpaque() is called on any page whose special size is non-zero. On a corrupt/foreign page with a non-zero but wrong special size this reinterprets arbitrary bytes as HashPageOpaque. The PageIsNew / special-size-zero guard does not establish that the special area is actually hash-sized. Consider validating PageGetSpecialSize(page) == MAXALIGN(sizeof(HashPageOpaqueData)) before dereferencing, so a garbage page is skipped rather than misread.
📄 src/backend/access/hash/hash_undo.c (L64-L65)
[low confidence] key_digest is only 32 bits. Verification passes when heap_tid + itup_sz + key_digest all match. In a normal index a heap TID appears once, so this is effectively fail-safe, but the header comment overstates it as an absolute guarantee ("no two legitimate entries in one index share all three"). A 32-bit digest collision combined with an identical heap_tid and length would let a committed entry be marked LP_DEAD (data loss), not just skipped. Since the failure mode of a collision is fail-open (kill), consider strengthening the comment to state the residual risk honestly, or widen the discriminator.
📄 src/backend/access/hash/hashinsert.c (L243-L249)
[low confidence] Two consecutive comment blocks precede this single check; the first ("Write UNDO record for the insertion...") is redundant with the second and its parenthetical about BufferGetBlockNumber is stale (the offset/blkno are captured inside HashUndoLogInsert while the pin is held, but the sole justification here duplicates the second block). Collapse to one minimal comment. Also note HashUndoLogInsert -> PrepareXactUndoDataParts can palloc and DeferXactUndoData can flush WAL (XactUndoFlushPending) while the metapage BUFFER_LOCK_EXCLUSIVE is still held (released only below); this matches the nbtree pattern but holding a buffer content lock across palloc/WAL-flush is worth calling out.
📄 src/include/access/undodefs.h (L22-L29)
Dead typedefs: UndoRecordSize, UndoLogNumber, and UndoLogOffset are never used by the new undo core (undorecord.c/undolog.c/undo.c/xactundo.c/undoapply.c/undoinsert.c). The only consumers, the perbackend code, define their own identical typedefs in access/perbackend/pbu_undolog.h and never pull these in. UndoLogOffset here is referenced only by the dead UndoRecordSetChunkHeader.size field (see undorecord.h). Per YAGNI/minimalism, drop the unused typedefs from this widely-included header. Confidence: high.
📄 src/include/access/undorecord.h (L107-L112)
Dead scaffolding: UndoRecordSetChunkHeader and its SizeOfUndoRecordSetChunkHeader macro have zero references anywhere in the tree (only this definition and the README). The multi-chunk / multi-log design they describe is not wired up. Additionally, the UndoRecordSetType enum's URST_INVALID/URST_MULTI/URST_EPHEMERAL are never used, and uset->type is written once (undorecord.c:154) and never read. This is speculative scaffolding; remove it to keep the patch minimal and avoid a fake on-disk format (SizeOfUndoRecordSetChunkHeader uses offsetof+sizeof but the struct is never serialized). Confidence: high.
📄 src/include/access/undorecord.h (L200-L202)
UndoWalBatchFlush/UndoWalBatchReset are documented here as "WAL batch management for deferred UNDO allocation logging," but their implementations in undoinsert.c are unconditional no-ops ("Legacy no-op ... kept for callers that haven't been updated yet") while still having live callers in xactundo.c. Same footgun as the legacy no-op block in undolog.h: a header comment advertising behavior that no longer exists. Either finish removing the callers and drop these, or document them plainly as no-ops. Confidence: high.
📄 src/include/access/undo_xlog.h (L279-L283)
Dead type with a misleading comment: xl_undo_chain_state has zero references anywhere in the tree. The comment claims it is "Saved in the two-phase state file during PREPARE TRANSACTION," but the UNDO chain state for prepared transactions is actually persisted via xl_xact_prepare.last_batch_lsn[NUndoPersistenceLevels] (xact.h) and gxact->undo_batch_lsn (twophase.c). This struct describes a mechanism that shipped differently. Remove it. Confidence: high.
📄 src/backend/access/hash/hash_undo.c (L415-L417)
The apply path marks the item LP_DEAD but never sets LH_PAGE_HAS_DEAD_TUPLES on the page opaque. Every other hash-AM site that marks a tuple dead sets this flag together with it (see _hash_kill_items() in hashutil.c: "Whenever we mark anything LP_DEAD, we also set the page's LH_PAGE_HAS_DEAD_TUPLES flag"). The flag is the sole trigger for opportunistic space reclaim: _hash_doinsert() only calls _hash_vacuum_one_page() when H_HAS_DEAD_TUPLES(pageopaque) is true. Without setting it, the space freed by aborted inserts is never reclaimed until a full VACUUM, causing hash-index bloat and undercutting the module's stated "zero-VACUUM" goal. Scan correctness is unaffected (scans test ItemIdIsDead per item), so this is an effectiveness/bloat bug, not corruption. Set the flag inside the critical section, e.g. HashPageGetOpaque(page)->hasho_flag |= LH_PAGE_HAS_DEAD_TUPLES; right after ItemIdMarkDead(). [medium confidence]
💡 Suggested change
Before:
ItemIdMarkDead(lp);
MarkBufferDirty(buffer);
After:
ItemIdMarkDead(lp);
HashPageGetOpaque(page)->hasho_flag |= LH_PAGE_HAS_DEAD_TUPLES;
MarkBufferDirty(buffer);
| COMMIT; | ||
|
|
||
| -- STEP 6: Check UNDO log statistics | ||
| SELECT * FROM pg_stat_get_undo_logs(); |
There was a problem hiding this comment.
These queries will fail at runtime with ERROR: function pg_stat_get_undo_logs() does not exist. Confidence: high.
The C implementations in src/backend/access/undo/undostats.c are declared with PG_FUNCTION_INFO_V1(pg_stat_get_undo_logs) / pg_stat_get_undo_buffers, and the file comment claims they are "registered in pg_proc.dat" — but there is NO corresponding entry for them in src/include/catalog/pg_proc.dat (nor any CREATE FUNCTION SQL). PG_FUNCTION_INFO_V1 only exports the fmgr symbol; it does not make the function SQL-callable. As written, this example is broken.
Add pg_proc.dat entries (OIDs from the reserved developer range) for these functions, then the example will work. Same problem affects examples/05.
| -- View UNDO log statistics | ||
| SELECT * FROM pg_stat_get_undo_logs(); | ||
|
|
||
| -- View UNDO buffer statistics | ||
| SELECT * FROM pg_stat_get_undo_buffers(); |
There was a problem hiding this comment.
All three of these calls will fail at runtime — pg_stat_get_undo_logs(), pg_stat_get_undo_buffers(), and pg_undo_force_discard() have C bodies (PG_FUNCTION_INFO_V1 in undostats.c) but are NOT registered in src/include/catalog/pg_proc.dat, so they are not SQL-callable. This example cannot run as-is. Register them in pg_proc.dat (reserved developer OID range). Confidence: high.
|
|
||
| -- Force discard of UNDO records older than the retention horizon | ||
| -- (normally handled automatically by the UNDO worker) | ||
| SELECT pg_undo_force_discard(); |
There was a problem hiding this comment.
pg_undo_force_discard() is likewise absent from pg_proc.dat; this line will error. See the note above. Confidence: high.
| if (BTreeTupleIsDeleteMarked(itup)) | ||
| { | ||
| /* | ||
| * Delete-marked tombstone (Phase 5): its index key intentionally |
There was a problem hiding this comment.
This adds an empty if branch whose body is only a comment, then chains the real work into else if/else. PostgreSQL style prefers a positive guard: simply exclude delete-marked tuples from the fingerprint block so the existing posting/plain dispatch is left untouched, e.g.
if (state->heapallindexed && P_ISLEAF(topaque) && !ItemIdIsDead(itemid) &&
!BTreeTupleIsDeleteMarked(itup))with the explanatory comment attached to that guard. This keeps the diff minimal, avoids an empty control-flow branch, and reads as if it had always been written that way.
| * Delete-marked tombstone (Phase 5): its index key intentionally | ||
| * no longer matches the CURRENT key of the heap tuple it points | ||
| * at (the live version is reachable under a different key via a | ||
| * separate live entry). It is therefore EXPECTED that this entry |
There was a problem hiding this comment.
Comment style: the all-caps emphasis (CURRENT, EXPECTED) and the internal "Phase 5" phase label are out of step with the surrounding PostgreSQL comment style, which uses plain lowercase prose and does not reference project-internal design-doc phase numbers ("Phase 5" / PHASE5_DELETE_MARKING_DESIGN.md is meaningless to a pgsql-hackers reader). Suggest describing the mechanism directly ("a delete-marked leaf tombstone") without the phase jargon or shouting. Confidence: low (style only, not correctness).
| void | ||
| FluxDirtyMapShmemInit(void) | ||
| { | ||
| /* Initialization is handled by FluxDirtyMapShmemCallbacks */ | ||
| } |
There was a problem hiding this comment.
FluxDirtyMapShmemInit() is an empty stub with no callers -- initialization is done entirely through FluxDirtyMapShmemCallbacks/FluxDirtyMapShmemInit_cb. It is dead scaffolding and is also declared redundantly in two headers (src/include/access/flux.h:867 and src/include/access/flux_dirtymap.h:57). Remove the function and both declarations per the minimalism discipline. Confidence: high (verified no callers exist in the tree).
| #define FLUX_DIRTYMAP_KEY(relid, blkno) \ | ||
| (((uint64) (relid) << 32) | (uint64) (blkno)) |
There was a problem hiding this comment.
The comment claims 0 is safe as EMPTY because "relid 0 is never a user relation," but the actual invariant that must hold is that the composed key is never all-zero, i.e. NOT(relid==0 && blkno==0). A valid relid with blkno 0 composes to a nonzero key and is fine, so the comment overstates the requirement. Since Mark/Check take relid/blkno directly, add a cheap Assert(key != FLUX_DIRTYMAP_EMPTY_KEY) in FluxDirtyMapMark to catch a caller that ever composes the sentinel, rather than relying on an implicit assumption. Confidence: low (defensive; not a demonstrated bug).
| BlockNumber old_nblocks = RelationGetNumberOfBlocks(rel); | ||
|
|
||
| if (new_nblocks < old_nblocks) | ||
| { | ||
| /* Truncated - update FSM */ | ||
| FreeSpaceMapPrepareTruncateRel(rel, new_nblocks); | ||
| } |
There was a problem hiding this comment.
This dead-code path never prunes FSM entries. The sole caller (flux_operations.c:7552) invokes FluxVacuumFSM(onerel, RelationGetNumberOfBlocks(onerel)), passing the post-truncate block count as new_nblocks. Here old_nblocks is re-fetched from the same RelationGetNumberOfBlocks(rel), so old_nblocks == new_nblocks and the new_nblocks < old_nblocks branch is never taken -- FreeSpaceMapPrepareTruncateRel() is never called and stale FSM entries for truncated pages are left behind. The pre-truncation block count must be supplied by the caller (captured before FluxTruncateRelation) rather than re-queried here.
| * Returns the block number of a page with sufficient free space. | ||
| */ | ||
| /* | ||
| * FluxTargetFreeSpace |
There was a problem hiding this comment.
The header comment for FluxGetPageWithFreeSpace ends here, but the function body does not follow -- the entire FluxTargetFreeSpace comment and definition are inserted between this doc block and the function it documents (FluxGetPageWithFreeSpace begins at line 116). Move FluxTargetFreeSpace and its comment ahead of this block (or move this comment down to sit directly above FluxGetPageWithFreeSpace) so each doc block sits on the function it describes.
| * legal INSERT. FLUX hit exactly this: with fillfactor=10 the reservation is | ||
| * 7428 bytes, so a 1000-byte row asked for 8428 and the INSERT failed where | ||
| * heap accepted it (regression tests insert, brin, brin_bloom). |
There was a problem hiding this comment.
Comment drift: this narrative cites "with fillfactor=10 the reservation is 7428 bytes", but the code computes saveFreeSpace from FLUX_DEFAULT_FILLFACTOR, which is 90, not 10. The concrete numbers describe a scenario the code no longer exercises with the default and may mislead readers. Consider phrasing the example without a specific fillfactor value, or align it with the default actually used below.
There was a problem hiding this comment.
🔍 OCR found 216 issue(s).
- 25 inline, 191 in summary (inline capped at 25)
⚠️ 2 warning(s) during review
📄 src/backend/access/flux/flux_fsm.c (L49-L52)
Comment-drift: this block comment describes FluxGetPageWithFreeSpace() ("Returns the block number of a page with sufficient free space."), but it is followed by a second comment block for FluxTargetFreeSpace(), which is the function that actually appears next. The FluxGetPageWithFreeSpace() doc comment is stranded above the wrong function. Move this comment down to immediately precede FluxGetPageWithFreeSpace().
📄 src/backend/access/flux/flux_fsm.c (L30-L31)
Include ordering: "miscadmin.h" is out of alphabetical order within the system-header group (it should sort before storage/, utils/). pgindent/PostgreSQL convention expects sorted includes. Move it up with the other alphabetized headers.
📄 src/backend/access/flux/README (L35-L36)
Terminology drift: FLUX stores old versions in the per-backend UNDO log (the shared undo engine, am_undo_engine = UNDO_ENGINE_PERBACKEND), not in a per-relation "fork". Every other place in this README correctly calls it "the per-backend undo log" / "the UNDO chain" (e.g. lines 22-25, 55). Calling it a "fork" here could send a maintainer looking for a relation fork that does not exist. Suggest "read the pre-update image from UNDO".
💡 Suggested change
Before:
* UNDO-based old-version reconstruction for MVCC. Older snapshots read the
+ pre-update image from the UNDO fork, not from a second on-page copy.
After:
* UNDO-based old-version reconstruction for MVCC. Older snapshots read the
+ pre-update image from the per-backend undo log, not from a second on-page
+ copy.
📄 src/backend/access/flux/flux_dirtymap.c (L202-L209)
Nested spinlock hazard on platforms with PG_HAVE_ATOMIC_U64_SIMULATION (fallback.h, e.g. 32-bit ports without native 64-bit atomics). There, pg_atomic_read_u64/pg_atomic_write_u64 are implemented via pg_atomic_compare_exchange_u64_impl/pg_atomic_fetch_add_u64_impl, each of which acquires the per-slot sema spinlock (see src/backend/port/atomics.c). This code calls those atomic ops while already holding part->mutex, i.e. it acquires a second spinlock while holding the first. PostgreSQL forbids this (spin.h: "spinlocks must not be held for more than a few instructions"), and it can trip the stuck-spinlock PANIC / deadlock detection. Since these slots are only ever mutated under part->mutex, use plain (non-atomic) accesses under the lock in the Mark path, and keep atomic reads only on the lock-free Check path (with a proper barrier).
📄 src/backend/access/flux/flux_dirtymap.c (L200-L209)
The open-addressed insert probe loop runs while holding part->mutex and can iterate up to FLUX_DIRTYMAP_PART_SLOTS (8192) times. Even at the 0.75 load-factor ceiling, primary-clustering can make individual probe chains hundreds of slots long. Holding a spinlock across that many iterations violates the PostgreSQL rule that a spinlock must be held for "only a few instructions" (spin.h) and can trigger the stuck-spinlock PANIC under contention. Consider an LWLock per partition, or bound the work done under the spinlock.
📄 src/backend/access/flux/flux_fsm.c (L153-L160)
FluxGetCommitTimestamp() is called inside the critical section (after START_CRIT_SECTION()), but it can elog(ERROR, "FLUX MVCC not initialized") when FluxMvccShmem == NULL (flux_mvcc.c:359-360). Throwing an ERROR inside a critical section is promoted to a PANIC and crashes the entire cluster. It also calls GetCurrentTimestamp(), which should not run under a crit section. Compute the timestamp before START_CRIT_SECTION().
Suggested fix: hoist the timestamp out of the crit section, e.g.:
uint64 init_commit_ts = RelationNeedsWAL(rel) ? FluxGetCommitTimestamp() : 0;
START_CRIT_SECTION();
MarkBufferDirty(buffer);
if (RelationNeedsWAL(rel)) { ... use init_commit_ts ... }
📄 src/backend/access/flux/flux_fsm.c (L173-L175)
Redundant PageSetLSN: FluxXLogInitPage() already performs PageSetLSN(page, recptr) internally (flux_xlog.c:904) with the same recptr it returns. Setting it again here is a no-op. Drop this line to avoid confusion.
📄 src/backend/access/flux/flux_fsm.c (L193-L193)
FreeSpaceMapVacuumRange() recursively scans the FSM tree from the root on every single-page extension, which happens in the hot insert path. Heap avoids this by bulk-extending many pages at once and vacuuming the FSM range only once per bulk extension (hio.c). Extending one page at a time and vacuuming the FSM root-to-leaf on each extension can be a contention/throughput concern under concurrent inserts. Consider batching extension or relying on the combo RecordAndGetPageWithFreeSpace path instead of a per-page tree vacuum.
📄 src/backend/access/flux/flux_lock.c (L476-L488)
Confidence: high. This LOCKMODE mapping is inconsistent with FluxLockTuple/FluxUnlockTuple (and with heap's canonical tupleLockExtraInfo, which this file's other functions correctly follow: KeyShare->AccessShareLock, Share->RowShareLock, NoKeyExclusive->ExclusiveLock, Exclusive->AccessExclusiveLock). Because locks are actually acquired via FluxLockTuple's mapping, this predicate queries LockHeldByMe() with the wrong lockmode (e.g. a KeyShare lock held as AccessShareLock is checked here as ShareLock) and will report false for locks the transaction genuinely holds. It should reuse the identical 4-way switch. (Note this function currently has no callers -- see the dead-code comment.)
📄 src/backend/access/flux/flux_lock.c (L469-L471)
Confidence: high. FluxHoldsTupleLock has no callers anywhere in the tree (only a declaration in flux.h and a forward decl in flux_operations.c). Per YAGNI/minimalism, drop it. If kept, fix the mode->LOCKMODE mapping noted above.
📄 src/backend/access/flux/flux_lock.c (L387-L390)
Confidence: high. FluxLockMultipleTuples has no callers. It is dead scaffolding (hand-rolled O(n^2) bubble sort, in-place mutation of the caller's array with no CHECK_FOR_INTERRUPTS, palloc0 with ntids==0). Remove it; if a multi-tuple ordered lock is ever needed, prefer qsort over ItemPointerCompare rather than reintroducing a bubble sort.
📄 src/backend/access/flux/flux_lock.c (L324-L326)
Confidence: high. FluxLockPage/FluxUnlockPage have no callers (only decl in flux.h and forward decl in flux_operations.c). They are one-line wrappers over LockAcquire/LockRelease with no added value. Drop as speculative scaffolding.
📄 src/backend/access/flux/flux_lock.c (L448-L450)
Confidence: high. FluxLockRelationForDDL has no callers and is a one-line wrapper over LockRelationOid(). Drop as dead code.
📄 src/backend/access/flux/flux_lock.c (L160-L160)
Confidence: high. Non-ASCII character embedded between "already/" and "will" (a zero-width/soft-hyphen-like code point). PostgreSQL source must be ASCII-only. Replace with plain ASCII, e.g. "has already/will release".
📄 src/backend/access/flux/flux_lock.c (L70-L74)
This comment contradicts the actual release logic and the lock-manager invariant. It claims only one ref is released per row and "the extra refs release naturally at LockReleaseAll, which does not assert once our explicit release has cleared the last one". That is wrong: FluxReleaseSerializeLocks deliberately drains ALL refs in a while (LockHeldByMe(...)) LockRelease(...) loop, and its own comment correctly explains why -- LockReleaseAll only skips the "tuple lock held at commit" WARNING for a LOCKTAG_TUPLE locallock whose entry was already removed (i.e. nLocks reached 0, see lock.c RemoveLocalLock at nLocks==0). Leaving "extra refs" behind (nLocks>0) would keep the locallock entry alive and trip the WARNING. Fix this header comment so it matches the drain-to-zero behavior; as written it will mislead maintainers into "optimizing" the loop into a single LockRelease and reintroduce the bug.
📄 src/backend/access/flux/flux_mvcc.c (L278-L286)
Dead code: FluxMvccShmemInit() (the legacy ShmemInitStruct path) has no caller anywhere in the tree. Shmem is set up exclusively through FluxMvccShmemCallbacks (wired in subsystemlist.h via PG_SHMEM_SUBSYSTEM), whose .init_fn is FluxMvccShmemInit_cb. This function duplicates the entire init body of FluxMvccShmemInit_cb (LWLock init, horizon fields, slot memset, on_shmem_exit(FluxShmemExit, 0)) with no consumer. Per YAGNI/minimalism, remove FluxMvccShmemInit() (and its extern in flux.h) so the only init path is the subsystem callback. Keeping two divergent init routines is a DRY hazard.
📄 src/backend/access/flux/flux_mvcc.c (L759-L767)
serializable_horizon is dead/broken state. Its only writer is this Min() update; a codebase-wide search finds no reader outside this function. Worse, the logic is monotonically wrong: it is initialized to 1 and only ever decreased via Min(serializable_horizon, xact_commit_ts), so a commit timestamp (a large wall-clock microsecond value) can never lower it below 1 -- the field is pinned at 1 forever and would be a useless horizon even if something read it. Since visibility is now CLOG-based and nothing consumes this value, remove the field, this update block, and the mvcc_lock that exists only to protect it.
📄 src/backend/access/flux/flux_mvcc.c (L787-L790)
Dead code: FluxGetSnapshotTimestamp() has no caller outside this file (searched the whole tree, including its extern in flux.h). The file's own header documents that visibility is now CLOG-based, so this function's returned timestamp drives no decision. Remove it (and its prototype) per YAGNI.
📄 src/backend/access/flux/flux_mvcc.c (L775-L782)
Dead code: FluxAbortTransaction() has no caller anywhere in the tree. The abort path is handled entirely by FluxXactCallback (XACT_EVENT_ABORT -> FluxForgetSerializeLocks + FluxCleanupTransactionState), which never invokes this. Remove it and its prototype.
📄 src/backend/access/flux/flux_mvcc.c (L873-L875)
Stale-cache hazard in the fast path. Both my_oldest_active_gen and oldest_active_generation start at 0, and my_oldest_active_cached persists across transactions in this static. After a uint32 generation wraparound (or the 0/0 startup coincidence), current_gen == my_oldest_active_gen can be true while the shared slots have changed, returning a stale horizon. This is tolerable today only because this value is cosmetic (it feeds the DEFRAG WAL commit-ts word and stats, not the reclamation gate, which is the XID horizon). If a future consumer ever treats this as a reclamation/visibility boundary, the stale window becomes a correctness bug. Consider documenting this invariant at the function header, or gate on a monotonically-increasing 64-bit generation to remove the wrap window.
📄 src/backend/access/flux/flux_mvcc.c (L579-L583)
oldest_active_ts is a plain uint64 written lock-free here and from FluxResolvePreparedSlot() and the slow-path scan, and read lock-free elsewhere. On 32-bit platforms a 64-bit store is not guaranteed atomic, so a concurrent reader can observe a torn value. This is currently benign because the field is cosmetic (WAL commit-ts word / stats), not a reclamation gate, but the write barrier here only orders the count decrement, not this store. If correctness ever depends on it, use pg_atomic_uint64.
📄 src/backend/access/flux/flux_handler.c (L2654-L2657)
Racy hint-bit write under a SHARE lock, and dead code. In flux_tuple_satisfies_snapshot the buffer is always valid (it is ReadBuffer'd and held with BUFFER_LOCK_SHARE at the top of the function), so both else branches here are unreachable. Worse, if ever reached they mutate tuple_hdr->t_flags directly on a shared buffer page while holding only a SHARE lock and without going through the hint-bit protocol (BufferSetHintBits16 / MarkBufferDirtyHint) -- a torn/racy write to a shared page and a footgun. Drop the dead else and always use BufferSetHintBits16.
💡 Suggested change
Before:
else
tuple_hdr->t_flags &=
~FLUX_TUPLE_UNCOMMITTED;
}
After:
else
BufferSetHintBits16(&tuple_hdr->t_flags,
tuple_hdr->t_flags & ~FLUX_TUPLE_UNCOMMITTED,
buffer);
}
📄 src/backend/access/flux/flux_handler.c (L2654-L2657)
Second occurrence of the same dead/racy hint-write path (buffer is always valid in this function). Same fix as above: remove the else that mutates tuple_hdr->t_flags under a SHARE lock without the hint-bit protocol.
💡 Suggested change
Before:
else
tuple_hdr->t_flags &=
~FLUX_TUPLE_UNCOMMITTED;
}
After:
else
BufferSetHintBits16(&tuple_hdr->t_flags,
tuple_hdr->t_flags & ~FLUX_TUPLE_UNCOMMITTED,
buffer);
}
📄 src/backend/access/flux/flux_handler.c (L994-L1000)
SSI coverage gap for TID range scans. This visibility check has no SSI bookkeeping, unlike flux_scan_getnextslot/flux_index_fetch_tuple, which call FluxCheckForSerializableConflictOut on invisible tuples and PredicateLockTID on visible ones. TID range scans use SO_TYPE_TIDRANGESCAN, so flux_scan_begin's relation-level PredicateLockRelation (gated on SO_TYPE_SEQSCAN | SO_TYPE_SAMPLESCAN) does not cover them either. Heap's tidrange path performs per-tuple HeapCheckForSerializableConflictOut/PredicateLockTID via heapgettup[_pagemode]. As written, a SERIALIZABLE WHERE ctid >= ... AND ctid < ... scan silently misses write-skew conflicts that heap catches. Add FluxCheckForSerializableConflictOut for the invisible case and PredicateLockTID for the visible case here, guarded by IsolationIsSerializable().
📄 src/backend/access/flux/flux_handler.c (L493-L495)
Session-lifetime memory leak on the error path. rs_vistuples is allocated in TopMemoryContext (here and again in the bitmap path) but freed only in flux_scan_end. If the scan is abandoned by an ERROR before flux_scan_end runs, the enclosing executor/query context reset reclaims the scan descriptor (palloc'd in the current context) but this TopMemoryContext block leaks for the whole session. Heap keeps rs_vistuples as an inline array in the scan descriptor precisely to tie it to scan lifetime. Allocate this in a scan-lifetime context (e.g. the context the scan descriptor lives in), not TopMemoryContext.
📄 src/backend/access/flux/flux_handler.c (L1208-L1210)
Same TopMemoryContext allocation in the bitmap path -- leaks for the session lifetime if the scan errors out before flux_scan_end. Use a scan-lifetime context here as well.
📄 src/backend/access/flux/flux_handler.c (L4266-L4270)
Missing ItemIdIsNormal(nitemid) guard before PageGetItem. Every other item access in this file checks ItemIdIsNormal first; here PageGetItem(npage, nitemid) is called (inside a critical section) with no check. PageGetItem on a non-normal item id is undefined behavior. Although the tuple was just inserted and should be normal, add the guard for defensiveness and consistency, and do so before START_CRIT_SECTION() so a bail-out is not inside the critical section.
📄 src/backend/access/flux/flux_handler.c (L1472-L1474)
Per-tuple executor-state churn on the index-scan hot path. flux_index_entry_key_mismatch is invoked for every tuple returned by an index scan on a delete-marking FLUX table (the xs_itup != NULL && RelationSupportsDeleteMarking guard is true for every such scan), and it calls BuildIndexInfo + CreateExecutorState + FreeExecutorState on each call. That is a significant per-tuple cost (catalog access, executor state alloc/free) versus heap's index scan. Cache the IndexInfo/EState in the scan-level IndexScanFluxData (built once in flux_index_scan_begin, torn down in flux_index_scan_end) and reset only the per-tuple ExprContext between tuples.
📄 src/backend/access/flux/flux_handler.c (L397-L399)
On rescan the scan-key/nkeys handling is inconsistent with the table-AM contract. Heap's initscan keeps rs_nkeys unchanged and memcpys the new key contents into the existing rs_key buffer -- the key argument is new key content, not a new count. Here rs_nkeys = key ? scan->rs_base.rs_nkeys : 0 keeps the stale count when a new key is provided and zeroes it only when key == NULL, then overwrites rs_key with the caller's pointer. If a caller ever rescans with a differently-sized key array, rs_nkeys no longer matches rs_key. FLUX does not currently consume scan keys, so this is latent, but the logic is misleading and diverges from the documented contract; mirror heap (leave rs_nkeys alone, copy key contents).
📄 src/backend/access/flux/flux_handler.c (L4827-L4827)
Stray doubled percent sign in a comment: ~9%%. This is not a format string, so the %% is unintended -- use ~9%.
💡 Suggested change
Before:
* which showed up as ~9%% of CPU under high-concurrency pgbench. A stale
After:
* which showed up as ~9% of CPU under high-concurrency pgbench. A stale
📄 src/backend/access/flux/flux_handler.c (L5183-L5192)
Dead/aspirational scaffolding. This function is a no-op ((void) rscan; (void) slot;) whose header comment describes compression-dictionary training that FLUX explicitly does not do (the body even says so). Its sole caller is guarded by SO_TYPE_ANALYZE but does nothing. Per YAGNI/minimalism, remove the function, its single call site, and the misleading comment rather than leaving a stub.
📄 src/backend/access/flux/flux_mvcc.c (L717-L728)
SSI soundness hole: this drops rw-conflict-out edges for concurrent writers that have already COMMITTED. flux_header_dirty_xid() returns InvalidTransactionId unless the writer is still TransactionIdIsInProgress() (see its contract in flux_operations.c: it requires the xid be in progress). So a writer that committed after our snapshot but is still within the SSI conflict window (xid >= TransactionXmin) yields Invalid here and we return early, never calling CheckForSerializableConflictOut(). Heap's HeapCheckForSerializableConflictOut takes the writer xid straight from the header (GetXmin/GetUpdateXid) regardless of commit state and only bails when TransactionIdPrecedes(xid, TransactionXmin). The call sites in flux_handler.c (lines 578, 600, 628) explicitly cite heap's visible-RECENTLY_DEAD / DELETE_IN_PROGRESS and invisible-concurrent-insert cases as the anomaly this is meant to fix -- exactly the committed-writer cases that are silently missed here, so write-skew/phantom anomalies heap rejects can slip through under SERIALIZABLE. Derive the xid from the header (t_xmin for insert, t_xmax/t_xmin for delete/update) and gate only on TransactionXmin, as heap does; the comment claiming Invalid means "committed so long ago it's no longer tracked" is incorrect. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L182-L185)
Dead write-only state (YAGNI). rc_read_point_cid, rc_read_point_xcc and is_read_only are only ever initialized in FluxInitTransactionState() and never read anywhere in the tree (is_read_only is only ever set to true, never to false). The RC per-command read-point comment describes a mechanism that no longer exists now that visibility is CLOG-based. Remove these fields and their initializations. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L212-L213)
epq_reconcile_verptr is dead: it is written in FluxEpqReconcileMark() but never read. FluxEpqReconcileMatches() deliberately ignores head_verptr/head_xid and matches on (relid, tid, cid) only (see its own comment). The stored verptr therefore drives no decision. Drop the field and the assignment. (high confidence)
📄 src/backend/access/flux/flux_operations.c (L389-L392)
Contract mismatch: FluxPagePruneOpt is declared extern int and this header comment states "Returns the number of tuples pruned", but the success path returns true (i.e. 1) regardless of ndead, while the no-op paths return 0. The one caller that uses the value (flux_tuple_insert at line 835) treats it as a boolean, but any future caller relying on the documented count semantics would silently get 1. Either change the return type to bool (and update the prototype in flux.h line 742 and the doc comment) or return the actual ndead count.
[high confidence]
📄 src/backend/access/flux/flux_operations.c (L581-L584)
Success path of an int-returning "count" function returns a boolean literal. See the contract-mismatch note on the function header: this should return the pruned-tuple count (ndead) to match the documented int return, or the function should be declared bool.
[high confidence]
📄 src/backend/access/flux/flux_operations.c (L749-L755)
Double-release hazard on the insert overflow error path. This cleanup loop unconditionally calls UnlockReleaseBuffer() for every overflow buffer, but FluxFormTuple's "spatial locality optimization" can place two overflow records on the SAME page, producing two entries with the identical Buffer. The success-path release loop (and flux_release_update_overflow_buffers) explicitly dedup against already-released buffers; these error loops do not, so an aliased buffer is unlock/unpinned twice, tripping the buffer content-lock/refcount assertions. Apply the same dedup guard (skip a buffer already released by a prior entry) here and at the retry-loop error path (~line 887).
[high confidence]
📄 src/backend/access/flux/flux_operations.c (L885-L890)
Same double-release hazard as the initial InvalidBlockNumber cleanup loop: this retry-path cleanup releases every overflow buffer without the dedup guard, so two overflow records that landed on one page (aliased Buffer) are double-unlocked/unpinned. Add the already-released check used on the success path.
[high confidence]
📄 src/backend/access/flux/flux_operations.c (L8006-L8013)
Buffer pin leak in the swallow-and-continue CATCH. ReadBuffer() pins buf before LockBuffer(); if LockBuffer throws, buf holds a live pin. The CATCH sets buf = InvalidBuffer and continues WITHOUT releasing the pin. Because this runs at PRE_COMMIT and the transaction proceeds to commit (the error is intentionally swallowed), the ResourceOwner will not reclaim the pin until much later. Release the buffer in the CATCH if it is valid before nulling it, e.g. if (BufferIsValid(buf)) ReleaseBuffer(buf);.
[moderate confidence]
📄 src/backend/access/flux/flux_operations.c (L1502-L1502)
Non-ASCII character in source. The PostgreSQL tree requires ASCII-only source files; this comment contains a U+2192 RIGHT ARROW glyph. Replace with an ASCII arrow ("->"). Several other comments in this file have the same problem (lines ~4236-4237, ~4403-4407, ~7642, ~8255).
[high confidence]
💡 Suggested change
Before:
/* Own LOCK entry → proceed */
After:
/* Own LOCK entry -> proceed */
📄 src/backend/access/flux/flux_operations.c (L4403-L4407)
Non-ASCII arrow glyphs (U+2192) in these comments; replace with ASCII "->" to satisfy the ASCII-only source requirement.
[high confidence]
📄 src/backend/access/flux/flux_operations.c (L6262-L6267)
In-function typedef and stale comment. PostgreSQL convention places typedefs at file scope (also so pgindent's typedef list can be maintained); defining FluxDefragMove inside the function body is atypical and will not be picked up by the typedef list. Additionally, the adjacent comment ("the UPDATE path (which ereport()s rather than move a tuple that no longer fits)") is now inaccurate: this same patch changed the no-room UPDATE path to fall back to flux_update_out_of_place (the "BUG 2 fallback"), not to ereport. Hoist the typedef to file scope and fix the stale claim.
[moderate confidence]
📄 src/backend/access/flux/flux_operations.c (L1001-L1001)
Missing apostrophes: "FLUXs" and "heaps" should be "FLUX's" and "heap's". Minor, but this comment block is otherwise carefully written.
[low confidence]
💡 Suggested change
Before:
* FLUXs ceiling is genuinely lower than heaps: a FLUX tuple header
After:
* FLUX's ceiling is genuinely lower than heap's: a FLUX tuple header
📄 src/backend/access/flux/flux_operations.c (L755-L755)
These are user-reachable out-of-space conditions (FluxGetPageWithFreeSpace failing to allocate/extend), reported via elog(ERROR, ...) which produces ERRCODE_INTERNAL_ERROR (XX000), an uppercase-prefixed, untranslated message that reads as a FLUX internal bug. If genuinely reachable on disk exhaustion, prefer ereport() with a proper SQLSTATE (as the neighboring "row is too big" case does with ERRCODE_PROGRAM_LIMIT_EXCEEDED). If truly can't-happen, an Assert plus a comment is clearer than a user-facing elog.
[low confidence]
📄 src/backend/access/flux/flux_handler.c (L1253-L1268)
SSI coverage gap in the bitmap heap scan path. Heap's heapam_scan_bitmap_next_block acquires a per-tuple SIREAD lock via PredicateLockTID() on every visible tuple (lossy path) or through heap_hot_search_buffer() (exact path), and CheckForSerializableConflictOut() on invisible ones. This FLUX bitmap page-scan collects visible offsets into rs_vistuples[] with no PredicateLockTID/FluxCheckForSerializableConflictOut at all, in either the exact or lossy branch. Under SERIALIZABLE, a bitmap-index-scan-based read therefore leaves no rw-dependency trace, so write-skew anomalies that heap rejects ("could not serialize access ...") are silently accepted. This is the same class of gap already noted for flux_scan_getnextslot_tidrange, but a distinct code path. Add SSI bookkeeping here (visible -> PredicateLockTID, invisible -> FluxCheckForSerializableConflictOut) guarded by IsolationIsSerializable(), matching the seqscan/index-fetch paths in this file.
📄 src/backend/access/flux/flux_handler.c (L1106-L1107)
*recheck becomes sticky within a page. When advancing to a new block the callback sets *recheck = tbmres->recheck, but for the 2nd+ tuple returned from the same page (this fast-path) *recheck is only ever forced to true for FLUX_TUPLE_UPDATED tuples and is never reset back to the page's tbmres->recheck value. So once one in-place-updated tuple appears on a page, every later non-updated tuple on that same page also gets *recheck == true and is needlessly re-qualified by BitmapHeapNext(). Results stay correct (over-rechecking is safe), but this is a spurious per-tuple recheck cost that heap does not incur (heap sets recheck only per block). Consider stashing the block's recheck value on the scan descriptor and OR-ing the per-tuple UPDATED bit into a fresh copy for each returned tuple.
📄 src/backend/access/flux/flux_handler.c (L3571-L3571)
Page modification outside the critical section, before WAL logging. tuple_hdr->t_flags |= FLUX_TUPLE_LOCKED mutates the shared buffer page here, but START_CRIT_SECTION() / MarkBufferDirty(buf) / XLogInsert do not run until ~50 lines below (after SLogTupleInsert). SLogTupleInsert can palloc, take LWLocks, and ereport(ERROR) (e.g. OOM). If it errors, the page carries a FLUX_TUPLE_LOCKED bit that was never MarkBufferDirty'd nor WAL-logged; should that buffer later be written out (or is left as an unlogged change), crash/standby replay diverges. PostgreSQL's rule is that any buffer content change that must be WAL-logged has to occur inside the critical section, atomically with the XLogInsert (as heap_lock_tuple does when it stamps xmax). Move the flag set to after START_CRIT_SECTION(), or set it only after all fallible work is done.
📄 src/backend/access/flux/flux_pbu.c (L177-L180)
Confirmed footgun: FluxPbuCancelUndo() has no callers anywhere in the tree (verified by search), and there is no transaction-abort hook that resets this module's static state. Once PrepareUndoInsert() returns here, flux_pbu_prepared is set true and the per-backend engine holds pinned/locked undo buffers in its own static undo_buffer[] array. If any code between FluxPbuPrepareInsert() and FluxPbuFinishUndo() throws, the transaction aborts: ResourceOwner physically unpins the buffers, but neither FluxPbuFinishUndo/CancelUndo nor the engine's ResetUndoBuffers() runs (ResetUndoBuffers is only reachable via UnlockReleaseUndoBuffers). So flux_pbu_prepared stays true, uur_payload is leaked, and buffer_idx/undo_buffer[] retain stale (now-freed) buffer handles. The NEXT FLUX DML in this backend hits Assert(!flux_pbu_prepared) (a no-op in production), then MemSet clobbers the leaked StringInfo pointer, and PrepareUndoInsert -> UndoGetBufferSlot may match a stale entry and Assert(BufferIsLockedByMeInMode(...)) on a released buffer -- undefined behavior / potential undo corruption.
Concrete reachable path: in the in-place UPDATE (flux_operations.c around lines 5548-5590), FluxPbuPrepareInsert() is inside a PG_TRY whose PG_CATCH only calls flux_release_update_overflow_buffers() and PG_RE_THROW() -- it does NOT call FluxPbuCancelUndo(); FluxXLogPrepareLogicalImage() runs after prepare and can ereport on OOM. The DELETE path also has a small window (GetCurrentTransactionId() at flux_operations.c:1873, which can palloc) between prepare and START_CRIT_SECTION with no cleanup.
Either register a subxact/xact abort callback that resets this state (calling FluxPbuCancelUndo semantics), or wrap every prepare in PG_TRY that calls FluxPbuCancelUndo() on error. Note Assert() does not protect production builds.
📄 src/backend/access/flux/flux_pbu.c (L164-L166)
Payload leak on error before finish/cancel. initStringInfo() allocates the payload buffer in the CURRENT memory context (whatever the caller runs in -- not necessarily a short-lived one), and it is only pfree'd in FluxPbuFinishUndo()/FluxPbuCancelUndo(). If PrepareUndoInsert() below throws (UndoRecordAllocate can extend the log and ereport; UndoGetBufferSlot can ereport), flux_pbu_prepared is still false so no cancel path applies, and this StringInfo is leaked until the enclosing context is reset. Since these three statics (uur/payload) persist across ops, a throw here leaves flux_pbu_uur.uur_payload.data dangling for the next FluxPbuPrepareInsert(), which MemSet's the struct to zero without freeing it. Wrap prepare in PG_TRY and free the payload (or allocate it in a dedicated context that is reliably reset) on the error path.
📄 src/backend/access/flux/flux_pvs.c (L96-L96)
Comment drift (comment-accuracy discipline). The (void) tid; cast and the "reserved for future diagnostics / currently unused" claim in the function header are false: tid is actively dereferenced in the live depth-cap WARNING below (ItemPointerGetBlockNumber(tid) / ItemPointerGetOffsetNumber(tid)). All callers verified to pass a valid non-NULL tid, so there is no crash, but the (void) tid; and the header wording must be dropped since the parameter is used now. Confidence: high.
📄 src/backend/access/flux/flux_pvs.c (L336-L338)
Narrowing cast: out_len is declared int (see flux.h prototype) while candidate_len is Size. For a corrupt/large reconstructed image this truncates silently. FLUX images should be bounded well under INT_MAX, but consider either changing the out parameter to Size for consistency with onpage_len, or asserting candidate_len <= PG_INT32_MAX before the cast. Confidence: low.
📄 src/backend/access/flux/flux_pbu.c (L239-L239)
payload_len is truncated to uint16 for the WAL header (line 239), but the full int-width uur_payload.len is registered as block data (line 254). If a before-image payload ever exceeds 65535 bytes, the two disagree: FluxPbuRedoUndo() reads the full datalen and compares it against SizeOfFluxUndo + (truncated)payload_len, then elog(PANIC) -- turning an oversized DML into an unrecoverable crash-recovery failure on the standby/after crash, not a clean primary-side error. In practice FLUX main-fork tuples are page-bounded and the per-backend engine itself caps the record at uint16 (InsertPreparedUndo's undo_len is uint16), so this is not currently reachable; but the silent narrowing cast hides that invariant. Add an explicit bound check (Assert or ereport) that uur_payload.len <= UINT16_MAX before the cast so a future large-payload path fails loudly on the primary instead of PANICking during redo.
📄 src/backend/access/flux/flux_pvs.c (L103-L103)
Out-of-bounds read on the corruption path this loop is explicitly hardening against. At the top of every iteration hdr->t_verptr (offset 8, 8 bytes -> needs >=16 bytes) and later FluxTupleGetXmin(hdr) are dereferenced from candidate, but candidate_len is never checked against sizeof(FluxTupleHeader). A corrupt undo record can produce a reconstructed image shorter than the header: the full-tuple case only rejects phdr.tuple_len == 0, and the delta case accepts any delta->old_len (even 0, which palloc(0)s and reconstructs a zero-length image via FluxUndoDeltaReconstruct). The next iteration then reads past the allocation. Add a candidate_len < sizeof(FluxTupleHeader) guard before reading the header (and reject phdr.tuple_len/delta->old_len shorter than the header) so a malformed record is treated as end-of-chain / best-effort instead of over-reading.
💡 Suggested change
Before:
const FluxTupleHeader *hdr = (const FluxTupleHeader *) candidate;
After:
const FluxTupleHeader *hdr = (const FluxTupleHeader *) candidate;
📄 src/backend/access/flux/flux_operations.c (L5582-L5591)
Prepared-but-not-cancelled undo reservation leaks on error. FluxPbuPrepareInsert() sets the backend-global flux_pbu_prepared = true (flux_pbu.c:177). The FluxXLogPrepareLogicalImage() calls below run inside this same PG_TRY and can ereport(ERROR) -- they palloc/heap_form_tuple for logically-logged relations (this slow path IS taken for such relations). The PG_CATCH only releases overflow buffers and re-throws; it never calls FluxPbuCancelUndo(). Since no transaction-abort/xact-end hook resets flux_pbu_prepared (it is cleared only in FluxPbuFinishUndo/FluxPbuCancelUndo, and the latter has no other callers), the flag stays stuck true across the abort. The next FLUX UPDATE/DELETE then trips Assert(!flux_pbu_prepared) in FluxPbuPrepareInsert (assert builds) or proceeds over inconsistent undo state (production). Fix: call FluxPbuCancelUndo() in the PG_CATCH before PG_RE_THROW(), or reserve the undo record after the logical-image prep.
💡 Suggested change
Before:
FluxXLogPrepareLogicalImage(relation, old_tuple_for_inplace_wal,
&update_old_img);
FluxXLogPrepareLogicalImage(relation, new_tuple, &update_new_img);
}
PG_CATCH();
{
flux_release_update_overflow_buffers(&update_overflow_buffers, buffer);
PG_RE_THROW();
}
PG_END_TRY();
After:
FluxXLogPrepareLogicalImage(relation, old_tuple_for_inplace_wal,
&update_old_img);
FluxXLogPrepareLogicalImage(relation, new_tuple, &update_new_img);
}
PG_CATCH();
{
if (FluxPbuUndoPrepared())
FluxPbuCancelUndo();
flux_release_update_overflow_buffers(&update_overflow_buffers, buffer);
PG_RE_THROW();
}
PG_END_TRY();
📄 src/backend/access/flux/flux_operations.c (L6091-L6096)
Potential infinite loop when the page fills between FSM selection and insertion. FluxGetPageWithFreeSpace returned target_block (so we did not take the single-insert fallback), but the page is unlocked before the crit-section relock, so a concurrent inserter can fill it. If the very first FluxPageAddTuple then returns InvalidOffsetNumber, batch_count stays 0, ndone is never advanced, and the outer while (ndone < ntuples) loop spins forever on the same tuple/block. The single-insert path (flux_tuple_insert) recovers from a stale-FSM page by re-recording free space, pruning, and extending the relation; flux_multi_insert has no such guard. Add a batch_count == 0 fallback that records the actual free space and routes the tuple through flux_tuple_insert (or extends), guaranteeing forward progress.
📄 src/backend/access/flux/flux_operations.c (L1873-L1873)
Same prepared-undo leak as the UPDATE-inplace path. FluxPbuPrepareInsert (above) has set flux_pbu_prepared = true. GetCurrentTransactionId() here allocates a fresh XID on first use (XactLockTableInsert -> LockAcquire -> palloc, as the comment notes), which can ereport(ERROR) (lock-table exhaustion / OOM). If it throws, the transaction aborts but no abort/xact-end hook resets flux_pbu_prepared, so the flag stays stuck true and the next FLUX UPDATE/DELETE trips Assert(!flux_pbu_prepared). Either assign the XID before FluxPbuPrepareInsert, or wrap prepare..crit-section in a PG_TRY that calls FluxPbuCancelUndo() on error.
📄 src/include/access/flux_xlog.h (L16-L18)
Installed access-method headers must not include postgres.h; the .c file includes it first. No other AM header (heapam.h, nbtree.h, hash.h, tableam.h) does this. This is worse than a convention nit here: flux_xlog.h is included by the FRONTEND build src/bin/pg_waldump/rmgrdesc.c (which does #define FRONTEND 1 then includes postgres.h itself). Unconditionally re-including postgres.h from a header reached by the frontend is fragile and violates the include discipline. Drop this line and let the translation unit include postgres.h/postgres_fe.h first.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/xlogreader.h"
After:
#include "access/xlogreader.h"
📄 src/include/access/flux.h (L865-L868)
These three symbols are declared identically in flux_dirtymap.h (lines 56-58), and both headers are included in the same translation units (flux_handler.c, flux_operations.c). Duplicate extern declarations of the same symbols across two installed headers are redundant and a maintenance hazard: if a signature ever changes in one header, each TU silently picks up whichever declaration it saw last. Pick one owner (flux_dirtymap.h is the natural home) and drop these from flux.h.
💡 Suggested change
Before:
/* Dirty block map (lock-free sLog bypass) */
extern Size FluxDirtyMapShmemSize(void);
extern void FluxDirtyMapShmemInit(void);
extern const ShmemCallbacks FluxDirtyMapShmemCallbacks;
After:
/* Dirty block map (lock-free sLog bypass): see access/flux_dirtymap.h */
📄 src/include/access/flux.h (L270-L272)
This comment is stale and contradicts the t_verptr field documentation above (lines ~168-186), which states the trailing-UndoRecPtr scheme was removed and t_verptr is now a fixed, MAXALIGN'd header field ("no trailing-byte arithmetic"). The flag is still read by live code (flux_operations.c:5446) and FluxTupleGetVersionPtr/SetVersionPtr now ignore item_len and use hdr->t_verptr, so the flag no longer marks a trailer that "follows column data". Update the comment to describe its current meaning ("tuple has a version-chain head in t_verptr") so it does not mislead into re-introducing trailer arithmetic.
💡 Suggested change
Before:
#define FLUX_TUPLE_HAS_VERSION_PTR 0x0100 /* trailing UndoRecPtr
* (version-chain head) follows
* column data */
After:
#define FLUX_TUPLE_HAS_VERSION_PTR 0x0100 /* tuple has a version-chain
* head in the fixed t_verptr
* header field */
📄 src/include/access/flux_xlog.h (L402-L407)
This entire #else /* FRONTEND */ block of frontend-safe struct copies appears to be dead. The only frontend consumer of FLUX WAL desc is src/backend/access/rmgrdesc/fluxdesc.c, which is compiled with FRONTEND and defines its OWN local xl_flux_*_fe struct copies (see fluxdesc.c:54-90) rather than using these. pg_waldump/rmgrdesc.c includes this header only for the flux_desc/flux_identify prototypes. Unused speculative surface should be removed; if a frontend TU is intended to use these, wire it up, otherwise delete the block to keep the change minimal.
📄 src/include/access/flux.h (L226-L228)
Casting a plain on-disk uint32 to pg_atomic_uint32 * and operating on it bypasses pg_atomic_init_u32 and relies on the internal layout of pg_atomic_uint32. On all currently supported builds pg_atomic_uint32 is { volatile uint32 value; } with identical size/alignment and there is no lock/sema fallback for u32 (verified across arch-x86/arch-ppc/generic-gcc/generic-msvc), so this works today. But it is a hard portability contract violation if any future platform introduces a non-bare-uint32 u32 atomic, and it silently depends on t_writer being 4-byte aligned in the on-page tuple image. At minimum this should be guarded with a StaticAssert on sizeof/offsetof and a comment pinning the assumption; better, store t_writer as a pg_atomic_uint32 field so the abstraction is respected.
📄 src/backend/access/flux/flux_stats.c (L50-L51)
This entire file is dead code. FluxCollectRelationStats and FluxLogRelationStats have no callers anywhere in the tree (only the definitions here and the extern declarations in flux.h reference them); they are never wired into ANALYZE. Per the minimalism (YAGNI) discipline, a full extra sequential scan of every page plus the FluxRelationStats struct and both entry points should not be committed until something actually invokes them. Additionally, the accounting reads FLUX_TUPLE_COMPRESSED / FluxCompressionHeader and FluxIsOverflowRecordInline, but flux_tuple.c states FLUX "does not compress attributes" and "uses TOAST, not on-page overflow" -- so the compression_ratio / overflow columns measure features the tuple path never produces. Recommend removing this file (and the two extern decls) or wiring it into the ANALYZE hook with tests.
📄 src/backend/access/flux/flux_tuple.c (L516-L520)
Dead scaffolding: is_compressed[]/is_overflowed[] are memset/palloc0'd to false and never set true anywhere in flux_form_tuple_internal (FluxCompressAttribute/FluxStoreOverflowColumn are not called on this path), and has_compressed/has_overflow are likewise always false. Consequently the FLUX_TUPLE_COMPRESSED/FLUX_INFOMASK_COMPRESSED and FLUX_TUPLE_HAS_OVERFLOW infomask branches, and this free loop, are dead. The comment claiming these datums were "allocated by FluxCompressAttribute and FluxStoreOverflowColumn" is misleading. It is currently harmless (the free loop never fires, so caller Datums are never freed), but it is a latent footgun: if these arrays are ever wrongly set, this loop would pfree caller-owned Datums. Also the force_shrink, old_ovptrs, and old_ovpresent parameters are entirely unused. Per YAGNI, strip the compression/overflow bookkeeping and the unused parameters/wrappers.
📄 src/backend/access/flux/flux_slot.c (L859-L864)
Same-buffer fast path leaks values_block. When TTS_SHOULDFREE is set it pfrees rslot->tuple but not rslot->values_block, then clears SHOULDFREE. A materialized-values slot has tuple==NULL and values_block!=NULL, so this branch would free nothing and leak the block. It is currently unreachable only because materialize/copyslot always release the buffer (rslot->buffer=InvalidBuffer) when they set values_block, and no caller passes InvalidBuffer -- a fragile invariant. Mirror tts_flux_clear and free values_block here too.
💡 Suggested change
Before:
if (unlikely(TTS_SHOULDFREE(slot)))
{
if (rslot->tuple)
pfree(rslot->tuple);
slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
}
After:
if (unlikely(TTS_SHOULDFREE(slot)))
{
if (rslot->tuple)
pfree(rslot->tuple);
if (rslot->values_block)
{
pfree(rslot->values_block);
rslot->values_block = NULL;
}
slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
}
📄 src/backend/access/flux/flux_slot.c (L473-L473)
cmin/cmax retrieval reads slot->tts_tid and slot->tts_tableOid without validating them, unlike tts_flux_is_current_xact_tuple which guards ItemPointerIsValid(&slot->tts_tid) and errors on a missing storage tuple. For a virtual/materialized slot whose TID was never populated, this silently does a hash lookup on a bogus (relid, tid) key. It happens to be benign today (FluxLocalCidGet/SLogTupleLookupFiltered return InvalidCommandId on a non-matching key), but relying on that is a POLA violation; consider guarding on ItemPointerIsValid to make the contract explicit.
📄 src/backend/access/flux/flux_vm.c (L72-L74)
FluxVMInit is an empty function with only a comment ("This happens automatically when we first try to extend it"). Per the minimalism discipline this no-op adds no value; either remove it and its declaration/callers, or make it actually create the fork. Also note the copyright year here is 1996-2025 while sibling new files (flux_slot.c, flux_stats.c, flux_undo.c) use 1996-2026 -- pick one consistent year across the new FLUX files.
📄 src/backend/access/flux/flux_vm.c (L324-L328)
FluxVMCheckCached calls smgrexists() on the VM fork on every per-page invocation on the sequential-scan hot path. RelationGetNumberOfBlocksInFork is cheap (cached smgr nblocks), but smgrexists can incur an mdexists/open probe and partly defeats the point of caching the buffer across pages. Consider caching the fork-exists result (or relying on the already-cached nblocks==InvalidBlockNumber sentinel) so the hot path avoids a per-page smgr probe.
📄 src/backend/access/flux/flux_tuple.c (L656-L663)
The dropped-column/varlena offset-walking algorithm (dropped-col skip via att_addlength_pointer, varlena start alignment, missing-attr defaults) is duplicated near-identically across three functions: FluxDeformTupleUpTo here, FluxTupleToSlotWithOverflow here, and tts_flux_deform in flux_slot.c. Note FluxTupleToSlotWithOverflow re-aligns the varlena pointer after advancing (data_ptr = att_align_nominal(data_ptr + attr_len, ...)) while tts_flux_deform/FluxDeformTupleUpTo advance with a bare data_ptr += attr_len; these happen to be equivalent because the next iteration re-aligns, but the divergent styles make it easy for a future fix to land in only one copy. Consider consolidating the three deform paths into a single helper to keep this subtle offset math in one place.
📄 src/include/access/flux_undo.h (L186-L189)
prefixlen/suffixlen are cast to uint16 when stored in FluxUndoDelta, but old_len is uint32. This silently truncates if old_len >= 65536. Today it is safe only because every call site passes old_len <= FLUX_MAX_TUPLE_SIZE, which is MAXALIGN_DOWN(BLCKSZ - ...) and thus < BLCKSZ (max 32768) < 65536. That invariant is unstated and load-bearing: a future larger-image caller would corrupt the before-image with no diagnostic. Add an Assert(old_len <= UINT16_MAX) (or a StaticAssert that FLUX_MAX_TUPLE_SIZE fits uint16) here so the invariant is enforced rather than assumed.
💡 Suggested change
Before:
d = (FluxUndoDelta *) out;
d->old_len = old_len;
d->prefixlen = (uint16) prefixlen;
d->suffixlen = (uint16) suffixlen;
After:
d = (FluxUndoDelta *) out;
Assert(prefixlen <= UINT16_MAX && suffixlen <= UINT16_MAX);
d->old_len = old_len;
d->prefixlen = (uint16) prefixlen;
d->suffixlen = (uint16) suffixlen;
📄 src/include/access/flux.h (L142-L148)
These field comments read as commit-message / bug-postmortem prose (measured test counts, 'measured before this field existed', 'UPDATE 3194 with 1194 rows left', specific isolation-test names) rather than a header comment explaining WHY the field exists. pgsql-hackers reviewers will flag this. Header comments should be concise and explain the invariant (t_cmin describes the same writer as t_xmin, re-stamped on in-place UPDATE); move the measured-regression narrative into the commit message. The same over-long narrative style recurs throughout these headers (e.g. FLUX_TUPLE_KEYS_UPDATED, t_verptr, the fillfactor macro) and should be trimmed.
📄 src/backend/access/recno/recno_compress.c (L779-L784)
Stack buffer overread/overflow in delta decompression. For tag 0x80, bytes_stored = input[1] is attacker/corruption-controlled (0-255) but is used directly in memcpy(&abs_val, input + 2, bytes_stored) where abs_val is a fixed 8-byte uint64 on the stack. Any bytes_stored > 8 writes past abs_val, and input + 2 + bytes_stored also overreads the compressed payload. This runs on on-disk data during decompression, so a corrupted page is a crash/memory-corruption hazard. Bound bytes_stored to sizeof(uint64) (and validate against the compressed length) before the memcpy, and raise ERRCODE_DATA_CORRUPTED otherwise. The tag 0x89 path below has the same defect (stored_size = input[1] bounded only by orig_size).
💡 Suggested change
Before:
if (tag == 0x80)
{
int bytes_stored = input[1];
uint64 abs_val = 0;
memcpy(&abs_val, input + 2, bytes_stored);
After:
if (tag == 0x80)
{
int bytes_stored = input[1];
uint64 abs_val = 0;
if (bytes_stored > (int) sizeof(uint64))
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("invalid delta compression length %d", bytes_stored)));
memcpy(&abs_val, input + 2, bytes_stored);
📄 src/backend/access/recno/recno_compress.c (L823-L827)
Same unchecked-length defect for the 0x89 (other-numeric) tag: stored_size comes straight from the compressed byte input[1] (0-255) and bounds the memcpy only implicitly by orig_size. If stored_size > orig_size this overflows the output buffer (allocated as VARHDRSZ + orig_size). Validate stored_size <= orig_size before copying.
💡 Suggested change
Before:
if (tag == 0x89)
{
Size stored_size = input[1];
memcpy(VARDATA(output), input + 2, stored_size);
After:
if (tag == 0x89)
{
Size stored_size = input[1];
if (stored_size > orig_size)
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("invalid delta compression stored size %zu", stored_size)));
memcpy(VARDATA(output), input + 2, stored_size);
📄 src/backend/access/recno/recno_fsm.c (L290-L295)
Per-operation memory leak on the hot DML path. RecnoMarkPageForDefrag calls RecnoGetFSMState(), which palloc0's a RecnoFSMState plus a fresh AllocSetContext and defrag queue every call ("For simplicity, create a new state each time"), then only schedules the page and returns without ever freeing that state. This path runs from RecnoRecordFreeSpace() on every insert/update/delete that marks a page for defrag, leaking an AllocSet context per DML operation. Only RecnoOpportunisticDefrag frees the state it allocates. Either cache the FSM state per relation (as the comment claims should happen) or free it here after scheduling.
📄 src/backend/access/recno/recno_clock.c (L255-L256)
Self-inflicted denial of service driven by unsynchronized external input. RecnoCheckClockHealth() runs in the started background worker (RecnoClockStartMonitor) and can call ereport(FATAL) plus set monitor.shutdown_pending based on error_ms derived from ClockBoundData read via a plain memcpy() from an external mmap of /dev/shm/clockbound. That memcpy of a multi-field struct concurrently written by an out-of-process daemon is NOT atomic (the comment "atomic read" is incorrect) and can tear earliest/latest/error_bound_ns; the validity checks do not catch a torn-but-in-range value. A bogus error bound then terminates the worker (FATAL) and flips shutdown_pending so the main loop escalates to server shutdown. External, untrusted, unsynchronized data must never be able to shut the server down. At minimum, treat a failed sanity check as "bounds unavailable" and never FATAL on it; a versioned seqlock-style read of the shared segment is required for tear-free access.
📄 src/backend/access/recno/recno_clock.c (L367-L369)
Portability: error_ms is uint64 but is printed with %lu, which is wrong on LLP64 (Windows) and 32-bit platforms where uint64 != unsigned long. Use UINT64_FORMAT. Both the FATAL and WARNING messages here (and any other uint64-with-%lu in this file) must be fixed for the portability gate.
💡 Suggested change
Before:
ereport(FATAL,
(errmsg("recno clock error bound %lu ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
After:
ereport(FATAL,
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
📄 src/backend/access/recno/recno_clock.c (L377-L379)
Same uint64/%lu mismatch in the 50%% WARNING path. Use UINT64_FORMAT.
💡 Suggested change
Before:
ereport(WARNING,
(errmsg("recno clock error bound %lu ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
After:
ereport(WARNING,
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
📄 src/backend/access/recno/recno_operations.c (L3138-L3139)
Dangling/misleading conditional at end of recno_tuple_update. if (update_indexes) has no braces and is immediately followed by pfree(new_tuple);, so pfree() runs unconditionally but reads as guarded by the if. update_indexes was already fully assigned earlier (TU_None / TU_Summarizing), so this stray conditional does nothing and the large preceding comment describing "return TU_Summarizing" no longer matches the code. Remove the dead if so the free is plainly unconditional, and drop/repair the stale comment block above it.
💡 Suggested change
Before:
if (update_indexes)
pfree(new_tuple);
After:
pfree(new_tuple);
📄 src/backend/access/recno/recno_lock.c (L391-L395)
Dead scaffolding (YAGNI): RecnoCheckDeadlock is a stub that unconditionally returns false ("Simplified deadlock check ... would use deadlock detector") and has no callers. An unused "deadlock check" that always reports "no deadlock" is a footgun if ever wired up, and dead code should not ship. Remove it (and any prototype) until a real implementation with a caller exists.
📄 src/backend/access/recno/recno_lock.c (L429-L442)
O(n^2) bubble sort to order TIDs before taking heavyweight locks. The comment asserts N is "typically small" but nothing bounds ntids, so a caller with many TIDs pays quadratic cost right before acquiring locks. Use qsort() with an ItemPointerCompare-based comparator (as heap TID sorting does) to get O(n log n).
📄 src/backend/access/recno/recno_mvcc.c (L989-L989)
Non-ASCII characters in source comments. PostgreSQL requires ASCII-only source; box-drawing glyphs like this and arrow characters (used elsewhere in this file and recno_mvcc.c) fail the ASCII gate. Replace with ASCII (e.g. plain dashes and "->").
💡 Suggested change
Before:
/* ── Our own operation ── */
After:
/* -- Our own operation -- */
📄 src/include/access/flux.h (L870-L870)
This GUC-backed extern variable lacks PGDLLIMPORT. Every extern GUC variable exposed in a PostgreSQL header carries PGDLLIMPORT (e.g. extern PGDLLIMPORT bool enable_seqscan; in optimizer/cost.h) so that it resolves correctly when read from another module or an extension on Windows/MSVC. Add the marker to stay consistent and portable.
💡 Suggested change
Before:
extern bool flux_lazy_uncommitted_clear;
After:
extern PGDLLIMPORT bool flux_lazy_uncommitted_clear;
📄 src/include/access/flux.h (L16-L18)
Installed backend AM headers should not include postgres.h; the compiling .c file is responsible for including it first. No core AM header (heapam.h, nbtree.h, hash.h, tableam.h) does this. While flux.h is currently only pulled into backend TUs (so it compiles), the extra include is against convention and a maintenance hazard should this header ever be reached from a FRONTEND unit (compare the confirmed issue in flux_xlog.h). Drop the #include "postgres.h" and rely on the includer.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/heapam.h"
After:
#include "access/heapam.h"
📄 src/include/access/flux.h (L643-L643)
%% is printf format escaping and is wrong inside a plain C comment; it renders literally as 100%% / ~10%%. Use a single %.
💡 Suggested change
Before:
* packing pages 100%% full (heap's default) guarantees that any in-place
After:
* packing pages 100% full (heap's default) guarantees that any in-place
📄 src/include/access/flux_undo.h (L208-L214)
The three struct fields are dereferenced (delta->prefixlen/suffixlen/old_len) before the delta_len < SizeOfFluxUndoDelta guard, so a caller that passes a delta buffer shorter than SizeOfFluxUndoDelta triggers an out-of-bounds read even though the function's contract promises it "Fails (returns false) if the delta is malformed". Current callers guard the length beforehand, so this is defense-in-depth only, but the read should happen after the size check to honor the documented contract. Move the delta_len < SizeOfFluxUndoDelta check to the top before touching the struct fields.
📄 src/backend/access/flux/flux_tuple.c (L316-L324)
Broken contract / footgun: flux_form_tuple_internal completely ignores its force_shrink, rel, overflow_buffers, old_ovptrs, and old_ovpresent parameters (the comment at lines 382-387 states FLUX no longer does on-page overflow). Consequently FluxFormTupleForceShrink does NOT force any varlena off-page and behaves identically to FluxFormTuple, directly contradicting its name and docstring ("forces every inline varlena attribute larger than an overflow pointer off-page ... collapses the main tuple to its minimum footprint"). Its only caller in flux_operations.c is inside an if (false) block, so this is currently a latent trap: if that recovery branch is ever re-enabled it will silently fail to shrink and re-hit the "tuple no longer fits" ERROR. Either drop these unused parameters and the force-shrink/update variants, or implement the promised behaviour.
📄 src/backend/access/flux/flux_tuple.c (L291-L295)
Dead code: FluxFormTupleUpdate has no callers anywhere in the tree (grep finds only its definition here and the extern in flux.h). It exists solely to forward the old_ovptrs/old_ovpresent COW-overflow arguments, which flux_form_tuple_internal now ignores entirely. Per the YAGNI/minimalism discipline, remove it (and its declaration) unless a caller is coming.
📄 src/backend/access/flux/flux_tuple.c (L1079-L1085)
Inconsistency with the two sibling deform routines: here the dropped-column skip tests att_isnull(i, nulls_bitmap) unconditionally, whereas tts_flux_deform (flux_slot.c) and FluxDeformTupleUpTo (below) guard it with (header->t_infomask & FLUX_INFOMASK_HASNULL) && att_isnull(...). It happens to be safe because FluxComputeDataSize always reserves the bitmap and FluxFormTuple memsets it to 0xFF, so att_isnull returns false when HASNULL is unset -- but relying on that is fragile and diverges from the other copies of this offset-walking logic. Add the same HASNULL guard for consistency.
📄 src/backend/access/flux/flux_vm.c (L384-L387)
Dead code: FluxVMExtend has no callers anywhere in the tree. VM extension for FLUX happens on demand inside flux_vm_extend()/flux_vm_readbuf() when FluxVMSet first touches a block, so this wrapper is never invoked. Remove it (and its extern in flux.h) per the minimalism discipline.
📄 src/backend/access/flux/flux_vm.c (L407-L411)
Dead code: FluxVMTruncate has no callers anywhere in the tree. Beyond being unused, note it calls smgrtruncate() directly with no WAL logging, so if it were ever wired into a relation-truncate path the VM fork would not be truncated on a standby -- leaving stale ALL_VISIBLE/ALL_FROZEN bits that the scan-path FluxVMCheckCached optimization would trust, skipping visibility checks and returning wrong results after replay. Remove it (and its extern) or, if truncation support is actually needed, route it through the WAL-logged truncate path.
📄 src/backend/access/rmgrdesc/Makefile (L17-L19)
Ordering breaks the alphabetical convention this OBJS list otherwise follows. atmdesc.o, fileopsdesc.o, fluxdesc.o are placed correctly, but recnodesc.o is inserted before genericdesc.o (r sorts after g/h/l/m/n/r... so recnodesc belongs after replorigindesc.o). Reorder to keep the list sorted. (low confidence on impact - purely stylistic; backend build order is irrelevant and pg_waldump uses a *desc*.c wildcard, but PostgreSQL convention is strict alphabetical.)
💡 Suggested change
Before:
fileopsdesc.o \
fluxdesc.o \
recnodesc.o \
After:
fileopsdesc.o \
fluxdesc.o \
📄 src/backend/access/rmgrdesc/Makefile (L36-L38)
The pbu_undoactiondesc.o, pbu_undologdesc.o, and undodesc.o entries are appended after tblspcdesc.o, breaking the otherwise-alphabetical OBJS ordering (they should sort near the p/s/t/u positions, and recnodesc.o from the earlier hunk belongs here too). Keep the list alphabetical to match PostgreSQL's Makefile convention. Stylistic only - no build impact.
📄 src/backend/access/Makefile (L25-L28)
SUBDIRS is alphabetically ordered upstream (brin, common, gin, ... transam). undo correctly sorts after transam, but flux (belongs after common) and recno (belongs after nbtree) are appended out of order. Note the corresponding meson.build orders these differently (flux/recno before nbtree, undo last) - neither is alphabetical, and the two build systems are now inconsistent with each other. Reorder both to keep them sorted and mutually consistent. Stylistic; no functional/build impact.
📄 src/backend/access/recno/recno_overflow.c (L582-L589)
WAL insert inside an open critical section (WAL + on-disk divergence / PANIC hazard).
START_CRIT_SECTION() is entered just above PageAddItem and only closed by END_CRIT_SECTION() after the buffer-tracking block. On the if (!found) fallback here you do ReadBuffer + LockBuffer(EXCLUSIVE) + a full RecnoXLogOverflowWrite() (which calls XLogBeginInsert/XLogInsert) while the critical section is open. Buffer I/O and a second WAL insert inside an already-open crit section is not the WAL protocol used elsewhere: any ereport during the read/insert is promoted to PANIC, and the two-record scheme (a separate RECNO_OVERFLOW_WAL_LINK_UPDATE record for the link plus the main INSERT/UPDATE record for the new record) is not atomic on redo. Prepare/lock prev_buffer before START_CRIT_SECTION(), or register it as an additional block on the single forward-op WAL record (as the found path does) instead of logging it separately here.
📄 src/backend/access/recno/recno_overflow.c (L1517-L1525)
Data-loss / primary-standby divergence: Pass 2 logs the page with RecnoXLogInitPage, whose redo (recno_xlog_init_page_redo) calls RecnoInitPage() -> PageInit(), wiping ALL items on the page. But the primary only deleted the orphaned overflow records here; live tuples and still-referenced overflow records remain on the page. Because RecnoXLogInitPage registers the buffer with REGBUF_WILL_INIT (no full-page image), a crash-recovery or standby replaying this record rebuilds an EMPTY page, destroying every surviving live item. A page-delete needs its own WAL type that removes only the specified offsets (or must force a full-page image), not an init-page record.
📄 src/backend/access/recno/recno_overflow.c (L951-L955)
No WAL logging for overflow-record deletion. RecnoPageIndexTupleDelete + MarkBufferDirty mutate shared on-disk state but the change is never WAL-logged (comment admits this). Unlike TOAST -- where the toast tuple deletion IS heap-WAL-logged -- here the parent-tuple modification does not describe these overflow-page changes, so after a crash or on a standby the deleted overflow records reappear while the parent no longer references them, diverging primary/standby and leaking space that VACUUM's orphan pass may or may not reach. This on-disk mutation must be WAL-logged and replayed.
📄 src/backend/access/recno/recno_overflow.c (L383-L384)
Ad-hoc block-0 avoidance is a fragile, undocumented pseudo-invariant. Treating physical block 0 as "probably the main tuple's page" and unconditionally refusing to place overflow records there (extending the relation instead) means a legitimately-free block 0 is permanently wasted for overflow, and the count == 0 heuristic misfires whenever the caller's main page is not block 0. This is a correctness smell masquerading as an optimization; the real fix is to pass the caller's pinned block number explicitly and skip exactly that block, not block 0.
📄 src/backend/access/recno/recno_overflow.c (L1552-L1553)
Dead code: RecnoStoreOverflow, RecnoFetchOverflow and RecnoDeleteOverflow have no callers anywhere in the tree, and RecnoFetchOverflow unconditionally ereport(ERROR)s. Per the YAGNI / no-dead-code discipline these "legacy interface (kept for compatibility during transition)" shims should be removed along with their header declarations; a not-yet-wired path with a caller-less always-error function is exactly the speculative scaffolding to cut before this reaches -hackers.
📄 src/backend/access/recno/recno_slog.c (L977-L983)
Large allocation under all partition locks. This acquires all RECNO_SLOG_PARTITIONS (128) LWLocks in SHARED mode and then palloc's sizeof(RecnoSLogKey) * RecnoSLogNumEntries() -- up to 262144 entries, i.e. a multi-MB allocation -- WHILE holding every partition lock. This (1) serializes the entire sLog for the duration, blocking all concurrent DML sLog activity, and (2) risks an ereport(ERROR) on OOM while holding all locks. Move the palloc before the lock-acquisition loop (the size is a fixed upper bound independent of the locks), so no allocation happens under the locks.
📄 src/backend/access/recno/recno_slog.c (L527-L529)
Uninterruptible busy-wait. This retry loop sleeps up to 3 x 1ms with pg_usleep and re-probes shared memory but never calls CHECK_FOR_INTERRUPTS(). Any loop that sleeps/spins in the backend must be interruptible so a query cancel / SIGTERM is honored promptly. Add CHECK_FOR_INTERRUPTS() at the top of each retry iteration.
📄 src/backend/access/recno/recno_slot.c (L523-L527)
Allocates an XID on a pure read path. When a physical tuple is attached, getsysattr for xmin/xmax calls GetCurrentTransactionId(), which assigns a new XID if the current transaction has none. Reading the xmin/xmax system column of a RECNO row during a read-only statement (or in a parallel worker, where XID assignment is disallowed) would therefore wrongly consume an XID or error out. The no-tuple branch above already reads xmin off the page; use GetCurrentTransactionIdIfAny() (returning InvalidTransactionId when unassigned) here rather than forcing assignment on a read.
📄 src/backend/access/recno/recno_undo.c (L301-L302)
64-bit portability: urec_ptr is an UndoRecPtr (uint64) but is printed with %llu via (unsigned long long) casts throughout this file. Per project rules 64-bit values must use UINT64_FORMAT (and be passed without the long-long cast), never %llu/%lld. This applies to every such site in recno_undo.c (lines ~301, 312, 329, 361, 397, 414, 430).
📄 src/backend/access/recno/recno_stats.c (L250-L252)
64-bit portability: int64 statistics are printed with %lld/(long long) and uint64 HLC values with %llu/(unsigned long long). Use INT64_FORMAT / UINT64_FORMAT for these instead of the long-long casts, per project rules (never %ld/%lld for 64-bit).
📄 src/backend/access/recno/recno_overflow.c (L1539-L1540)
64-bit portability: overflow_records_found / orphans_removed are int64 but printed with %lld/(long long). Use INT64_FORMAT rather than the long-long cast.
📄 src/backend/access/recno/recno_undo.c (L239-L243)
Silent skip can permanently lose a committed row's prior version. When the current on-page slot is smaller than the before-image (an aborted in-place UPDATE that shrank the slot), the restore is skipped and the routine returns false, leaving the row in its aborted-modified state and relying on VACUUM. But recno_undo_apply still proceeds to RecnoSLogRemove() the ABORTED marker afterwards, so once the sLog op is gone the aborted image becomes visible with nothing left to restore the committed pre-image. Confirm the shrink case is truly unreachable for a committed-row restore; if it is reachable, this is silent data corruption, and the sLog marker must NOT be removed when the physical restore was skipped.
📄 src/backend/access/recno/recno_xlog.c (L302-L302)
Blocking crash recovery / streaming apply on a physical-clock wait. RecnoReplicaHandleUncertainty is invoked from recno_redo_handle_hlc, which the INSERT/UPDATE/DELETE redo handlers call for every HLC-bearing record. When recno_uncertainty_wait is on, this spins/sleeps until the local physical clock passes commit_hlc + uncertainty_ms, stalling the startup/recovery process per record. uncertainty_ms is derived from bytes read out of the WAL record, so a corrupt/large value makes the total wait effectively unbounded (the 10ms cap is only per iteration). A per-record clock wait does not belong in the redo path -- it stalls recovery and lag on every standby. At minimum bound uncertainty_ms hard, and reconsider whether the wait should happen at read time rather than during redo.
📄 src/backend/access/recno/recno_xlog.c (L284-L285)
Two trailing WAL regions collide. recno_redo_handle_hlc extracts the HLC info from the very END of the main data: data + total_len - SizeOfXlRecnoHlcInfo. But when the relation is logically logged, RecnoXLogMaybeAppendLogicalTuple appends [heap bytes][uint32 heap_len] to the main data channel AFTER the HLC info was registered (HLC is registered a few lines above, the logical trailer below). So for a record with both RECNO_WAL_HAS_HLC and RECNO_WAL_LOGICAL_TUPLE, the last SizeOfXlRecnoHlcInfo bytes are the tail of the logical trailer, not the HLC struct -- the redo HLC reader consumes garbage and the standby advances/waits on a bogus timestamp. The HLC region must be located by a scheme that does not assume it is last (e.g. read it before the logical trailer, or from a fixed offset after the fixed header + tuple), for INSERT, UPDATE and DELETE.
📄 src/include/access/recno_undo.h (L27-L30)
This comment is factually wrong about the shared, WAL-durable UNDO RM id space. There is no UNDO_RMID_ZHEAP anywhere in the tree, and undormgr.h documents id 2 as FILEOPS (not just "reserved"). So "6 = ZHEAP" invents a reservation that does not exist, and id 6 is actually free while RECNO jumps to 7. Since these ids are stamped into WAL-durable UNDO records and must be unique/stable, an inaccurate ownership map here is a real hazard for the next consumer. Either take id 6 (the next free value, per undormgr.h's "next free value" rule) or fix the comment to state the actual assignments (2=FILEOPS, 5=FLUX, 6 free).
📄 src/include/access/recno.h (L141-L141)
The comment claims this pad makes the bitmap start MAXALIGN'd, but it does not: offsetof(RecnoTupleHeader, t_attrs_bitmap) is 68 (8+8+8+6+6+4+4+4+4+4+4+2+2+1+3), which is 4 mod 8. The FLUX sibling this header claims to mirror guards exactly this with StaticAssertDecl(offsetof(FluxTupleHeader, t_attrs_bitmap) == FLUX_TUPLE_OVERHEAD, ...); RECNO has no such compile-time assert. Attribute reads self-correct via att_align_nominal() so this is not a live crash, but the comment is misleading and the missing static assert removes the guard-rail that would catch a future field-layout change silently shifting the on-disk data offset. Suggest fixing the comment and adding a StaticAssertDecl mirroring FLUX.
📄 src/include/access/recno.h (L103-L103)
Stray doubled percent sign (100%%) in a plain C comment - a printf-format artifact. Project rules require ASCII prose without stray format specifiers; should read 100%.
📄 src/include/access/recno.h (L330-L330)
Function-like macro that ignores its argument and unconditionally expands to (true). This is a footgun: it silently discards any side effects in the passed expression, and it reads as dead configurability (all call sites like if (RelationHasUndo(relation)) are now always-true branches). Since undo is now unconditional, consider removing the macro and its now-dead guard branches rather than keeping a no-op knob; at minimum the always-true nature should be obvious at call sites.
📄 src/backend/access/hash/hashinsert.c (L242-L248)
Duplicated comment block: two adjacent paragraphs describe the same thing (writing UNDO after WAL logging while holding the pin, gated by the index_undo reloption). The second paragraph is more accurate and complete; the first (lines 242-246) is redundant. Remove the first block to keep the diff minimal and avoid confusing future readers.
💡 Suggested change
Before:
/*
* Write UNDO record for the insertion if the parent table AM supports
* UNDO. This must happen after WAL logging but while we still hold the
* buffer pin (needed for BufferGetBlockNumber).
*/
/*
* Write index UNDO only when the parent table asks for it (the index_undo
After:
/*
* Write index UNDO only when the parent table asks for it (the index_undo
📄 src/backend/access/hash/hash_undo.c (L442-L445)
Inconsistent with the reference nbtree implementation: nbtree_undo_apply()'s default case returns UNDO_APPLY_ERROR with a WARNING ("unknown subtype %u") because an unrecognized info subtype signals WAL/version corruption. Here an unknown subtype is silently swallowed as UNDO_APPLY_SKIPPED, so a corrupt or forward-incompatible record is treated as a benign no-op with no diagnostic. Prefer surfacing it as the nbtree path does.
📄 src/include/access/recno_diff.h (L15-L18)
Server header must not include "postgres.h" (the .c file includes it first; frontend TUs include "postgres_fe.h"). This header only needs the base int types, which come via the includer. Remove the "postgres.h" include.
📄 src/include/access/recno_slog.h (L16-L19)
Server header must not include "postgres.h". Drop it and rely on the including .c file, as every core access/*.h header does.
📄 src/include/access/recno_xlog.h (L15-L18)
Server header must not include "postgres.h". This is especially harmful here: recno_xlog.h declares the WAL opcodes and rmgr callbacks (recno_desc/recno_identify) needed by the frontend pg_waldump path, but including "postgres.h" (plus recno.h -> heapam.h) makes the header impossible to include from a frontend TU. The result is that recnodesc.c hand-duplicates every XLOG_RECNO_* opcode with a "keep in sync" comment -- a drift hazard. Remove the "postgres.h" include and split the frontend-relevant declarations so recnodesc.c can include them instead of copying the constants.
📄 src/backend/access/recno/recno_slog.c (L1606-L1616)
Buffer lock / relcache leak in the PG_CATCH path. If ReadBuffer/LockBuffer succeeds but a later step (e.g. MarkBufferDirtyHint, or a CHECK_FOR_INTERRUPTS) throws, this catch block swallows the error via EmitErrorReport()/FlushErrorState() but never releases the content lock+pin on buf nor closes rel. Because the error is caught rather than propagated to xact abort, LWLockReleaseAll() is not invoked, so the buffer content LWLock leaks -- a held content lock at PRE_COMMIT can deadlock other backends, and the relcache reference persists. Catching an error without an internal subtransaction (BeginInternalSubTransaction / RollbackAndReleaseCurrentSubTransaction) is itself the classic unsafe pattern here. At minimum the catch must UnlockReleaseBuffer(buf) (guarded by BufferIsValid) and relation_close(rel, AccessShareLock) before FlushErrorState, and should be wrapped in a subtransaction.
📄 src/backend/access/recno/recno_stats.c (L155-L163)
Wrong location for the compression header, producing garbage compression stats. The on-disk RECNO tuple layout is [RecnoTupleHeader][MAXALIGN(null-bitmap)][attribute data], and a compressed varlena's RecnoCompressionHeader lives inside that attribute's varlena payload (after per-attribute alignment), not immediately after the fixed header. RECNO_TUPLE_OVERHEAD == offsetof(RecnoTupleHeader, t_attrs_bitmap), so (char *) hdr + RECNO_TUPLE_OVERHEAD points at the NULL bitmap. Casting those bytes to RecnoCompressionHeader yields bogus orig_size/comp_size, so total_uncompressed_size / total_compressed_size (and the reported compression_ratio) are meaningless. Either locate the header via the same deform walk used elsewhere, or drop the ratio estimate.
📄 src/backend/access/hash/hash_undo.c (L403-L407)
The forward-apply path marks the item LP_DEAD but never sets the page's LH_PAGE_HAS_DEAD_TUPLES hint on the hash opaque. Unlike nbtree (which no longer gates cleanup on BTP_HAS_GARBAGE and always scans for LP_DEAD items), the hash AM only reclaims dead items opportunistically when H_HAS_DEAD_TUPLES(pageopaque) is true (_hash_doinsert() -> _hash_vacuum_one_page(), hashinsert.c:139). Without setting the flag here, the entries marked dead during rollback are invisible to the opportunistic cleanup path and are left for full VACUUM -- directly contradicting this file's header claim that entries are marked LP_DEAD "so they are not left for VACUUM". _hash_kill_items() sets this flag whenever it marks anything dead (hashutil.c:619-623); the apply path must do the same. (moderate confidence)
💡 Suggested change
Before:
START_CRIT_SECTION();
ItemIdMarkDead(lp);
MarkBufferDirty(buffer);
After:
START_CRIT_SECTION();
ItemIdMarkDead(lp);
/*
* Set LH_PAGE_HAS_DEAD_TUPLES so _hash_vacuum_one_page()
* will reclaim this slot opportunistically; the hash AM
* gates that cleanup on H_HAS_DEAD_TUPLES(). Matches
* _hash_kill_items().
*/
HashPageGetOpaque(page)->hasho_flag |= LH_PAGE_HAS_DEAD_TUPLES;
MarkBufferDirty(buffer);
📄 src/backend/access/recno/recno_overflow.c (L437-L440)
FSM-stale retry can release a caller-owned undo/overflow buffer. buffer here may have been resolved from overflow_buffers (the cache lookup just above sets it to overflow_buffers->buffers[i].buffer, a buffer the caller still expects to remain pinned+locked for atomic WAL logging). On this stale-FSM path UnlockReleaseBuffer(buffer) is called unconditionally, dropping that pin+lock while overflow_buffers->buffers[i].buffer still references it, then a fresh target is read. The result is a pin/lock imbalance and use-after-release of a tracked buffer during subsequent WAL registration. Before releasing, verify the buffer is not one already recorded in overflow_buffers (as the cross-page link-update block below does with its found scan); only UnlockReleaseBuffer buffers that are not caller-owned.
📄 src/backend/access/recno/recno_overflow.c (L1158-L1161)
Dead code: RecnoGetOverflowStats has no callers in the tree (only its declaration and this definition). Remove the unused diagnostic function and its extern until something actually calls it.
📄 src/backend/access/hash/hash_undo.c (L149-L150)
HashUndoInsert has a 2-byte padding hole between heap_tid (ItemPointerData, 6 bytes, 2-byte aligned, ends at offset 18) and key_digest (uint32, 4-byte aligned, starts at offset 20). SizeOfHashUndoInsert is therefore 24, and hdr is stack-allocated without being zeroed, so HashUndoLogInsert() copies the 2 uninitialized padding bytes into the UNDO/WAL payload. This produces non-deterministic WAL content and trips Valgrind's uninitialized-memory checks on the buildfarm. memset(&hdr, 0, sizeof(hdr)) before populating the fields avoids it. (moderate confidence)
💡 Suggested change
Before:
hdr.index_oid = RelationGetRelid(rel);
hdr.blkno = BufferGetBlockNumber(buf);
After:
memset(&hdr, 0, sizeof(hdr));
hdr.index_oid = RelationGetRelid(rel);
hdr.blkno = BufferGetBlockNumber(buf);
📄 src/backend/access/recno/recno_slog.c (L1606-L1616)
PG_CATCH here swallows the error with EmitErrorReport()/FlushErrorState() but never releases the resources acquired in the PG_TRY: the buffer content lock + pin (buf, locked BUFFER_LOCK_EXCLUSIVE) and the relation reference (rel, opened with AccessShareLock). If ReadBuffer/LockBuffer/PageGetItem errors out mid-way, control resumes here without unwinding through the normal abort path, so LWLockReleaseAll and the relcache/resowner cleanup are bypassed. Result: a leaked buffer content LWLock (deadlock risk against any later acquirer) and a leaked relcache pin, all at PRE_COMMIT. Catch-and-continue like this is only safe inside a subtransaction (BeginInternalSubTransaction / RollbackAndReleaseCurrentSubTransaction), which restores the resource owner. Either wrap the per-entry work in an internal subtransaction, or drop the PG_TRY entirely and let the error propagate (as the comment's fallback to the lazy path suggests is acceptable).
📄 src/include/access/relscan.h (L147-L150)
This comment describes xs_want_itup_recheck as a generic "index AM" contract, but in practice only nbtree honors the flag: gist/gin/spgist/hash/brin scan paths look at xs_want_itup only (verified in gistget.c, spgscan.c, etc.), and flux_operations.c itself states "xs_want_itup_recheck is honoured only by nbtree". The safety net that keeps this correct is the amdeletemark!=NULL gate in flux_inplace_index_maint_possible (only nbtree qualifies), not the flag being universally honored. As written, the comment is a footgun: a future author could reasonably assume setting this flag on any AM populates xs_itup for the recheck, which is false. Recommend narrowing the wording to state that only nbtree currently honors it.
💡 Suggested change
Before:
* slot). This separate flag asks the index AM to keep xs_itup populated
* for the recheck during an ordinary (buffer-slot) index scan.
*/
bool xs_want_itup_recheck;
After:
* slot). This separate flag asks the index AM to keep xs_itup populated
* for the recheck during an ordinary (buffer-slot) index scan. Note only
* nbtree currently honors this flag; delete-marking is gated on nbtree via
* amdeletemark, so no other AM reaches the recheck path.
*/
bool xs_want_itup_recheck;
📄 src/backend/access/recno/recno_xlog.c (L380-L383)
Block-id overrun guard is off-by-N. WAL block ids run 0..XLR_MAX_BLOCK_ID (32). Block 0 is the main data page; overflow buffers take ids 1..N; the per-backend undo pages then take recno_undo_block_id..+RECNO_PBU_MAX_UNDO_BLOCKS (up to 4 more). With MAX_OVERFLOW_BUFFERS == XLR_MAX_BLOCK_ID == 32, a tuple with 32 distinct overflow buffers assigns block ids 1..32, leaving no room for block 0's undo/overflow siblings, and the undo registration (RecnoPbuRegisterUndoBuffers) would push ids past 32. This guard only rejects count > 32, so it does not catch the case where count + 1 (main) + undo blocks exceeds the limit; XLogRegisterBuffer then trips its own Assert / corrupts the record. Tighten the guard to account for the main block and the reserved undo blocks (e.g. count + 1 + RECNO_PBU_MAX_UNDO_BLOCKS > XLR_MAX_BLOCK_ID). Same issue in RecnoXLogUpdate.
📄 src/backend/access/recno/recno_undo.c (L122-L123)
Bogus page LSN on unlogged/temp relations. For a non-WAL relation this stamps the page with GetXLogInsertRecPtr(), an LSN that does not correspond to any WAL record covering this change. Heap and the rest of the tree leave such pages at InvalidXLogRecPtr (no PageSetLSN). Setting a live insert-position LSN here can mislead LSN-ordering checks (e.g. buffer flush ordering, wal_consistency comparisons of the page against a later real LSN). Just return without touching the LSN when RelationNeedsWAL is false.
📄 src/backend/access/index/indexam.c (L267-L271)
The capability gate here uses RelationUndoEngine(heapRelation) == UNDO_ENGINE_NONE, which is only equivalent to am_supports_undo. Every other delete-marking gate in the tree (nbtinsert.c, nbtree.c, nbtsort.c, plancat.c, tableam.c) instead uses the more precise RelationSupportsDeleteMarking() (i.e. am_index_delete_marking), and tableam.h line 399 documents that as the predicate for delete-marking. A hypothetical table AM with am_supports_undo = true but am_index_delete_marking = false would pass this gate and (given a nbtree index) actually delete-mark, contrary to design. Harmless today since FLUX/RECNO both set am_index_delete_marking = true, but it is a latent footgun and the doc comment ("parent table must use UNDO") describes the weaker condition. Recommend gating on RelationSupportsDeleteMarking(heapRelation) for consistency. Confidence: moderate. Same applies to index_undo_mark below.
💡 Suggested change
Before:
if (RelationUndoEngine(heapRelation) == UNDO_ENGINE_NONE)
return false;
return indexRelation->rd_indam->amdeletemark(indexRelation, heapRelation,
values, isnull, heap_t_ctid);
After:
if (!RelationSupportsDeleteMarking(heapRelation))
return false;
return indexRelation->rd_indam->amdeletemark(indexRelation, heapRelation,
values, isnull, heap_t_ctid);
📄 src/backend/access/nbtree/nbtinsert.c (L1436-L1439)
The parenthetical "(UNDO insertion involves I/O)" is inaccurate and misleading. NbtreeUndoLogInsert -> nbtree_undo_write uses DeferXactUndoData(), which (per its own header comment in nbtree_undo.c) writes no WAL and performs no I/O: "Deferral itself writes no WAL, so no critical section is needed here." The actual reason this must run outside the critical section is that PrepareXactUndoDataParts() can palloc. Fix the comment so it states the real constraint; a wrong rationale here invites a future change to (unsafely) move the call back inside the crit section.
💡 Suggested change
Before:
* Write nbtree UNDO record for the insertion. This is done after the
* critical section (UNDO insertion involves I/O) but while we still
* hold the buffer lock. The UNDO record enables cleanup of this
* index entry if the transaction aborts.
After:
* Write nbtree UNDO record for the insertion. This runs after the
* critical section because PrepareXactUndoDataParts() can palloc (it
* must not be called in a critical section); the record itself is
* deferred and writes no WAL here. We still hold the buffer lock.
* The UNDO record enables cleanup of this index entry if the
* transaction aborts.
📄 src/backend/access/nbtree/nbtinsert.c (L162-L163)
Comments across all five files reference private tracker shorthand ("Phase 5", "Phase 8c", invariants "I2/I3/I5") and even a private design file (BTreeTupleIsDeleteMarked's header cites access/nbtree/PHASE5_DELETE_MARKING_DESIGN.md). For a patch destined for pgsql-hackers, comments must be self-contained and explain the reasoning in terms a reader of the tree can follow, without references that are meaningless outside the private design doc. Recommend a single sweep replacing "Phase 5/8c" and "invariant Ix" with a short in-tree explanation (or a pointer to the README that ships in this patch), and dropping the reference to the non-shipped .md file. Flagging once here rather than per-occurrence.
📄 src/backend/access/nbtree/nbtree.c (L2030-L2030)
Non-atomic tombstone replacement in the full-page fallback. This block runs inside the caller's transaction but NOT inside a single crit-section/atomic step: it deletes the plain entry (WAL-logged, buffer released), then calls _bt_doinsert() to re-insert the delete-marked tuple as a separate operation. _bt_doinsert() can legitimately raise an error (page-split palloc failure, CHECK_FOR_INTERRUPTS, lock acquisition, out-of-space in an extreme case). If it throws after the plain entry has already been physically deleted and WAL-logged, the tombstone is permanently lost: the old (k_old, T) entry is gone and the delete-marked entry was never inserted. Old-snapshot readers can then no longer reach the before-image via the index, i.e. index/heap divergence (invariant I5/I2). Recovery replays the delete (XLOG_BTREE_DELETE_MARK deleted=true) but there is no guaranteed paired XLOG_BTREE_INSERT_LEAF, so crash recovery reproduces the same lost tombstone. The delete and the re-insert must be made a single atomic unit (or the whole operation restructured so a failure leaves the original plain entry intact). Confidence: high.
📄 src/backend/access/nbtree/nbtinsert.c (L533-L534)
This unconditional skip of delete-marked entries also fires on the UNIQUE_CHECK_EXISTING recheck path. That path (execIndexing.c, exclusion-constraint / deferred recheck) requires re-finding the exact entry via the ItemPointerCompare(&htid, &itup->t_tid) == 0 test farther down that sets found = true; if that entry is delete-marked, this goto jumps past that test, found stays false, and _bt_check_unique() ends by throwing ERROR "failed to re-find tuple within index" (line ~785). If a delete-marking table's index can ever be checked with UNIQUE_CHECK_EXISTING, this is a spurious error. Either confirm delete-marking indexes never take the UNIQUE_CHECK_EXISTING path (and Assert it), or exempt that path from the skip (e.g. still allow the found-detection to run before skipping). Confidence: moderate -- reachability depends on whether exclusion/deferred-recheck constraints coexist with delete-marking.
📄 src/include/access/nbtree.h (L478-L479)
Dangling reference: the file access/nbtree/PHASE5_DELETE_MARKING_DESIGN.md does not exist in the tree (this change group adds no such document, and file_find finds none). A comment pointing at a non-existent design doc will confuse future readers and reviewers. Either add the design document to the tree or replace the reference with the relevant -hackers thread Message-Id / an in-tree README section. Same dangling reference also appears in the BT_IS_DELETE_MARKED comment in src/include/access/nbtree.h. Confidence: high.
📄 src/backend/access/nbtree/nbtree.c (L1872-L1872)
Dangling reference: access/nbtree/PHASE5_DELETE_MARKING_DESIGN.md is not present in the tree. Point at an in-tree README or the design-discussion Message-Id instead. Confidence: high.
📄 src/backend/access/nbtree/nbtree.c (L2045-L2049)
The xl_btree_delete_mark WAL-emit sequence (build xlrec, XLogBeginInsert/RegisterData/RegisterBuffer/[RegisterBufData]/XLogInsert/PageSetLSN) is copy-pasted in five places across btdeletemark() and btundomark(). A future change to the record format or registration order must be mirrored in all five, which is error-prone. Consider a small static helper, e.g. _bt_log_delete_mark(rel, buf, offnum, setmark, deleted, tup, tupsz), to centralize it. Confidence: high (maintainability, not correctness).
📄 src/backend/access/rmgrdesc/fluxdesc.c (L54-L60)
These frontend shadow structs diverge from the authoritative FRONTEND-guarded definitions in flux_xlog.h. The real xl_flux_insert/xl_flux_delete are {offnum, flags, uint32 tuple_len, uint64 commit_ts} (16 bytes) and xl_flux_update includes old_tuple_len/new_tuple_len -- none has an xact_ts field. As written here, the desc reads a non-existent xact_ts (over-reads past the 16-byte record), omits tuple_len, and the oversized sizeof(*_fe) used as the length guard will reject valid records. flux_xlog.h already exposes FRONTEND-safe copies of these structs plus the flux_desc/flux_identify prototypes, so this whole file should #include "access/flux_xlog.h" and reuse them rather than re-duplicating (incorrectly). Any future drift is undetectable at compile time.
📄 src/backend/access/rmgrdesc/undodesc.c (L71-L75)
xl_undo_apply.operation_type carries an AM-specific UNDO subtype, not a globally unique code. FLUX_UNDO_INSERT, RECNO_UNDO_INSERT, HASH_UNDO_INSERT and NBTREE_UNDO_INSERT_LEAF are all 0x0001 with different meanings, and there is no heap AM defining the PRUNE/INPLACE/HOT_UPDATE names hardcoded here. This invented mapping produces misleading pg_waldump output (e.g. labels a RECNO delete's 0x0002 as "DELETE" only by coincidence, and 0x0006 as "HOT_UPDATE" which no AM emits). Either print the raw subtype value or key the mapping off the record's rmid/AM; do not assume a single global operation_type namespace.
📄 src/backend/access/rmgrdesc/undodesc.c (L37-L39)
urec_ptr/start_ptr/discard_ptr/old_seal_ptr are UndoRecPtr (uint64) and new_size is uint64; use UINT64_FORMAT rather than %llu with a (unsigned long long) cast. This is the tree's portability convention for 64-bit integers (see pbu_undologdesc.c using UndoLogOffsetFormat, which is UINT64_FORMAT).
📄 src/backend/access/rmgrdesc/pbu_undoactiondesc.c (L36-L37)
urec_ptr is UndoRecPtr (uint64); prefer UINT64_FORMAT over %016llX with (unsigned long long). There is no portable 64-bit hex length modifier, and the surrounding UNDO desc files standardize on UINT64_FORMAT for these pointers.
📄 src/backend/access/rmgrdesc/atmdesc.c (L38-L46)
No default case: an unrecognized info value leaves buf empty and atm_identify returns NULL, producing blank pg_waldump output instead of a diagnostic. fileopsdesc.c and recnodesc.c emit an "unknown ... op code" message for this case; keep the ATM desc consistent.
📄 src/backend/access/rmgrdesc/undodesc.c (L137-L139)
undo_desc has no default branch, so an unknown info value (or a record type not handled here) leaves buf empty. Add a default that emits the unrecognized op code, matching fileopsdesc.c/recnodesc.c.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L6-L7)
This comment is misleading: flux_xlog.c does not define flux_desc/flux_identify (verified: the only definitions are here, and rmgrlist.h binds RM_FLUX_ID to these). The header's frontend-safe struct copies and prototypes are what should be reused. Please correct the comment to avoid implying a second, backend copy exists.
📄 src/backend/access/rmgrdesc/recnodesc.c (L12-L12)
PostgreSQL source headers do not carry per-file "Author:" credit lines; attribution belongs in the commit message / release notes, not the file header. Please drop the author line to match the surrounding rmgrdesc files.
📄 src/backend/access/rmgrdesc/nbtdesc.c (L139-L141)
The desc output omits the 'deleted' flag, which distinguishes the full-page fallback (entry physically deleted, redo path PageIndexTupleDelete) from the in-place rewrite. Since redo behavior branches on 'deleted', pg_waldump readers debugging delete-mark WAL cannot tell the two cases apart. Consider printing it too, e.g. "offnum: %u, setmark: %c, deleted: %c". Confidence: high (debuggability, not correctness).
📄 src/backend/access/nbtree/nbtinsert.c (L1446-L1450)
Index UNDO is only logged on the non-split insert path. When a leaf insert triggers a page split, _bt_insertonpg takes the if (PageGetFreeSpace(page) < itemsz) branch and delegates to _bt_split() (which physically inserts the new leaf entry into the left/right half); that branch never calls NbtreeUndoLogInsert. Consequently, for a table using index UNDO (RelationUsesIndexUndo() true), any INSERT whose index entry happens to land during a page split gets no UNDO record. On abort the phantom leaf entry is never killed, leaving a live index tuple pointing at a rolled-back heap tuple -- index corruption / wrong scan results. The split path must also emit an INSERT_LEAF UNDO record for the new entry (recorded against whichever half it ended up on). Confidence: high.
📄 src/include/access/nbtxlog.h (L203-L206)
The comment is inaccurate about where the tuple image lives. The emit path (btdeletemark/btundomark) logs the replacement image via XLogRegisterBufData(0, ...) as block-0 data, and redo (btree_xlog_delete_mark) reads it with XLogRecGetBlockData(record, 0, &newsize) -- not appended to the fixed record data (XLogRecGetData). Wording like "follows the fixed portion of the record" and "NEW TUPLE IMAGE FOLLOWS AT END OF STRUCT" will mislead anyone implementing redo/desc for this record. State that the image is registered as block-0 data (and is absent under a full-page image, as with any registered block data). Confidence: high.
📄 src/backend/access/nbtree/nbtree_undo.c (L85-L85)
This packing claim is wrong. heap_tid (ItemPointerData, 6 bytes) ends at byte offset 18, and key_digest (uint32, 4-byte alignment) is padded to offset 20 -- so there is a 2-byte hole and sizeof(NbtreeUndoInsertLeaf) is 24, not 22. The code is self-consistent because it uses offsetof()/SizeOfNbtreeUndoInsertLeaf and memcpy throughout, but the comment misstates both the byte count and the "no holes" property. Correct the comment (or reorder fields to actually remove the hole). Confidence: high.
📄 src/backend/access/table/tableam.c (L873-L876)
[high confidence] This gate does not exclude the CREATE INDEX CONCURRENTLY / REINDEX CONCURRENTLY validate phase. validate_index() (index.c:3576, table_index_validate_scan) runs in a separate transaction from the one that created the index; by then the index catalog row is already committed, so rd_createSubid and rd_firstRelfilelocatorSubid are both InvalidSubTransactionId and both checks pass. The missing-entry index_insert calls in that phase carry a live heapRelation, so RelationUsesIndexUndo() returns true and per-row index UNDO is emitted against an already-committed index.
If the validate transaction aborts, the index is NOT removed (it is committed, merely indisvalid=false), yet the anchored UNDO records will be applied during that abort and mark the freshly inserted leaf entries LP_DEAD (nbtree_undo.c:7-8) even though they point at legitimately live heap tuples. That is index corruption, not a harmless no-op. The docstring's premise ("aborting the transaction removes the whole index, so per-entry UNDO for it could only ever be a no-op at apply time") is false for the CONCURRENTLY path.
Add a guard that excludes an index whose entry is being (re)validated concurrently -- e.g. skip when !indexrel->rd_index->indisvalid (invalid indexes are exactly the CIC/RIC build/validate targets), or route the validate-scan inserts around the UNDO gate. Please also correct the docstring.
💡 Suggested change
Before:
/* A brand-new or REINDEXed index is dropped wholesale by the abort. */
if (indexrel->rd_createSubid != InvalidSubTransactionId ||
indexrel->rd_firstRelfilelocatorSubid != InvalidSubTransactionId)
return false;
After:
/*
* A brand-new or REINDEXed index is dropped wholesale by the abort.
* Also skip an index that is not yet valid: CREATE/REINDEX INDEX
* CONCURRENTLY validates a committed-but-invalid index in a separate
* transaction (rd_createSubid is already Invalid there), and index UNDO
* applied on that transaction's abort would kill live entries of a
* committed index.
*/
if (indexrel->rd_createSubid != InvalidSubTransactionId ||
indexrel->rd_firstRelfilelocatorSubid != InvalidSubTransactionId ||
!indexrel->rd_index->indisvalid)
return false;
📄 src/include/access/amapi.h (L207-L207)
[high confidence] The internal development-phase labels ("Phase 5", "Phase 8c") are meaningless outside this fork's development timeline and will read as noise to upstream reviewers. Per the comment-accuracy/minimalism discipline, comments should describe what the code does, not reference private phase numbering. Same for the /* can be NULL (Phase 5) */ and /* can be NULL (Phase 8c) */ markers on the struct members below. Drop the phase references.
💡 Suggested change
Before:
* delete-mark an existing (key, TID) entry in place (Phase 5)
After:
* delete-mark an existing (key, TID) entry in place
📄 src/include/access/amapi.h (L221-L221)
[high confidence] Remove the internal "Phase 8c" reference; it is meaningless outside this fork's development timeline.
💡 Suggested change
Before:
* undo an in-place indexed-column UPDATE's index change (Phase 8c)
After:
* undo an in-place indexed-column UPDATE's index change
📄 src/include/access/amapi.h (L348-L349)
[high confidence] Drop the internal phase labels from these member comments; "(Phase 5)"/"(Phase 8c)" carry no meaning for upstream readers.
💡 Suggested change
Before:
amdeletemark_function amdeletemark; /* can be NULL (Phase 5) */
amundomark_function amundomark; /* can be NULL (Phase 8c) */
After:
amdeletemark_function amdeletemark; /* can be NULL */
amundomark_function amundomark; /* can be NULL */
📄 src/include/access/rmgrlist.h (L53-L62)
The RmgrIds enum values are derived from the order of PG_RMGR entries in this file, and those values are the WAL rmgr IDs written to disk. Wrapping RM_FLUX_ID/RM_FILEOPS_ID/RM_RECNO_ID in #ifdef makes the numeric IDs of every following entry (RM_UNDOLOG_ID, RM_UNDOACTION_ID, RM_NEXT_ID, RM_MAX_BUILTIN_ID) build-configuration dependent. Any translation unit that includes rmgrlist.h without the exact same -DUSE_FLUX/-DUSE_FILEOPS/-DUSE_RECNO flags computes a different mapping and will misinterpret WAL. These macros are injected only via override CPPFLAGS in src/Makefile.global.in / meson.build and are NOT in any public config header (pg_config.h), so an out-of-tree extension or custom rmgr module built with PGXS sees the entries compiled out, shifting RM_UNDOLOG_ID/RM_UNDOACTION_ID and corrupting rmgr-ID interpretation. The project comments already state these macros are "always defined"; conditionally compiling always-present entries is exactly the "#ifdef fencing your feature" anti-pattern the conventions forbid. Remove the #ifdefs and register these rmgrs unconditionally (confidence: high).
📄 src/include/access/rmgrlist.h (L60-L60)
Comments in this list should explain WHY, not carry release-planning labels. "Phase 10" and "resurrected" are project-process notes that will drift and read as noise in the upstream tree. Trim to a factual description of the rmgr (e.g. what RECNO's WAL covers), or drop the comment entirely for consistency with the other entries which carry none (confidence: moderate).
📄 src/backend/access/transam/rmgr.c (L9-L9)
This #include is placed before the main include group and outside the IWYU pragma: begin_keep block, yet it is only needed to supply fileops_redo/desc/identify for the RM_FILEOPS_ID PG_RMGR entry expanded from rmgrlist.h. It belongs inside the begin_keep block with the other rmgr xlog headers (e.g. next to the new access/* includes below). Its placement also breaks the existing include ordering. Note it is unconditional while the RM_FILEOPS_ID entry it feeds is under #ifdef USE_FILEOPS, which is inconsistent with the conditional flux/recno includes below (confidence: high).
📄 src/backend/access/transam/rmgr.c (L44-L45)
This new include block is appended after utils/relmapper.h, breaking the alphabetical/logical ordering of the existing IWYU keep group (brin_xlog.h ... relmapper.h were sorted). For minimal, pgindent-clean diffs, merge these into the sorted list rather than tacking them on at the end (confidence: moderate).
📄 src/include/access/tableam.h (L403-L406)
Double blank line after the new am_index_delete_marking field. The added + blank line stacks on top of the pre-existing blank line before the "Slot related callbacks" comment, leaving two consecutive blank lines. pgindent/git diff --check cleanliness: collapse to a single blank line. [low confidence]
💡 Suggested change
Before:
bool am_index_delete_marking;
/* ------------------------------------------------------------------------
After:
bool am_index_delete_marking;
/* ------------------------------------------------------------------------
📄 src/include/access/amapi.h (L214-L218)
The public IndexAmRoutine gains two new callbacks (amdeletemark, amundomark), but doc/src/sgml/indexam.sgml -- which documents the IndexAmRoutine struct member-by-member and describes every optional callback (amgettuple/amgetbitmap/etc.) -- is not updated in this change. A user-visible index-AM API extension without a corresponding SGML update is incomplete for a pgsql-hackers submission; add the new members to the struct listing and document their contract (semantics, return value, when NULL is allowed). [high confidence]
📄 src/backend/access/transam/twophase.c (L160-L160)
Redundant declaration and contradictory comment. This file already adds #include "storage/fileops.h" above (in this same diff), and that header declares extern void FileOpsAtPrepare(void);. So this local prototype is a duplicate, and the comment's justification ("to avoid pulling storage/fileops.h into this file") is factually wrong. Drop this local prototype and rely on the header include, or drop the include if the local prototype is intended.
📄 src/backend/access/transam/twophase.c (L1027-L1027)
The comment narrates the patch's incremental bump history (0x57F94534 -> 0x57F94535 -> 0x57F94536), but the code only ever ships the final value. Comments should describe current state, not the sequence of edits that produced it. Condense to a single statement of why the current magic corresponds to the current on-disk format.
📄 src/backend/access/transam/twophase.c (L3039-L3042)
Inconsistent NULL-handling versus the sibling RecoveryTransactionIdIsPrepared() (which guards both max_prepared_xacts <= 0 and TwoPhaseState == NULL). Here only max_prepared_xacts <= 0 is checked before dereferencing TwoPhaseState->prepXacts[i]. In the normal running server TwoPhaseState is always initialized when this is reached (callers gate on UndoLogShared != NULL), so this is not a confirmed crash, but for defensive consistency add the same TwoPhaseState == NULL guard.
💡 Suggested change
Before:
if (max_prepared_xacts <= 0)
return InvalidXLogRecPtr;
LWLockAcquire(TwoPhaseStateLock, LW_SHARED);
After:
if (max_prepared_xacts <= 0)
return InvalidXLogRecPtr;
if (TwoPhaseState == NULL)
return InvalidXLogRecPtr;
LWLockAcquire(TwoPhaseStateLock, LW_SHARED);
📄 src/backend/access/transam/twophase_rmgr.c (L25-L30)
This comment claims the FILEOPS callbacks are "declared locally by prototype to avoid pulling storage/fileops.h in here", but line 23 of this same diff adds #include "storage/fileops.h", and no local FILEOPS prototype is actually added (the callbacks are declared in twophase_rmgr.h). The comment contradicts the code. Additionally, the fileops_twophase_* symbols come from twophase_rmgr.h (already included), so the storage/fileops.h include appears unnecessary here. Remove the stale comment and drop the unused include (minimal diff).
📄 src/backend/access/transam/xlog.c (L767-L769)
Declaring these entry points via a local extern prototype instead of including the owning header (access/perbackend/pbu_undolog.h) is a maintainability/portability hazard. pbu_undolog.h does declare both functions with matching signatures (verified), so the prototype is redundant duplication today. More importantly, the workaround only exists because two independent UNDO engines define clashing type names (UndoRecPtr, UndoLogControl) in shared headers. If either signature ever drifts, the compiler will not catch the mismatch here -- it becomes a silent linker/ABI mismatch. The underlying header type collision is the real defect and should be resolved (namespace the pbu types) rather than papered over with hand-copied prototypes. (moderate confidence)
📄 src/backend/access/transam/xlog.c (L8319-L8329)
This checkpoint phase-timing instrumentation is unrelated observability scaffolding bundled into an UNDO feature patch; it should be split out (or dropped) per minimal-diff discipline. It largely duplicates what LogCheckpointEnd() already reports, and it scatters eight INSTR_TIME pairs across CreateCheckPoint. Secondary nits if kept: (1) the eight accumulators are named _ms but the message divides by 1000.0 and labels them s -- misleading; store/print consistently. (2) The message uses mixed-case phase labels (SyncPre, DelayStart, ...) which is not the usual PostgreSQL log style. (low confidence on the bug axis; high confidence this is scope creep)
📄 src/backend/access/transam/xlogrecovery.c (L1911-L1915)
Comment inaccuracy: this claims PbuPerformUndoRecovery() "Runs unconditionally for the same promoted-standby reason as PerformUndoRecovery() above", but PerformUndoRecovery() above is not unconditional -- it is guarded by if (UndoRecoveryNeeded()). The stated parallel is misleading. PbuPerformUndoRecovery() itself is a tolerated no-op when no per-backend undo logs exist (it walks UndoLogNext() which returns NULL), so there is no correctness bug, but the justification comment should be corrected to describe the actual guard difference. (moderate confidence)
📄 src/include/access/xlog_internal.h (L35-L35)
XLOG_PAGE_MAGIC jumps two steps (0xD122 -> 0xD124), skipping 0xD123. A magic bump is legitimate here because the patch adds new rmgrs/WAL record types, but each format-incompatible change should bump it exactly once; a two-step jump looks like a merge artifact or an accidental double-increment and will conflict against upstream, which owns this value. Confirm the increment is deliberate (0xD123 is not used anywhere -- verified) and coordinated. Related: this PR also bumps CATALOG_VERSION_NO in catversion.h, which is the committer's job at push time and should be dropped from the patch. (moderate confidence)
📄 src/backend/access/transam/xact.c (L2819-L2825)
This comment contradicts the shipped implementation. XactUndoHasUnrecoverableUndo() (xactundo.c) now return false; unconditionally, and its own header comment states the opposite of what is written here: "Both UNDO mechanisms are now recoverable across 2PC" -- the permanent chain-head LSN is read back in twophase.c and fed to ATMAddAborted() by FinishPreparedTransaction() on ROLLBACK PREPARED. So the claim that "nothing ... ever reads it back" and that "Silently proceeding would make ROLLBACK PREPARED a no-op ... corrupting data" is stale and describes a rejection/corruption path that no longer exists. Update this comment to match the guard's actual (now-permissive) behavior, or the reader is badly misled about a critical 2PC/UNDO invariant.
📄 src/backend/access/transam/xact.c (L3128-L3130)
This comment is stale and self-contradictory with the code immediately below it. It states "Physical UNDO application is NOT needed during standard transaction abort" and that UNDO is preserved "for ... future in-place update mechanisms." But ~40 lines later AtAbort_XactUndo() applies physical UNDO synchronously (inline apply of the WAL chain via ApplyUndoChainFromWAL) for the in-place AMs that already shipped (flux/recno). The "future" framing is wrong -- that machinery is live now. Rewrite to reflect that (a) s->undoRecPtr here is the per-relation chain pointer that the abort path does NOT consume (abort reads XactUndo.last_batch_lsn[]), and (b) physical UNDO IS applied by AtAbort_XactUndo() below.
📄 src/include/access/xact.h (L154-L154)
These new hook function-pointer externs are missing PGDLLIMPORT. Every other exported hook pointer in the tree's headers (e.g. planner_hook, ExecutorStart_hook, ProcessUtility_hook) is declared extern PGDLLIMPORT, and all other externs in this very header use it. On Windows/MSVC, an out-of-tree module/extension that reads or assigns these pointers will fail to link without PGDLLIMPORT. Add PGDLLIMPORT to all five.
💡 Suggested change
Before:
extern void (*TableAMPrepare_hook) (void);
After:
extern PGDLLIMPORT void (*TableAMPrepare_hook) (void);
📄 src/include/access/xact.h (L169-L172)
Missing PGDLLIMPORT on these four exported hook pointers (same issue as TableAMPrepare_hook above). Cross-module/extension assignment on Windows/MSVC will fail to link.
💡 Suggested change
Before:
extern void (*PendingPhysOpsDo_hook) (bool isCommit);
extern void (*PendingPhysOpsPostPrepare_hook) (void);
extern void (*PendingPhysOpsAtSubCommit_hook) (void);
extern void (*PendingPhysOpsAtSubAbort_hook) (void);
After:
extern PGDLLIMPORT void (*PendingPhysOpsDo_hook) (bool isCommit);
extern PGDLLIMPORT void (*PendingPhysOpsPostPrepare_hook) (void);
extern PGDLLIMPORT void (*PendingPhysOpsAtSubCommit_hook) (void);
extern PGDLLIMPORT void (*PendingPhysOpsAtSubAbort_hook) (void);
📄 src/backend/access/transam/xact.c (L333-L333)
TableAMPrepare_hook is dead scaffolding: it is defined here and declared in xact.h with an elaborate comment about being "called from PrepareTransaction(), between StartPrepare() and EndPrepare()", but a tree-wide search finds no call site and no assignment anywhere. Unlike the PendingPhysOps* hooks (which are assigned in fileops_undo.c and called in xact.c), nothing wires this up. Per the minimalism/YAGNI gate, remove it until a caller exists, or actually add the call in PrepareTransaction if it is needed by an AM in this series.
📄 src/include/access/xact.h (L409-L410)
Inaccurate magic value in the comment. It claims the bump was 0xD120 -> 0xD121, but xlog_internal.h in this same change sets XLOG_PAGE_MAGIC to 0xD124 (from a prior 0xD122). Correct the cited values so the comment does not mislead about which WAL format version carries this field.
📄 src/include/access/xact.h (L423-L424)
This StaticAssertDecl is ineffective: offsetof(xl_xact_prepare, last_batch_lsn) > 0 is trivially true for any non-first field and does NOT verify that the array size (3) matches NUndoPersistenceLevels -- which is the invariant the surrounding comments claim to guard. The real guard is StaticAssertDecl(NUndoPersistenceLevels == 3, ...) in xactundo.c. Either drop this decorative assert or make it meaningful; as written it gives false confidence that a change to NUndoPersistenceLevels would be caught here.
📄 src/backend/access/transam/twophase_rmgr.c (L51-L53)
The "(postabort-only)" annotation is inaccurate: the postcommit array below registers pbu_twophase_postcommit (a real function defined in pbu_recovery.c, not NULL), so per-backend UNDO has both a postcommit and a postabort callback. Drop "(postabort-only)" or correct it to reflect that both are registered. [low confidence: comment-accuracy only]
💡 Suggested change
Before:
NULL, /* FILEOPS (post-commit/abort only) */
NULL, /* per-backend UNDO (postabort-only) */
flux_twophase_recover /* FLUX */
After:
NULL, /* FILEOPS (post-commit/abort only) */
NULL, /* per-backend UNDO (post-commit/abort only) */
flux_twophase_recover /* FLUX */
📄 src/backend/access/transam/twophase.c (L79-L82)
Include ordering is not alphabetical. PostgreSQL keeps includes in each group alphabetical: access/atm.h should precede access/commit_ts.h, and access/xactundo.h belongs at the end of the access/* group (after access/xlogutils.h). Reorder to keep pgindent/community convention.
💡 Suggested change
Before:
#include "access/commit_ts.h"
#include "access/atm.h"
#include "access/xactundo.h"
#include "access/htup_details.h"
After:
#include "access/atm.h"
#include "access/commit_ts.h"
📄 src/backend/access/transam/twophase_rmgr.c (L17-L18)
Include ordering is not alphabetical: access/flux.h should precede access/multixact.h. Reorder to follow the community's alphabetical-include convention.
💡 Suggested change
Before:
#include "access/multixact.h"
#include "access/flux.h"
After:
#include "access/flux.h"
#include "access/multixact.h"
#include "access/twophase_rmgr.h"
📄 src/backend/access/transam/xlogrecovery.c (L83-L83)
Redundant extern prototype: PbuPerformUndoRecovery() is already declared with an identical signature in access/perbackend/pbu_undorequest.h (verified). Re-declaring it locally is the same maintainability/portability hazard flagged for the pbu prototypes in xlog.c: if the canonical signature ever changes, this stale copy silently diverges and the compiler cannot catch the mismatch across TUs. If the justification is genuinely a header type conflict, that conflict should be fixed at the header level (e.g. a narrow forward-declaration header) rather than by scattering hand-written prototypes into recovery core. Confidence: high.
📄 src/backend/access/transam/xlog.c (L59-L60)
Include ordering: access/undo_xlog.h should sort before access/undolog.h because _ (0x5F) collates before l (0x6C) -- the same convention that places xlog_internal.h before xlogarchive.h in this very file. Note xlogrecovery.c in this same patch orders them correctly (undo_xlog.h then undolog.h), so the two files are inconsistent. Swap for alphabetical order and cross-file consistency. Confidence: high.
💡 Suggested change
Before:
#include "access/undolog.h"
#include "access/undo_xlog.h"
After:
#include "access/undo_xlog.h"
#include "access/undolog.h"
📄 src/backend/access/transam/xact.c (L3184-L3186)
Stale/inaccurate comment. It claims the per-backend UNDO apply "is a no-op unless the per-backend engine produced UNDO for this xact (which no access method does yet)". This is contradicted within this very changeset: both the new flux (flux_handler.c: am_undo_engine = UNDO_ENGINE_PERBACKEND) and recno (recno_handler.c) access methods emit per-backend UNDO for their DML and rely on PbuAtAbort_ApplyUndo() to roll it back on abort. The parenthetical is factually wrong and should be removed/corrected so the comment describes current behavior.
📄 src/backend/access/transam/xact.c (L6752-L6754)
Incorrect magic values in this comment: it states the bump was 0xD120 -> 0xD121, but xlog_internal.h in this same change sets XLOG_PAGE_MAGIC to 0xD124. (This is the same inaccuracy already flagged in xact.h; correct this second occurrence too so the two comments agree and neither misleads about which WAL format carries last_batch_lsn.)
📄 src/include/access/xact.h (L415-L415)
SizeOfXactPrepare has no callers anywhere in the tree (a tree-wide search finds only this definition). PostgreSQL's convention is to define a SizeOf.../MinSizeOf... macro only when it is actually used to compute a record size; a bare sizeof(xl_xact_prepare) alias with no user is dead scaffolding. Drop it, or use it where the prepare record size is currently computed (e.g. via sizeof(xl_xact_prepare) in ParsePrepareRecord/twophase.c).
📄 src/backend/access/undo/undoapply.c (L305-L307)
Unlike PerformUndoRecovery() in undo_xlog.c, which PANICs when chain_prev >= batch_lsn to guard against a circular/forward-pointing chain, this abort-path chain walk assigns batch_lsn = chain_prev with no monotonicity check. A corrupt or recycled batch whose chain_prev points forward or equal causes an infinite loop during rollback. Add the same next_lsn < batch_lsn or invalid guard here before following the chain.
Confidence: high.
📄 src/backend/access/undo/undoapply.c (L203-L204)
"ARIES-style rollback semantics" / "ARIES requires" contradicts the README's explicit position that this is CTR and "not ARIES". Reconcile the terminology to avoid reviewer confusion (see also undo_xlog.c PerformUndoRecovery comment).
Confidence: medium.
📄 src/backend/access/undo/undo_xlog.c (L87-L89)
Comment drift: MAX_UNDO_LOGS is defined as 100 in undolog.h, not 64. This stale value also undermines the "acceptable at recovery time" reasoning. Fix the constant reference (or drop the specific number).
Confidence: high.
💡 Suggested change
Before:
* Find the log control structure. O(MAX_UNDO_LOGS) scan:
* with MAX_UNDO_LOGS=64 this is acceptable at recovery
* time (called once per record).
After:
* Find the log control structure. O(MAX_UNDO_LOGS) scan:
* with MAX_UNDO_LOGS this is acceptable at recovery
* time (called once per record).
📄 src/backend/access/undo/undo_xlog.c (L103-L107)
Slot exhaustion is silently ignored: if the second loop finds no free slot, log stays NULL and the allocation is dropped, leaving insert_ptr un-advanced. This leaves UNDO log metadata inconsistent after recovery with no diagnostic. At minimum emit a WARNING/PANIC when no free slot exists rather than silently continuing. (Same silent-no-slot pattern exists in the XLOG_UNDO_ROTATE case below.)
Confidence: high.
📄 src/backend/access/undo/undo_xlog.c (L248-L251)
Buffer overflow in the non-FPI redo fallback. The forward-apply producer (apply_flux_undo_restore_tuple in flux_undo.c) refuses to restore when ItemIdGetLength(lp) < old_len, so a CLR is only emitted for a slot that was large enough at apply time. But this redo path runs against whatever page state the forward redo produced, which may have shrunk the slot (a later in-place update replayed before this CLR). It calls ItemIdSetNormal(lp, ItemIdGetOffset(lp), tuple_len) then memcpy(htup, data, tuple_len) with no check that the current slot storage can hold tuple_len, so memcpy can write past the item into adjacent tuples / the page. Note emit_flux_undo_clr uses REGBUF_STANDARD (not REGBUF_FORCE_IMAGE as the README claims), so BLK_NEEDS_REDO is genuinely reachable. Add a ItemIdGetLength(lp) >= xlrec->tuple_len guard before ItemIdSetNormal/memcpy (matching the forward-apply path's skip-and-warn behavior).
Confidence: high.
📄 src/backend/access/undo/undo_xlog.c (L801-L803)
PerformUndoRecovery() runs in the startup process, where MyDatabaseId is InvalidOid (the startup process is not connected to any database). Storing it as deferred->dboid means the logical revert worker's filter entry_dboid != MyDatabaseId (logical_revert_worker.c) can never match a real per-database worker, so these deferred transactions may never get reverted. UNDO records carry urec_reloid but no dboid, so the correct database cannot be recovered here either. This deferral path is effectively broken for cross-database recovery.
Confidence: high.
📄 src/backend/access/undo/undo_xlog.c (L1021-L1024)
TOCTOU + per-batch syscall: access(path, F_OK) does not eliminate the SIGBUS risk it claims to (the segment can be recycled between this check and XLogReadRecord), and it adds a stat syscall to every batch on the hot rollback path. The underlying read_local_xlog_page/WALRead path already reports a missing segment as a WALReadError (returned via errormsg below), so this check is largely redundant. Consider removing it and relying on the reader's own error path.
Confidence: medium.
📄 src/backend/access/undo/undo_xlog.c (L425-L425)
Non-ASCII characters in source comment ('\u00d7' multiplication sign and '\u2248' approximately-equal). PostgreSQL source must be ASCII-only. Replace with 'x' and '~='.
Confidence: high.
💡 Suggested change
Before:
* 1 million entries × ~40 bytes each ≈ 40 MB, which is reasonable for
After:
* 1 million entries x ~40 bytes each is ~40 MB, which is reasonable for
📄 src/backend/access/undo/undo_xlog.c (L580-L580)
Terminology contradicts the README, which argues at length that this subsystem implements CTR and is explicitly "not ARIES". Calling this "the ARIES-style undo phase" here (and "ARIES requires"/"ARIES-style rollback" in undoapply.c) will confuse -hackers reviewers. Reconcile the wording with the README's CTR framing.
Confidence: medium.
📄 src/backend/access/undo/undo.c (L60-L63)
Declaring the per-backend engine's entry points as local externs instead of including their headers (pbu_undolog.h, pbu_undorequest.h, etc.) defeats compiler type-checking of these signatures. If a definition's signature ever diverges from the local extern, this becomes undefined behavior at link/runtime with no diagnostic. Worse, PendingUndoShmemSize is (faithfully) declared int here but fed to add_size(), which expects Size: its definition computes a Size internally and truncates to int, so a large rollback hash table (hash_estimate_size + queues) can overflow the int and corrupt shmem sizing. Include the real headers so the compiler verifies these, and fix the underlying int return type to Size.
Confidence: high.
📄 src/backend/access/undo/undo.c (L230-L233)
Aspirational/future-tense comment describing behavior that is not wired up in this patch ("is wired into the checkpoint/startup path once a per-backend AM can produce non-empty undo-log state"). The review standard flags comments describing not-yet-shipped behavior. Either wire the CheckPointUndoLogs/StartupUndoLogs path or state plainly that per-backend undo-log crash survival is not implemented here.
Confidence: low.
📄 src/backend/access/undo/undo.c (L169-L170)
YAGNI: DiscardWorkerRegister() and UndoLauncherRegister() unconditionally register two additional always-on background workers for the per-backend UNDO engine, whose own comment admits it has no in-tree producer ("No in-tree access method writes per-backend UNDO yet" per README). Two always-registered workers consume max_worker_processes budget for a path that is not wired to any producer in this changeset. For a first patch to -hackers this is speculative scaffolding that should be deferred until a consumer AM lands.
Confidence: medium.
📄 src/backend/access/undo/xactundo.c (L947-L950)
The inline-abort PG_CATCH does not reset the cached WAL reader. UndoReadBatchFromWAL/UndoValidateBatchLSN cache a process-global static XLogReaderState (undo_batch_reader) with an open segment FD and decode buffer. If ApplyUndoChainFromWAL throws mid-read, the reader is left in an inconsistent state (stale FD / partial decode). Only logical_revert_worker.c calls UndoResetBatchReader() after its catch; this abort path does not, so the next abort in the same backend reuses the corrupt reader. Call UndoResetBatchReader() in this PG_CATCH.
Confidence: medium.
💡 Suggested change
Before:
BufferLockReleaseAll();
LeaveInlineUndoApplyState(saved_trans_state);
FlushErrorState();
undo_applied = false;
After:
BufferLockReleaseAll();
UndoResetBatchReader();
LeaveInlineUndoApplyState(saved_trans_state);
FlushErrorState();
undo_applied = false;
|
|
||
| -- Force discard of UNDO records older than the retention horizon | ||
| -- (normally handled automatically by the UNDO worker) | ||
| SELECT pg_undo_force_discard(); |
There was a problem hiding this comment.
pg_undo_force_discard is registered in pg_proc.dat with signature pg_undo_force_discard(bool) (proargtypes => 'bool'), and there is no zero-argument overload or SQL default. This call with no arguments will fail at runtime with ERROR: function pg_undo_force_discard() does not exist. Pass the required force_rotate boolean, e.g. SELECT pg_undo_force_discard(false);.
Also note the function requires pg_maintain role membership (see undostats.c), so it will error for unprivileged users; the surrounding comment should mention this.
| SELECT pg_undo_force_discard(); | |
| +-- Requires membership in the pg_maintain role. Pass force_rotate=true to also | |
| +-- seal and rotate the active log segment before discarding. | |
| +SELECT pg_undo_force_discard(false); |
| -- STEP 5: Commit the transaction | ||
| COMMIT; |
There was a problem hiding this comment.
This script has no matching BEGIN;, so every preceding statement runs in autocommit mode. The COMMIT; here executes with no open transaction and emits WARNING: there is no transaction in progress. The STEP-5 comment claiming it commits "the transaction" is also misleading. Either wrap steps 2-4 in an explicit BEGIN; ... COMMIT; block, or drop this COMMIT.
| (github.event_name == 'issue_comment' && github.event.issue.pull_request && | ||
| (startsWith(github.event.comment.body, '/open-code-review') || | ||
| startsWith(github.event.comment.body, '@open-code-review') || | ||
| startsWith(github.event.comment.body, '/pg-history'))) |
There was a problem hiding this comment.
The issue_comment trigger gates only on the comment body prefix, with no commenter-authorization guard (e.g. github.event.comment.author_association in ('OWNER','MEMBER','COLLABORATOR')). issue_comment events always run in the base repo's context with its secrets/variables and token, so any user who can comment on a PR (including from an untrusted fork) can start a run that assumes the write-capable AWS OIDC role passed to the reusable workflow. There is no author_association check anywhere in .github/ in this repo, and the reusable ocr-review.yml is external (gburd/ci-workflows), so whether it checks out/executes PR-head code under the assumed role cannot be verified here. Add an authorization gate on the issue_comment path, and confirm the reusable workflow never executes PR-head code with these privileges. [high confidence]
|
|
||
| jobs: | ||
| check: | ||
| uses: gburd/ci-workflows/.github/workflows/ocr-model-check.yml@v1 |
There was a problem hiding this comment.
The reusable workflow is pinned to the mutable tag @v1. A moved/hijacked v1 tag in gburd/ci-workflows would execute arbitrary logic here with the configured AWS OIDC role. Pin to an immutable commit SHA. [moderate confidence]
| (startsWith(github.event.comment.body, '/open-code-review') || | ||
| startsWith(github.event.comment.body, '@open-code-review') || | ||
| startsWith(github.event.comment.body, '/pg-history'))) | ||
| uses: gburd/ci-workflows/.github/workflows/ocr-review.yml@v1 |
There was a problem hiding this comment.
The reusable workflow is pinned to a mutable tag @v1. If the v1 tag in gburd/ci-workflows is moved or the account is compromised, arbitrary logic runs here with the configured AWS OIDC role and repo token. For a privileged flow, pin to an immutable commit SHA (gburd/ci-workflows/.github/workflows/ocr-review.yml@<sha>). [moderate confidence]
| if (part->full) | ||
| return true; |
There was a problem hiding this comment.
The sticky full flag is written under the spinlock but read lock-free here and in FluxDirtyMapCheck. A plain bool is not guaranteed to be atomically/coherently readable across CPUs under PostgreSQL's portability rules (atomics/barriers only via port/atomics). A torn/stale read is benign in the false direction (extra probe), but relying on plain-bool cross-backend visibility violates the atomics-only discipline. Use pg_atomic_flag / pg_atomic_uint32, or add an explicit comment justifying that a stale-false read is always safe and that bool loads are single-copy-atomic on all target arches.
[moderate confidence]
| #define FLUX_DIRTYMAP_KEY(relid, blkno) \ | ||
| (((uint64) (relid) << 32) | (uint64) (blkno)) |
There was a problem hiding this comment.
The EMPTY_KEY==0 sentinel collides with the composed key when relid==0 and blkno==0 (FLUX_DIRTYMAP_KEY(0,0)==0). The comment asserts relid 0 is never a user relation; that holds for the current callers (all pass RelationGetRelid of a real relation). But this is a latent footgun: any future caller passing relid 0 with block 0 would make Mark a no-op and Check report the page clean, silently skipping the probe. Consider Assert(relid != InvalidOid) in Mark/Check to fail loudly rather than corrupt silently.
[moderate confidence]
| void | ||
| FluxDirtyMapShmemInit(void) | ||
| { | ||
| /* Initialization is handled by FluxDirtyMapShmemCallbacks */ | ||
| } |
There was a problem hiding this comment.
FluxDirtyMapShmemInit() is an empty no-op; the real init happens in FluxDirtyMapShmemInit_cb via the registered ShmemCallbacks (PG_SHMEM_SUBSYSTEM in subsystemlist.h). The function is still declared in both flux.h and flux_dirtymap.h but appears to have no caller. If nothing calls it, drop it (and its declarations) as dead scaffolding; if it is a required part of an external/subsystem contract, document why it must exist as a stub.
[moderate confidence]
| #define FLUX_DIRTYMAP_PARTITIONS 128 | ||
| #define FLUX_DIRTYMAP_PART_SLOTS 8192 /* power of two */ |
There was a problem hiding this comment.
The map is fixed at 8 MB (128 x 8192 x 8 bytes) and grow-only with no reset path. Once a partition latches full, every Check on it permanently returns dirty for the life of the server, degrading the fast path to always-probe. With ~786K distinct in-place-modified pages the map latches full and never recovers (no GUC to size it, no reclamation on VACUUM/DROP). For a long-running write-heavy server this is a permanent, unbounded performance cliff. The header calls reclamation "a future optimization"; at minimum this sizing should be a documented GUC, and the permanent-degradation tradeoff called out for reviewers.
[moderate confidence]
| void | ||
| FluxVacuumFSM(Relation rel, BlockNumber new_nblocks) | ||
| { | ||
| BlockNumber old_nblocks = RelationGetNumberOfBlocks(rel); | ||
|
|
||
| if (new_nblocks < old_nblocks) | ||
| { | ||
| /* Truncated - update FSM */ | ||
| FreeSpaceMapPrepareTruncateRel(rel, new_nblocks); | ||
| } | ||
| } |
There was a problem hiding this comment.
FluxVacuumFSM() re-reads the CURRENT block count for old_nblocks. Its sole caller (flux_operations.c:7560) invokes it after FluxTruncateRelation() -> RelationTruncate() has already physically truncated the relation, and passes RelationGetNumberOfBlocks(onerel) as new_nblocks. So old_nblocks == new_nblocks always, the < test is never true, and FreeSpaceMapPrepareTruncateRel() is never reached -- this whole function is effectively dead code on the truncation path.
That is harmless only because RelationTruncate() already performs full FSM truncation itself (storage.c: it calls FreeSpaceMapPrepareTruncateRel(), truncates the FSM fork via smgrtruncate(), and runs FreeSpaceMapVacuum). So this call adds nothing.
Separately, even if this branch WERE reached, it is incomplete/incorrect: FreeSpaceMapPrepareTruncateRel()'s contract (freespace.c) requires the caller to ALSO smgrtruncate() the FSM pages and call FreeSpaceMapVacuumRange() to fix upper-level nodes. This function does neither, so it would leave the FSM fork un-truncated and upper nodes stale, potentially handing out out-of-range blocks. Recommend removing FluxVacuumFSM() and its call entirely, and relying on RelationTruncate().
There was a problem hiding this comment.
🔍 OCR found 245 issue(s).
- 25 inline, 220 in summary (inline capped at 25)
⚠️ 1 warning(s) during review
📄 src/backend/access/flux/flux_handler.c (L1471-L1473)
flux_index_entry_key_mismatch() calls BuildIndexInfo() + CreateExecutorState() + FreeExecutorState() on every tuple returned by a delete-marking index scan (it is invoked per-tuple from flux_index_getnext_slot). BuildIndexInfo is a catalog-heavy operation that reconstructs the whole IndexInfo (expression/predicate trees, key attr metadata); doing it plus a full executor-state create/destroy per row is a large per-tuple cost on an index-scan hot path. IndexScanFluxData (flux.h) has no field to cache this, so nothing is reused across the scan. Build the IndexInfo/EState once in flux_index_scan_begin (store them in IndexScanFluxData) and reset only the per-tuple memory context per row, freeing in flux_index_scan_end.
📄 src/backend/access/flux/flux_handler.c (L1614-L1619)
This header comment is contradicted by the AM's actual behavior and by other comments in this same file. It claims FLUX does OUT-OF-PLACE updates for any key-changing UPDATE with "no in-place index staleness to recheck". But flux_methods sets .am_index_delete_marking = true, and flux_index_getnext_slot performs a delete-marking key recheck (flux_index_entry_key_mismatch) precisely because a key-changing UPDATE stays IN PLACE (see the Phase 8c block at the TableAmRoutine: "An indexed-column UPDATE stays IN PLACE ... delete-marks the old (k_old,TID) entry"). One of the two designs must be stale. Fix this comment (and the equally contradictory "key-changing UPDATE is heap-like (new TID ...)" comment just below the .am_index_delete_marking = true line) so the file describes a single, actual behavior.
📄 src/backend/access/flux/flux_handler.c (L3757-L3757)
Comment references an internal triage document (flux_default_triage.md) that does not exist in the tree. Such references — along with the "Phase 8b/8c invariant I2" phase-tracking labels used throughout this file — are internal development artifacts unsuitable for an upstream patch and will be meaningless to reviewers/future maintainers. Remove or rewrite these references to describe the behavior directly.
📄 src/backend/access/flux/flux_handler.c (L1012-L1015)
TID-range scanning re-reads and re-locks the same block on every returned tuple: each call does ReadBuffer + LockBuffer(SHARE) + PageGetMaxOffsetNumber, returns after the first visible tuple (releasing the buffer at the bottom of the offnum loop), then the next call restarts at scan->rs_cblock/scan->rs_coffset and re-reads the same page. For a dense page this is O(tuples) buffer pin/lock cycles versus the page-mode rs_vistuples[] approach used by flux_scan_getnextslot. Consider reusing the page-mode visible-offset collection here so a page is locked/scanned once, matching the regular scan path.
📄 src/backend/access/flux/flux_handler.c (L4837-L4837)
Two issues in this comment: (1) ~9%% contains a stray doubled percent sign — this is prose in a C comment, not a printf format string, so it should read ~9%. (This doubled-%% also appears in the fill-factor comment ~10%%/100%%.) (2) It states a specific performance number ("~9% of CPU under high-concurrency pgbench") for code that no longer exists in this patch, with no reproducible benchmark reference. Per project conventions, drop the unsubstantiated figure and just state that allvisfrac is derived from relallvisible to mirror table_block_relation_estimate_size and avoid a per-plan VM scan.
📄 src/backend/access/flux/flux_lock.c (L123-L126)
PREPARE leaks the serialize-lock tracking set into the next transaction (high confidence). The only consumers of flux_serlocks are the COMMIT (FluxReleaseSerializeLocks) and ABORT (FluxForgetSerializeLocks) callbacks; FluxXactCallback in flux_mvcc.c does nothing with these locks on XACT_EVENT_PREPARE. At PREPARE, AtPrepare_Locks/PostPrepare_Locks transfer the transaction-level LOCKTAG_TUPLE ExclusiveLock to the prepared xact's dummy PGPROC and remove it from this backend's locallock table. But the static flux_serlocks array is never reset, so when this backend runs its next transaction and reaches COMMIT, FluxReleaseSerializeLocks iterates the stale entries. If that next transaction re-locks the same (dbid, rel, blk, off) row, the surviving stale entry makes FluxTrackSerializeLock dedup it away, and the release loop then drains the new transaction's lock plus can misfire against the prepared xact's tag. At minimum the tracking set must be forgotten at PREPARE (the prepared xact owns the lock and its own 2PC machinery must release it). Add FluxForgetSerializeLocks() to the PREPARE branch of FluxXactCallback, or handle prepared-xact serialize-lock release explicitly.
📄 src/backend/access/flux/flux_mvcc.c (L760-L768)
Dead scaffolding: serializable_horizon (and the mvcc_lock that exclusively guards it) is write-only. It is initialized to 1, only ever driven downward via Min() here, and no code anywhere reads it (verified: the only references are these writes plus the two init sites). Because it starts at 1 and can only decrease, it is permanently pinned at 1 (or the first tiny commit_ts) and conveys no information even if something did read it. This LWLock acquire/release on every serializable commit is pure overhead. Remove serializable_horizon, mvcc_lock, is_serializable, and this block (YAGNI). Confidence: high.
📄 src/backend/access/flux/flux_mvcc.c (L182-L185)
Dead fields: rc_read_point_cid, rc_read_point_xcc, and is_read_only are written in FluxInitTransactionState() but never read anywhere (verified across the tree). The comment above them describes an RC per-command read-point mechanism ("Captured once per command ... reused for every tuple") that is not implemented -- visibility is decided entirely by FluxTupleSatisfiesMVCC via CLOG. This is aspirational scaffolding for a code path that isn't wired. Remove the unused fields and the stale comment. Confidence: high.
📄 src/backend/access/flux/flux_mvcc.c (L278-L284)
Dead code / duplicated init logic. FluxMvccShmemInit() is never called: the wired init path is FluxMvccShmemInit_cb() registered via PG_SHMEM_SUBSYSTEM(FluxMvccShmemCallbacks) in subsystemlist.h. This function and its FluxMvccShmemSize()/ShmemInitStruct path only survive as declarations in flux.h. Keeping two byte-for-byte copies of the same field initialization is a drift hazard (a future field added to one copy but not the other silently mis-initializes shared memory under whichever path is actually used). Remove FluxMvccShmemInit() (and its prototype) since the subsystem callback is authoritative. Confidence: high.
📄 src/backend/access/flux/flux_handler.c (L3173-L3177)
Tuple-lock leak and empty-slot bug on the retry path. flux_tuple_lock uses goto reacquire after FluxLockTuple() sets have_tuple_lock = true (the in-progress-writer/delete/lock-conflict wait loops). On a retried pass, if the item pointer is no longer normal (concurrent prune/defrag), this branch returns TM_Invisible directly and bypasses the out_unlock label, so the acquired heavyweight LOCKTAG_TUPLE lock is never released via FluxUnlockTuple() (it lingers until end-of-transaction, spuriously blocking concurrent lockers of this tuple). It also leaves slot empty, which is exactly the bug the shared out_unlock block was added to fix: the executor's ON CONFLICT path reads xmin off this slot for TM_Invisible and raises the wrong error on an empty slot. Route this exit through out_unlock instead of returning directly.
💡 Suggested change
Before:
if (!ItemIdIsNormal(itemid))
{
UnlockReleaseBuffer(buf);
return TM_Invisible;
}
After:
if (!ItemIdIsNormal(itemid))
{
result = TM_Invisible;
goto out_unlock;
}
📄 src/backend/access/flux/flux_handler.c (L3198-L3209)
Same tuple-lock leak on the TM_SelfModified retry path. If a prior pass took the heavyweight tuple lock (have_tuple_lock == true) via one of the wait loops and jumped back to reacquire, and this pass now sees the tuple as self-modified, the direct return TM_SelfModified bypasses the out_unlock label's if (have_tuple_lock) FluxUnlockTuple(...) release. The LOCKTAG_TUPLE lock then lingers until transaction end, blocking concurrent lockers of this tuple. Return via out_unlock (setting result = TM_SelfModified) so the tuple lock is released.
💡 Suggested change
Before:
if (FluxCheckSelfModified(relation, tid, tuple_hdr, cid, &self_cmax))
{
if (tmfd)
{
tmfd->ctid = *tid;
tmfd->xmax = InvalidTransactionId;
tmfd->cmax = self_cmax;
tmfd->traversed = false;
}
UnlockReleaseBuffer(buf);
return TM_SelfModified;
}
After:
if (FluxCheckSelfModified(relation, tid, tuple_hdr, cid, &self_cmax))
{
if (tmfd)
{
tmfd->ctid = *tid;
tmfd->xmax = InvalidTransactionId;
tmfd->cmax = self_cmax;
tmfd->traversed = false;
}
result = TM_SelfModified;
goto out_unlock;
}
📄 src/backend/access/flux/flux_handler.c (L3880-L3881)
flux_index_delete_tuples always returns InvalidTransactionId as the snapshotConflictHorizon, even though it marks entries deletable. The index AM (e.g. nbtree _bt_delitems_delete) stamps this value into the deletion WAL record; on a hot standby it drives ResolveRecoveryConflictWithSnapshot(). With InvalidTransactionId no recovery conflict is generated, so a standby query holding an older snapshot can be served after the index entries for still-visible-to-it tuples were physically removed on replay, yielding wrong/lost-row results. heap_index_delete_tuples computes a real horizon from the removed tuples' xmax. FLUX should return the max deleter xid (t_xmax) of the entries it marked deletable, not Invalid. (moderate confidence)
📄 src/backend/access/flux/flux_mvcc.c (L788-L794)
Dead code: FluxGetSnapshotTimestamp(), FluxTupleVisibleToSnapshot(), FluxGetMvccStats(), and FluxUpdateOldestActiveTimestamp() have no callers anywhere in the tree (verified) -- they exist only as definitions here plus their extern declarations in flux.h. FluxGetSnapshotTimestamp in particular is a remnant of the removed timestamp-based visibility model (the file header states visibility now comes from CLOG, not timestamps), and its RC branch even advances the wall clock via FluxGetCommitTimestamp(). Per the minimalism discipline, remove these unused functions and their declarations rather than shipping speculative scaffolding.
📄 src/backend/access/flux/flux_mvcc.c (L896-L901)
TOCTOU race in the cache-validation logic. current_gen is sampled (line 874) before the slot scan (lines 885-891). If another backend registers a transaction with an older start timestamp and bumps oldest_active_generation during this scan, that new slot may be missed, yet my_oldest_active_gen is stored as the pre-scan current_gen (line 901). A subsequent call then hits the fast path (current_gen == my_oldest_active_gen) and returns the stale, too-new cached value even though an older active transaction exists. This is tolerable only because the value drives cosmetic page commit-ts and the non-strict VM all-visible hint (documented as non-decision), but the caching is not safe if any correctness gate ever consumes it. Re-read the generation counter after the scan and retry if it changed. (moderate confidence)
📄 src/backend/access/flux/flux_mvcc.c (L718-L729)
SSI conflict-out can be missed for concurrent writers that already COMMITTED, weakening the SERIALIZABLE guarantee this file claims ("Full SSI"). flux_header_dirty_xid() only returns a writer XID that is still TransactionIdIsInProgress(); for a writer that committed a DELETE/UPDATE (making the tuple invisible to our snapshot) it returns InvalidTransactionId, so this function returns early at the !TransactionIdIsValid(xid) branch and never calls CheckForSerializableConflictOut(). Heap's HeapCheckForSerializableConflictOut() instead derives the xmin/update-xid unconditionally (via HeapTupleSatisfiesVacuum) and reports it to predicate.c for any xid >= TransactionXmin, including recently-committed writers -- that is exactly the rw-antidependency (reader reads a version a concurrent transaction overwrote-and-committed). The current code drops that edge, so write-skew anomalies heap rejects can be silently accepted when the conflicting writer commits before the reader probes the tuple. The comment claiming Invalid means "committed so long ago it's no longer tracked / heap's HEAPTUPLE_DEAD case" is wrong: it also fires for writers still well above TransactionXmin. Derive the writer xid from t_xmin (insert/live) or t_xmax (deleted/updated) directly and pass it to CheckForSerializableConflictOut whenever it is >= TransactionXmin, regardless of in-progress state. (moderate confidence)
📄 src/backend/access/flux/flux_operations.c (L5273-L5278)
Tuple-lock leak: this out-of-place fallback returns without calling FLUX_RELEASE_TUPLOCK(). have_tuple_lock can be true here (set true by the DELETED/visibility/write-conflict wait branches that fall through to retry_fit and reach Strategy 3). flux_update_out_of_place takes its own locks but never drops the outer heavyweight LockTupleNoKeyExclusive on otid, so the lock leaks past commit and trips the LockReleaseAll held-at-commit assertion. Release before returning.
💡 Suggested change
Before:
if (did_inplace_key_update != NULL)
*did_inplace_key_update = false;
return flux_update_out_of_place(relation, otid, slot, cid,
options, snapshot,
crosscheck, wait, tmfd,
lockmode, update_indexes);
After:
if (did_inplace_key_update != NULL)
*did_inplace_key_update = false;
FLUX_RELEASE_TUPLOCK();
return flux_update_out_of_place(relation, otid, slot, cid,
options, snapshot,
crosscheck, wait, tmfd,
lockmode, update_indexes);
📄 src/backend/access/flux/flux_operations.c (L5292-L5298)
Same tuple-lock leak as the sibling fallback above: this out-of-place return skips FLUX_RELEASE_TUPLOCK() while have_tuple_lock may be true, leaking the heavyweight LockTupleNoKeyExclusive on otid past commit. Release it before returning.
💡 Suggested change
Before:
/* BUG 2 fallback (see above): out-of-place instead of error. */
if (did_inplace_key_update != NULL)
*did_inplace_key_update = false;
return flux_update_out_of_place(relation, otid, slot, cid,
options, snapshot, crosscheck,
wait, tmfd, lockmode,
update_indexes);
After:
/* BUG 2 fallback (see above): out-of-place instead of error. */
if (did_inplace_key_update != NULL)
*did_inplace_key_update = false;
FLUX_RELEASE_TUPLOCK();
return flux_update_out_of_place(relation, otid, slot, cid,
options, snapshot, crosscheck,
wait, tmfd, lockmode,
update_indexes);
📄 src/backend/access/flux/flux_operations.c (L7751-L7756)
Crash-consistency hole: this sets durable visibility state on a WAL-logged relation (sets FLUX_TUPLE_DELETED, clears FLUX_TUPLE_UNCOMMITTED) via MarkBufferDirty with no XLogInsert, then removes the sLog entry. On a WAL-logged relation, a crash after the sLog removal but before this dirty page is flushed loses the DELETED transition; redo has no record of it, so the aborted INSERT's tuple can be resurrected as visible. The DELETED flag drives post-crash visibility and must be WAL-logged (or the transition must be reconstructable from CLOG during redo).
📄 src/backend/access/flux/flux_operations.c (L7492-L7500)
VACUUM all-visible/all-frozen predicate is incomplete for heap-shaped xmax/lock semantics. It checks DELETED/SPECULATIVE/UNCOMMITTED and t_xmin, but ignores FLUX_TUPLE_LOCKED and t_xmax. A live, xmin-committed tuple can still carry an in-progress FLUX_TUPLE_LOCKED (SELECT FOR UPDATE/SHARE) with a valid t_xmax; marking it all-frozen lets index-only scans skip the heap fetch and future VACUUMs skip the page, which can yield wrong results. heap's all-frozen determination excludes tuples with any live xmax (including lock-only). Add the LOCKED/t_xmax check.
📄 src/backend/access/flux/flux_operations.c (L3794-L3796)
CAS fast-path torn-read hazard. This memcpy overwrites the on-page tuple header (flags, t_xmin, etc.) while holding only BUFFER_LOCK_SHARE_EXCLUSIVE, which permits concurrent plain-SHARE readers (bufmgr: SHARE_EXCLUSIVE conflicts only with EXCLUSIVE and itself). FluxTupleSatisfiesMVCC reads t_flags/t_xmin/t_xmax directly without any barrier, so a concurrent SHARE reader can observe a partially written header (e.g. new bytes written but FLUX_TUPLE_UNCOMMITTED/xmin not yet consistent). The comment's argument that UNCOMMITTED+in-progress-xmin hides the row assumes atomic visibility of those fields, but memcpy provides no such ordering. This is a design-level correctness concern for the share-exclusive in-place overwrite.
📄 src/backend/access/flux/flux_operations.c (L67-L67)
Dead code: MAX_OVERFLOW_PTRS_PER_TUPLE is defined but never referenced anywhere in the tree. Remove it (YAGNI / minimal diff).
📄 src/backend/access/flux/flux_pbu.c (L84-L88)
[high confidence] This process-global flux_pbu_prepared/flux_pbu_uur state has no error-path reset, so an ERROR thrown between FluxPbuPrepareInsert() and the matching FluxPbuFinishUndo()/FluxPbuCancelUndo() leaves it corrupted for the rest of the backend's life. This is reachable today: in the in-place UPDATE path (flux_operations.c ~5603/5610) FluxPbuPrepareInsert() runs, then FluxXLogPrepareLogicalImage() (~5638/5640) can ereport on OOM; its PG_CATCH (~5642) only releases overflow buffers and re-throws -- it does NOT call FluxPbuCancelUndo(). After such a throw flux_pbu_prepared stays true and the uur_payload StringInfo leaks; the next FLUX DML then either trips Assert(!flux_pbu_prepared) (assert build) or silently clobbers the static and re-initStringInfo()s over the leaked buffer (production build). Either add FluxPbuCancelUndo() to the UPDATE PG_CATCH, or register a transaction/subtransaction abort callback that resets this state and releases the pinned undo buffers.
📄 src/backend/access/flux/flux_pbu.c (L303-L307)
[high confidence] FluxPbuCancelUndo() is dead code: it has no callers anywhere in the FLUX tree. The header comment (line 40) and this function's comment claim it is used for "a same-size CAS update that finds no byte actually changed", but that CAS no-op path (flux_operations.c ~3680-3705) returns BEFORE any FluxPbuPrepareInsert() and explicitly notes "nothing to cancel". Per the minimalism/YAGNI discipline, either wire this into the error/abort cleanup path (where it is actually needed -- see the static-state reset issue above) or remove it along with its misleading comment.
📄 src/backend/access/flux/flux_pvs.c (L229-L230)
Portability defect (hard gate): UndoRecPtr is uint64 (access/undodefs.h), so formatting it with %llu and casting to unsigned long long is not portable and violates the tree's rule that int64 values must use UINT64_FORMAT / UndoRecPtrFormat. Existing FLUX code already uses UINT64_FORMAT (flux_pbu.c) and the undo subsystem defines UndoRecPtrFormat. This same defect repeats in every WARNING/DEBUG2 that prints verptr in this file. Replace the %llu + (unsigned long long) cast with UINT64_FORMAT (or UndoRecPtrFormat) and pass verptr directly.
💡 Suggested change
Before:
"FLUX PVS: FLUX_UNDO_UPDATE payload too short at %llu",
(unsigned long long) verptr);
After:
"FLUX PVS: FLUX_UNDO_UPDATE payload too short at " UINT64_FORMAT,
verptr);
📄 src/backend/access/flux/flux_pvs.c (L6-L11)
Comment/code drift: this header describes verptr as a "trailing UndoRecPtr (verptr) into the new on-page image" and repeatedly refers to the "trailing verptr". That is the old on-disk layout. Per flux.h, the version-chain head now lives in the fixed header field FluxTupleHeader.t_verptr at offset 8 (the accessor FluxTupleGetVersionPtr ignores item_len and returns hdr->t_verptr), and it was "formerly an unaligned 8-byte trailer located by item_len - 8". Update this block to describe the current fixed-header field, otherwise it misleads readers about where the pointer lives.
📄 src/backend/access/flux/flux_pvs.c (L24-L28)
Comment/code drift: the header states "Visibility per step is authoritative on the core MVCC snapshot: XidInMVCCSnapshot(...)" as the sole rule, but the end-of-chain branch below decides inserter visibility with TransactionIdDidCommit(ins_xid) && !XidInMVCCSnapshot(...), and the self-transaction branch additionally uses a curcid/urec_cid comparison. The header should mention the CLOG (TransactionIdDidCommit) and command-id checks so it matches the implemented logic (and FluxTupleSatisfiesMVCC).
📄 src/backend/access/flux/flux_pvs.c (L96-L96)
Inaccurate comment: tid is not unused. It is dereferenced just below in the depth-cap WARNING via ItemPointerGetBlockNumber(tid) / ItemPointerGetOffsetNumber(tid). Both this (void) tid; cast and the "currently unused but reserved for future diagnostics" note in the function header are stale. Drop the (void) tid; and fix the doc to say tid is used for diagnostics. (All current callers pass a valid, non-NULL tid, so no crash today, but the comment is wrong.)
📄 src/backend/access/flux/flux_operations.c (L5008-L5010)
TOAST cleanup is missing on the in-place UPDATE path. flux_toast_tuple() here can externalize wide varlena columns into the relation's TOAST table and set upd_toasted = true, but flux_toast_cleanup(&upd_ttc) is never called on any exit path (unlike the INSERT path, which calls it at the end via flux_toast_cleanup(&ttc)). upd_toasted is set but never read. Consequences: (1) the palloc'd toasting temporaries leak on every UPDATE that toasts -- a hot-path leak; (2) heap's toast_tuple_cleanup() also deletes the superseded external datums on UPDATE, so the old tuple's replaced external TOAST entries are orphaned. This affects both the TM_Ok success return (~line 5886) and the intermediate returns (out-of-place fallbacks, force-shrink re-validation). Add a flux_toast_cleanup(&upd_ttc) on all exits after this call (guarded by upd_toasted), matching the INSERT path.
📄 src/backend/access/flux/flux_operations.c (L3817-L3819)
Atomics-discipline violation: pd_commit_ts_and_flags is declared as a plain uint64 in FluxPageOpaqueData, yet here it is (a) read non-atomically at line 3810 and (b) cast to pg_atomic_uint64 * for pg_atomic_compare_exchange_u64. Mixing a plain load with an atomic CAS on the same address, and casting a non-atomic field to pg_atomic_uint64 *, is not portable: on platforms where 64-bit atomics fall back to a spinlock-backed implementation, pg_atomic_uint64 is not layout-compatible with a bare uint64, so the CAS operates on the wrong bytes. Everywhere else this field is touched with the non-atomic FluxPageSet/GetCommitTs macros (under an exclusive buffer lock). Either declare the field as pg_atomic_uint64 and use pg_atomic_read/init everywhere, or avoid the CAS on the SHARE_EXCLUSIVE path.
📄 src/backend/access/flux/flux_pvs.c (L101-L103)
The chain-walk loop can iterate up to FLUX_PVS_MAX_CHAIN_DEPTH (10000) times, each performing an UndoFetchRecord (buffer/I-O). UndoFetchRecord does not itself call CHECK_FOR_INTERRUPTS, so a long or corrupt version chain makes this read path effectively uninterruptible (a query cannot be cancelled). Add a CHECK_FOR_INTERRUPTS() at the top of the loop, consistent with the tree's rule for long loops on read paths. (moderate confidence)
💡 Suggested change
Before:
for (;;)
{
const FluxTupleHeader *hdr = (const FluxTupleHeader *) candidate;
After:
for (;;)
{
const FluxTupleHeader *hdr = (const FluxTupleHeader *) candidate;
CHECK_FOR_INTERRUPTS();
📄 src/backend/access/flux/flux_pvs.c (L126-L127)
candidate_len is passed to FluxTupleGetVersionPtr (which is ignored) but there is no check that candidate is large enough to be a FluxTupleHeader before dereferencing hdr->t_verptr (offset 8) and, at end-of-chain, hdr->t_xmin (offset 24). On the first iteration onpage_len is a full tuple, but on later iterations candidate is a reconstructed before-image whose length comes from an UNDO payload (phdr.tuple_len / delta->old_len). This code already hardens against corrupt UNDO (depth cap, payload length checks), yet a corrupt record supplying tuple_len/old_len smaller than the fixed header would cause an out-of-bounds read here. Reject candidate_len < sizeof(FluxTupleHeader) before reinterpreting the image. (moderate confidence)
💡 Suggested change
Before:
verptr = FluxTupleGetVersionPtr(hdr, candidate_len);
if (!UndoRecPtrIsValid(verptr))
After:
if (candidate_len < sizeof(FluxTupleHeader))
break; /* image too small to be a valid FLUX tuple */
verptr = FluxTupleGetVersionPtr(hdr, candidate_len);
if (!UndoRecPtrIsValid(verptr))
📄 src/backend/access/flux/flux_slot.c (L859-L865)
Latent dead-pointer / leak inconsistency: the same-buffer fast path frees rslot->tuple but never handles rslot->values_block. tts_flux_clear() (the different-buffer path) frees both tuple and values_block under SHOULDFREE; here values_block is left set while SHOULDFREE is cleared, so if the slot ever reaches this branch holding a materialized values block it leaks (and leaves a dangling values_block pointer). Currently only latent because all callers pass a valid buffer while a values-only slot has buffer == InvalidBuffer, but the asymmetry with tts_flux_clear is a footgun. Free values_block here too.
💡 Suggested change
Before:
/* Same buffer; just free any materialized data */
if (unlikely(TTS_SHOULDFREE(slot)))
{
if (rslot->tuple)
pfree(rslot->tuple);
slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
}
After:
/* Same buffer; just free any materialized data */
if (unlikely(TTS_SHOULDFREE(slot)))
{
if (rslot->tuple)
pfree(rslot->tuple);
if (rslot->values_block)
{
pfree(rslot->values_block);
rslot->values_block = NULL;
}
slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
}
📄 src/include/access/flux.h (L865-L868)
These three symbols are already declared in access/flux_dirtymap.h (lines 56-58). Both headers are included together in flux_handler.c and flux_operations.c, so this is a duplicate declaration and a maintenance hazard: if the two ever drift (different signature), you get a conflicting-declaration compile error. Drop these three lines from flux.h and rely on the #include "access/flux_dirtymap.h" that both consumers already carry.
💡 Suggested change
Before:
/* Dirty block map (lock-free sLog bypass) */
+extern Size FluxDirtyMapShmemSize(void);
+extern void FluxDirtyMapShmemInit(void);
+extern const ShmemCallbacks FluxDirtyMapShmemCallbacks;
After:
/* Dirty block map: see access/flux_dirtymap.h */
📄 src/include/access/flux.h (L16-L18)
A header must never #include "postgres.h"; that is the job of each .c file (which includes it first, or postgres_fe.h for FRONTEND builds). Including it here violates the tree's include discipline and will draw an immediate rejection on -hackers. Remove this line; the .c files that include flux.h already pull in postgres.h.
💡 Suggested change
Before:
+#include "postgres.h"
+
+#include "access/heapam.h"
After:
#include "access/heapam.h"
📄 src/include/access/flux.h (L870-L870)
flux_lazy_uncommitted_clear is a GUC (registered in guc_parameters.dat) and is exposed in a public header, so extensions may read it. On Windows/MSVC an extern global read cross-module fails to link without PGDLLIMPORT. The sibling access/recno.h in this same change correctly declares its GUCs extern PGDLLIMPORT bool ...; this one is an oversight.
💡 Suggested change
Before:
+extern bool flux_lazy_uncommitted_clear;
After:
extern PGDLLIMPORT bool flux_lazy_uncommitted_clear;
📄 src/include/access/flux.h (L377-L379)
FluxIsOverflowPtr classifies any varlena whose payload is >= sizeof(FluxOverflowPtr) and begins with the 32-bit magic 0x52564F50 as an overflow pointer. A user-supplied bytea/text value that happens to start with those 4 bytes and is long enough would be misclassified during deform, causing FLUX to dereference ov_first_block/ov_first_offset as page locations -- a data-corruption/crash footgun. A 32-bit magic in user-controlled column data is not a safe discriminator on its own. Confirm the deform path only treats a datum as an overflow pointer when FLUX itself stored it (e.g. gated by the per-attribute FLUX_INFOMASK_HASOVERFLOW / a stored flag), not by the magic alone.
📄 src/backend/access/flux/flux_stats.c (L50-L52)
Dead code (verified: no callers anywhere in the tree). FluxCollectRelationStats/FluxLogRelationStats are declared in flux.h and defined here but are never invoked from the ANALYZE path or elsewhere. This is speculative scaffolding: an entire extra full-relation sequential scan (share lock per page) plus three DEBUG1 log messages, wired to nothing. Per the minimalism/YAGNI discipline this whole file should be dropped until an actual caller exists; a full extra pass over the relation during ANALYZE would also need justification and a way to disable it. (The %lld+(long long) casts are portable and match the rest of the FLUX code, so that is not the concern here -- the concern is the dead pass itself.)
📄 src/include/access/flux_xlog.h (L16-L18)
A header must never #include "postgres.h". This header is included by the FRONTEND translation unit src/bin/pg_waldump/rmgrdesc.c (which defines FRONTEND and includes postgres.h under that guard); a header re-including postgres.h violates the include discipline and prevents flux_xlog.h from being includable standalone in a clean frontend TU. Remove this line; consumers include postgres.h (or postgres_fe.h) themselves before including this header.
💡 Suggested change
Before:
+#include "postgres.h"
+
+#include "access/xlogreader.h"
After:
#include "access/xlogreader.h"
📄 src/include/access/flux_undo.h (L179-L180)
When force_offset is used, suffixlen = old_len - 1 - force_offset and prefixlen is only capped to <= force_offset; there is no re-validation that prefixlen + suffixlen <= old_len afterward. For the current call sites (force_offset = offsetof(t_verptr) = 8, old_len is a full tuple length >> 40) this cannot underflow, but the guard if (force_offset < old_len) alone does not prevent middle_len = old_len - prefixlen - suffixlen from unsigned-wrapping into a huge memcpy length if a future caller passes force_offset close to old_len. The reconstruct side validates this invariant but compute does not. Add an explicit bound (e.g. if ((Size) prefixlen + suffixlen > old_len) return 0;) before computing middle_len so the API is safe for all inputs, not just today's callers.
💡 Suggested change
Before:
+ middle_len = old_len - prefixlen - suffixlen;
+ total = SizeOfFluxUndoDelta + middle_len;
After:
if ((Size) prefixlen + suffixlen > old_len)
return 0;
middle_len = old_len - prefixlen - suffixlen;
total = SizeOfFluxUndoDelta + middle_len;
📄 src/backend/access/flux/flux_tuple.c (L1125-L1126)
Deform/form asymmetry for varlena (verified against the form path and the two live deform paths). After a varlena, this advances data_ptr = att_align_nominal(data_ptr + attr_len, att->attalign) -- an extra trailing alignment. But FluxFormTuple advances data_ptr += attr_len with no trailing align, and the live deform paths tts_flux_deform (flux_slot.c) and FluxDeformTupleUpTo (this file) also advance by += attr_len with no trailing align. When a varlena is followed by a less-aligned attribute (e.g. a char/bool), this trailing align pushes data_ptr past where the form path wrote the next attribute, so every subsequent attribute is read from the wrong offset. This function (FluxTupleToSlotWithOverflow/FluxTupleToSlot) currently has NO callers, so the bug is not live -- but if it is ever wired up it will silently corrupt reads. Either delete these two unused functions or fix the advance to match the form path (data_ptr += attr_len).
💡 Suggested change
Before:
slot->tts_values[i] = PointerGetDatum(data_ptr);
data_ptr = (char *) att_align_nominal(data_ptr + attr_len, att->attalign);
After:
slot->tts_values[i] = PointerGetDatum(data_ptr);
data_ptr += attr_len;
📄 src/backend/access/flux/flux_tuple.c (L788-L791)
Dead code (verified: no callers). FluxPageUpdateTuple, FluxPageGetLiveTuples (whose own comment states "no current callers"), FluxPageDefragment, and FluxFormTupleUpdate are all defined/declared but never called. FluxPageIndexTupleDelete (below) is reached only from FluxPageUpdateTuple, so it is dead too -- and it is a near-verbatim copy of core PageIndexTupleDelete (bufpage.c), which is a top pgsql-hackers rejection reason. The live UPDATE path uses PageIndexTupleOverwrite instead (flux_operations.c). Drop these unused routines and the copied page primitive rather than shipping dead scaffolding.
📄 src/include/access/flux.h (L270-L272)
Stale comment. This bit's description ("trailing UndoRecPtr ... follows column data") documents the obsolete trailing-pointer layout. The struct comment for t_verptr now states every tuple reserves the version pointer as a fixed aligned header field (born-with-flag) and there is no trailing pointer. The flag is still set on new writes, but the comment no longer matches what the bit means; update it to describe current semantics to avoid misleading future readers.
📄 src/backend/access/flux/flux_vm.c (L72-L79)
Dead code (verified: no callers). FluxVMInit is an empty stub (only a comment). FluxVMExtend, FluxVMTruncate, FluxVMPinBuffer, FluxVMGetPageSize, FluxVMMapHeapToVM, FluxVMUpdateForInsert, FluxVMUpdateForUpdate, FluxVMUpdateForDelete, and FluxVMVacuumPage are all defined/declared but never called (only FluxVMSet/FluxVMClear/FluxVMCheck/FluxVMCheckCached have live callers). flux_vm_extend is only reachable via the unused FluxVMExtend/FluxVMPinBuffer/flux_vm_readbuf. Per YAGNI, remove the unused VM surface; keeping empty stubs and update/extend/truncate helpers that nothing invokes is exactly the speculative scaffolding the community rejects.
📄 src/backend/access/recno/recno_clock.c (L569-L570)
Reading an externally-written /dev/shm region into a fixed C struct with no size or version check is a footgun. The region is opened O_RDONLY and mmap'd for sizeof(ClockBoundData); RecnoReadClockBound() then memcpy's the full struct and validates fields only afterward. If the daemon-written segment is smaller than sizeof(ClockBoundData) this is an out-of-bounds read of foreign shared memory. The layout also depends on struct padding and sizeof(struct timespec), which is not a stable cross-platform/on-disk contract. Before mmap, fstat the fd and verify st_size >= sizeof(ClockBoundData), and add an explicit layout version field.
Confidence: high.
📄 src/backend/access/recno/recno_clock.c (L367-L369)
Portability defect: error_ms is uint64 but the format string uses %lu, which is 32-bit on LLP64 (Windows/MSVC) and on ILP32 platforms, printing wrong values and warning under -Wformat. Use UINT64_FORMAT. Same defect at the 50%% warning site below.
Confidence: high.
💡 Suggested change
Before:
ereport(FATAL,
(errmsg("recno clock error bound %lu ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
After:
ereport(FATAL,
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
📄 src/backend/access/recno/recno_clock.c (L377-L379)
Same %lu-for-uint64 portability defect here (error_ms is uint64). Use UINT64_FORMAT.
Confidence: high.
💡 Suggested change
Before:
ereport(WARNING,
(errmsg("recno clock error bound %lu ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
After:
ereport(WARNING,
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
📄 src/backend/access/recno/recno_clock.c (L358-L360)
Data-availability footgun. This drift check fires a self-shutdown ereport(FATAL) on a coarse heuristic driven by float arithmetic on an int GUC (error_ms > recno_max_clock_offset_ms * 0.8). A transient clock blip can FATAL the entire server because recno_fatal_on_clock_drift defaults to true (see its declaration). A storage AM should not crash a production cluster on a momentary NTP wobble; the default should be conservative (false), or this should demote to WARNING/hold-off rather than FATAL. Confirm the true default is intended and, if kept, document it and the GUC.
Confidence: high.
📄 src/backend/access/recno/recno_clock.c (L205-L206)
Non-standard bgworker signal handling: setting SIGTERM to PG_SIG_DFL makes a graceful shutdown request (fast/smart shutdown, or a config-driven worker stop) kill this worker uncleanly with the default disposition, instead of letting it exit via the normal request path. Combined with bgw_restart_time = 5 the postmaster will then keep restarting it. Use the standard pattern (pqsignal(SIGTERM, die) or SignalHandlerForShutdownRequest) and honor the request in the WaitLatch loop.
Confidence: high.
📄 src/backend/access/recno/recno_compress.c (L450-L461)
Build-configuration-dependent on-disk corruption path. The !USE_LZ4 / !USE_ZSTD stubs store data uncompressed and rely on the ratio check in RecnoCompressAttribute to reject the result -- but the matching stub decompressors (RecnoDecompressLZ4/ZSTD below) blindly memcpy orig_size bytes assuming raw data. A page written by a binary built WITH lz4/zstd carries a RecnoCompressionHeader with comp_type == RECNO_COMP_LZ4/ZSTD and genuinely compressed bytes; if that page is later read by a binary built WITHOUT the library, the stub decompressor returns garbage with no error. The header records comp_type but nothing lets the stub detect it cannot actually decode. This makes the compiled-in compression libraries part of the on-disk format contract, which is a corruption hazard.
Confidence: high.
📄 src/backend/access/recno/recno_compress.c (L957-L962)
Dictionary compression is unsafe and effectively dead. RecnoCompressDictionary()/RecnoDecompressDictionary() store only a 4-byte process-local dict_id on the page, while the dictionary lives in backend-local CacheMemoryContext and is never persisted (as the header admits). If RECNO_COMP_DICTIONARY were ever selected for the write path, another backend -- or the same backend after restart -- would resolve the id against a different or empty dictionary, yielding wrong data or "invalid dictionary ID" errors. Auto-selection (RecnoChooseCompressionType) never returns DICTIONARY, so this is currently unreachable dead code carrying a latent corruption footgun; drop the dictionary codec (and RECNO_DICT_* scaffolding) or gate it behind persisted, WAL-logged dictionaries before it can be selected.
Confidence: high.
📄 src/backend/access/recno/recno_fsm.c (L324-L327)
Dead/ineffective scaffolding on a hot path. RecnoGetFSMState() allocates a fresh RecnoFSMState -- including a new MemoryContext and a defrag_queue -- on every call, and RecnoRecordFreeSpace()/RecnoMarkPageForDefrag() call it per DML operation. The queue therefore never persists (the comment concedes "In practice, this would be cached per relation"), so RecnoScheduleDefrag can only ever trigger the immediate synchronous defrag when the transient queue "fills", and RecnoOpportunisticDefrag drains a queue that is always empty. Net effect: needless AllocSetContextCreate + palloc churn on every insert/delete for a scheduling mechanism that cannot work. Either cache the state per relation (rd_amcache) or remove the queue machinery.
Confidence: high.
📄 src/backend/access/recno/recno_lock.c (L363-L365)
Dead code (YAGNI). RecnoCheckDeadlock() is an exported function that always returns false (never detects a deadlock) and has no caller anywhere in the tree; the two switch arms and (void) casts are pure scaffolding. An always-false public deadlock check is also a footgun if a future caller trusts it. Remove the function and its prototype in recno.h.
Confidence: high.
📄 src/backend/access/recno/recno_lock.c (L429-L433)
Unused helper with an O(n^2) bubble sort. RecnoLockMultipleTuples() has no caller in the tree (only declaration + definition). If ever wired up, the bubble sort over tids is a latent performance issue. Per minimalism, remove it and its prototype until a caller exists; if kept, use qsort with an ItemPointerCompare comparator.
Confidence: high.
📄 src/backend/access/recno/recno_handler.c (L4593-L4595)
Dead code using a non-portable attribute. Both minimal_tuple_from_recno_tuple() (fabricates an all-NULL tuple, comment "prevents crashes") and recno_tuple_from_slot() (below) are unused and marked with the GCC-specific attribute((unused)) spelling, which the tree replaces with pg_attribute_unused() for MSVC portability. Remove both functions (and their forward decls) rather than shipping dead scaffolding; if a genuinely-unused function must stay, use pg_attribute_unused().
Confidence: high.
📄 src/backend/access/recno/recno_handler.c (L3563-L3568)
Bottom-up deletion freespace accumulation is broken. actualfreespace is declared and set to 0 inside the per-entry loop body, then only the current entry's istatus->freespace is added, so it never accumulates across deletable entries. The actualfreespace >= delstate->bottomupfreespace early-stop can therefore only ever succeed if a single entry alone meets the target, defeating the space-target logic that heap_index_delete_tuples relies on. Track a running total across iterations (declared outside the loop).
Confidence: high.
📄 src/backend/access/recno/recno_mvcc.c (L987-L990)
ASCII-only source violation: these loop comments use Unicode box-drawing characters (the double-line dash) which are not permitted in PostgreSQL source. In addition, the pervasive narrative/changelog style throughout this file ("Fix A", "Fix B", "Fix C", "BUG 1", "Caveat-2") reads as a development scratchpad; comments should explain why the code is as it is now, not record the history of fixes. Rewrite to plain ASCII, why-not-what comments before list submission.
Confidence: high.
📄 src/backend/access/recno/recno_clock.c (L138-L144)
Dead compatibility scaffolding introduced in the same patch that adds the subsystem. RecnoClockShmemInit keeps an "old-style ShmemInitStruct path" for callers that run "before the subsystem infrastructure", but the PG_SHMEM_SUBSYSTEM callbacks (RecnoClockShmemCallbacks) are new here and are the only intended entry point -- there is no pre-subsystem caller to be backward-compatible with. Drop the fallback branch and let RecnoClockShmemInit_cb be the single init path.
Confidence: moderate.
📄 src/backend/access/recno/recno_mvcc.c (L500-L503)
2PC lock-lifecycle hazard. recno_tuple_update takes a transaction-duration heavyweight LOCKTAG_TUPLE (NoKeyExclusive) via RecnoLockTuple and tracks it for release in the XACT_EVENT_COMMIT callback (RecnoReleaseSerializeLocks). But the XACT_EVENT_PREPARE arm here calls only RecnoForgetSerializeLocks() -- it drops the tracking set WITHOUT releasing the lock. At PREPARE, the standard tuple-lock lifecycle assumes short-lived tuple locks are already gone; a still-held LOCKTAG_TUPLE is either carried into the prepared-xact lock set (then never released by this backend) or trips the held-at-commit assertion. Because RECNO deliberately holds this lock to commit to conserve lost updates, PREPARE TRANSACTION on an updater is exactly the case that breaks. Verify against twophase.c whether a held LOCKTAG_TUPLE survives PREPARE cleanly; if not, this scheme is unsafe for 2PC and the lock must be released (not just forgotten) here.
Confidence: moderate (depends on 2PC lock-transfer behavior not in this diff).
📄 src/backend/access/recno/recno_operations.c (L2789-L2789)
Fragile on-page length invariant that redo and read-reconstruction both depend on. The in-place "fits within old slot" path memcpy's new_tuple_size bytes over the old header but deliberately leaves ItemIdGetLength unchanged, so for a shrinking update the slot length exceeds the tuple's real t_len and trailing stale bytes remain. Correctness then rests on every consumer using t_len, never the slot length: RecnoReconstructVisibleVersion's header comment explicitly requires being passed t_len (not the padded ItemId length), and the DELTA/inline-diff offsets are t_len-relative. Any consumer that reads ItemIdGetLength (redo image comparison in RecnoXLogUpdate, deform, VACUUM classification) will see the padded length and diverge. This is an easy-to-misuse invariant with no assertion guarding it; add Assert(new_tuple->t_len == new_tuple_size) here and an Assert in RecnoReconstructVisibleVersion that onpage_len == hdr->t_len, and confirm the redo path compares/produces the same padded image the primary leaves.
Confidence: moderate.
📄 src/backend/access/recno/recno_operations.c (L3701-L3704)
Phantom-row hazard from stale index entries. Cross-page defrag inserts NEW index entries for each relocated tuple (index_insert, UNIQUE_CHECK_NO) but leaves the OLD entries pointing at the now-LP_UNUSED source slot, relying on "the next index vacuum pass" to remove them. This runs in Phase IV-B, AFTER this VACUUM's index cleanup (Phase IV) has already completed, so the stale entries persist until a FUTURE VACUUM. In the meantime the freed source TID can be handed out to a new INSERT (RecnoRecordFreeSpace is called on the emptied source page right here), and an index scan following the stale entry would then resolve to an unrelated reused tuple -- a phantom/wrong-row result, and a unique-index scan could see a duplicate. TID reuse must be prevented until the stale entries are gone (e.g. do not release the source slots' space until a subsequent index cleanup, or delete-mark the old entries synchronously here). Verify a relocated source slot cannot be reused before its old index entry is cleaned.
Confidence: moderate.
📄 src/include/access/flux_xlog.h (L402-L407)
This entire FRONTEND-guarded block of WAL struct definitions is dead code. The only FRONTEND consumer of flux_xlog.h is pg_waldump/rmgrdesc.c, which uses just the flux_desc/flux_identify prototypes; the actual flux_desc implementation in rmgrdesc/fluxdesc.c does NOT include flux_xlog.h and instead defines its own xl_flux_*_fe copies. So nothing ever references these frontend structs.
Worse, they have already silently diverged from the backend definitions: this frontend xl_flux_update omits the dst_block_id and pad[3] fields that the backend struct carries, so sizeof(xl_flux_update) differs between the two definitions. If anyone ever wires a frontend parser to these, it will misparse cross-page update records. Per the minimalism/YAGNI discipline, drop this dead block (and ideally have fluxdesc.c share a single FRONTEND-safe definition rather than maintaining a third hand-copy).
📄 src/include/access/flux.h (L562-L563)
rs_snapshot_ts and rs_xact_ts are write-only: they are assigned in flux_handler.c (scan->rs_snapshot_ts = GetCurrentTimestamp(); scan->rs_xact_ts = GetCurrentTimestamp();) but never read anywhere in the FLUX backend. They are leftovers from the removed HLC/timestamp visibility scheme, which the header comments elsewhere state has been replaced by heap-shaped xmin/xmax MVCC + CLOG. Per YAGNI, remove both fields and their assignments.
📄 src/backend/access/flux/flux_xlog.c (L1348-L1350)
sLog key mismatch between the forward path and redo (high confidence). The primary populates the sLog keyed by the pg_class OID: SLogTupleInsert(RelationGetRelid(relation), ...) (flux_handler.c). The read/visibility path also looks up by relid: SLogTupleLookupFiltered(relid, ...) in FluxTupleSatisfiesMVCC, where relid == RelationGetRelid(rel). But this redo path inserts with rlocator.relNumber, i.e. the RelFileNumber (relfilenode), not the relation OID. These are equal only for a relation that has never been rewritten; after VACUUM FULL / CLUSTER / TRUNCATE / a rewriting ALTER TABLE the relfilenode diverges from the OID. On a hot standby (and during crash recovery) the redo-inserted UNCOMMITTED sLog entry is then keyed by the wrong value, so the reader's lookup returns nfound==0 and treats aborted rows as visible until the CLR arrives -- exactly the failure this code was added to prevent. The redo path needs the real relation OID (e.g. via a relfilenode->OID resolution safe for redo) rather than rlocator.relNumber.
📄 src/backend/access/flux/flux_xlog.c (L1662-L1664)
Same sLog key mismatch as the single-INSERT redo path (high confidence): this multi-insert redo keys the recovery sLog entry by rlocator.relNumber (relfilenode) while the forward path and the read-side visibility check key by the pg_class OID. For any FLUX table whose relfilenode differs from its OID (after any rewrite), the standby/crash-recovery entry lands under the wrong key and aborted batch rows become visible until the CLR arrives. Use the relation OID here, not the RelFileNumber.
📄 src/backend/access/flux/flux_slot.c (L468-L474)
Stale comment / design drift (low confidence). This comment states "t_cid removed from FluxTupleHeader" and that the command id "is now obtained from the sLog", but FluxTupleHeader now carries a dedicated t_cmin field (see flux.h) that is authoritative for the writer's command id while the inserter is uncommitted. When the slot holds a physical tuple, rslot->tuple->t_cmin is available directly and the FluxTupleSatisfiesMVCC path already reads it there; the sLog lookup here can return InvalidCommandId (e.g. after eviction) where t_cmin would still be correct. At minimum the comment should be corrected to match the current header layout; consider reading t_cmin from the header for consistency with the visibility path.
📄 src/backend/access/recno/recno_compress.c (L781-L784)
Stack buffer overflow on the read/recovery path. bytes_stored is read from a single untrusted on-disk byte (input[1], range 0-255) and passed directly to memcpy(&abs_val, input + 2, bytes_stored), where abs_val is a fixed 8-byte uint64. A corrupted or crafted page whose 0x80 tag carries a length byte > 8 overflows the stack. The compressor only ever writes 1-8 bytes here, so the decompressor must reject anything larger. Confidence: high.
💡 Suggested change
Before:
int bytes_stored = input[1];
uint64 abs_val = 0;
memcpy(&abs_val, input + 2, bytes_stored);
After:
int bytes_stored = input[1];
uint64 abs_val = 0;
if (bytes_stored > sizeof(abs_val))
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("invalid delta compression length %d", bytes_stored)));
memcpy(&abs_val, input + 2, bytes_stored);
📄 src/backend/access/recno/recno_compress.c (L729-L731)
Silent round-trip corruption for the 0x89 (other numeric type) path. The length is masked to 8 bits (input_size & 0xFF), but memcpy copies the full input_size bytes. On decompress, RecnoDecompressDelta reads back only the low-byte count (stored_size = input[1]) into an output buffer sized VARHDRSZ + orig_size: for any value >= 256 bytes the recovered length wraps and the write is short, leaving the tail of the output buffer uninitialized and the datum corrupted. Store the true length (e.g. a full 4-byte size) or reject inputs that do not fit the encoding. Confidence: high.
📄 src/include/access/recno.h (L24-L26)
Headers must not include "postgres.h". By project convention the .c file includes postgres.h first, and installed headers are expected to be usable from both backend and frontend translation units. Core AM headers (e.g. access/nbtree.h) do not include postgres.h. Drop this include here (and in recno_diff.h, recno_slog.h, recno_xlog.h) and rely on the .c files to include postgres.h first. This is especially important for recno_xlog.h, which guards a #ifndef FRONTEND block implying frontend inclusion; pulling in backend-only postgres.h there is a portability/frontend-build hazard.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/heapam.h"
After:
#include "access/heapam.h"
📄 src/include/access/recno_xlog.h (L18-L20)
Same convention issue as recno.h: a header must not include "postgres.h". recno_xlog.h guards a #ifndef FRONTEND section (recno_decode) implying it may be reached from frontend code; unconditionally pulling in backend-only postgres.h breaks that path. Remove this include and let the including .c file include postgres.h first.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/recno.h"
After:
#include "access/recno.h"
📄 src/include/access/recno_undo.h (L27-L30)
This comment about the UNDO rmid space is stale/inaccurate. It claims "2 and 4 are reserved out-of-core" and "6 = ZHEAP", but in this same changeset UNDO_RMID_FILEOPS=2 is an in-core consumer (storage/fileops.h), and there is no ZHEAP consumer. The canonical numbering note lives in access/undormgr.h; duplicating an out-of-date copy here invites drift. Reduce this to just the fact that RECNO takes id 7, and point at undormgr.h for the authoritative allocation.
💡 Suggested change
Before:
* RECNO's UNDO resource-manager ID. IDs 1 (NBTREE) and 3 (HASH) are core;
* 2 and 4 are reserved out-of-core; 5 = FLUX, 6 = ZHEAP. RECNO takes 7.
*/
#define UNDO_RMID_RECNO 7
After:
* RECNO's UNDO resource-manager ID. The authoritative id allocation is in
* access/undormgr.h; RECNO takes the next free value, 7.
*/
#define UNDO_RMID_RECNO 7
📄 src/include/access/recno_diff.h (L18-L20)
Header must not include "postgres.h"; remove it and rely on the .c file. See recno.h.
💡 Suggested change
Before:
#include "postgres.h"
#define RECNO_MAX_DIFF_SEGMENTS 32
After:
#define RECNO_MAX_DIFF_SEGMENTS 32
📄 src/include/access/recno_slog.h (L19-L21)
Header must not include "postgres.h"; remove it and rely on the .c file. See recno.h.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/xact.h"
After:
#include "access/xact.h"
📄 src/include/access/recno.h (L339-L339)
Footgun: this function-like macro takes rel but never evaluates it, expanding to a constant (true). A caller writing RelationHasUndo(fn_with_side_effects()) would silently drop the argument (fortunately current callers pass a plain variable). If UNDO is now unconditional as the comment states, prefer removing the argument (define RelationHasUndo() (true)) or, better, drop the macro entirely at the call sites so the dead per-table gate is not carried forward. Also note the comment here says the gate "is retired," yet recno_handler.c still describes RelationHasUndo as "the only per-table gate," so the surviving abstraction is misleading either way.
📄 src/backend/access/recno/recno_slot.c (L523-L527)
Read path uses the XID-assigning GetCurrentTransactionId() for xmin/xmax. This forces XID assignment during a SELECT, which PANICs on a hot-standby / read-only transaction ("cannot assign TransactionIds during recovery") and consumes an XID for every read of a RECNO tuple. It is also semantically wrong: xmin should be the tuple's writer, not the reading transaction. At minimum use GetCurrentTransactionIdIfAny() (returning InvalidTransactionId when none), but the value returned is still not the tuple's real xmin -- consider deriving it from t_xmin like the !rslot->tuple branch above does. Any consumer relying on xmin/xmax (EXPLAIN, ctid tooling, RI) will get corrupt results.
📄 src/backend/access/recno/recno_pbu.c (L123-L126)
The prepared/finish protocol relies on the file-scope statics recno_pbu_prepared, recno_pbu_uur, recno_pbu_urp being reset by a matching RecnoPbuFinishUndo()/RecnoPbuCancelUndo(). But RecnoPbuCancelUndo() is never called from any RECNO operation path (verified: only definitions/comments reference it). If any ereport(ERROR) fires between RecnoPbuPrepareInsert() and RecnoPbuFinishUndo() (e.g. the crit-section elog(PANIC) on a failed re-add, or an allocation failure in RecnoXLogEnsureLogicalImageCxt()), recno_pbu_prepared stays true, the pinned undo buffers stay pinned, and uur_payload leaks. The next DML then trips Assert(!recno_pbu_prepared) in RecnoPbuPrepareInsert (or silently clobbers stale state in a non-assert build). Register a transaction/subtransaction abort cleanup hook that calls RecnoPbuCancelUndo() (mirroring how the buffers are reset), or wrap the DML in PG_TRY/PG_FINALLY.
📄 src/backend/access/recno/recno_undo.c (L364-L367)
The bool result of apply_recno_undo_restore_tuple() is ignored here, and RecnoSLogRemove() below the switch runs unconditionally. When restore is skipped (current slot smaller than the before-image -> returns false, only DEBUG1), the aborting xid's RECNO_SLOG_ABORTED marker is still removed. That marker was the only thing hiding the un-restored row from readers, so a rolled-back UPDATE/DELETE whose physical restore was skipped becomes VISIBLE with its wrong (post-update or still-present) contents -- silent data corruption. Capture the return value and only remove the sLog marker when the restore actually succeeded (leaving the marker so VACUUM/readers still treat the row as aborted otherwise).
📄 src/backend/access/recno/recno_overflow.c (L1522-L1524)
This uses XLOG_RECNO_INIT_PAGE to log orphan-record removal, but the redo handler recno_xlog_init_page_redo() replays it with XLogReadBufferForRedoExtended(record, 0, RBM_ZERO_AND_LOCK, ...) + RecnoInitPage(), which ZEROES the entire page. A RECNO data page can hold a mix of live tuples and overflow records, so replaying this on a page that still contains live items destroys them on the standby / after crash -- a data-loss redo bug. Log the actual per-item deletions (as the other overflow paths do) instead of a reinit-page record; RecnoXLogInitPage is only correct for a page that is being fully (re)initialized.
📄 src/backend/access/recno/recno_overflow.c (L951-L952)
RecnoDeleteOverflowChain() mutates shared on-disk state (RecnoPageIndexTupleDelete + MarkBufferDirty + RecnoRecordFreeSpace) with NO WAL record. The comment's "idempotent, cleaned up by VACUUM" rationale does not cover physical page divergence: the DELETE path defers this to VACUUM, and the VACUUM caller (recno_operations.c:4133) also emits no WAL for these mutations. On a standby the overflow record survives while the primary removed it, and FSM accounting differs -- flagged by wal_consistency_checking and visible as replica divergence. Overflow-page mutations must be WAL-logged (or performed under a WAL record that captures the page image).
📄 src/backend/access/recno/recno_overflow.c (L383-L384)
This block-0 special case is incorrect: RECNO has no metapage (relation creation explicitly does not initialize block 0; block 0 is a normal data page holding tuples). So when the FSM legitimately returns block 0 as an overflow target, this "WORKAROUND" discards it and force-extends the relation, permanently wasting a page and defeating FSM reuse. Either drop the special case entirely or gate overflow placement on a real invariant (e.g. the caller-pinned page number), not the magic constant 0. The "WORKAROUND"/"probably" comment also signals this is speculative scaffolding that should not ship.
📄 src/backend/access/recno/recno_slog.c (L527-L529)
This retry loop sleeps (pg_usleep 1ms x3) waiting for concurrent transactions to release sLog slots, then hard-ERRORs if still full. Combined with the earlier RecnoSLogInsert OOM path ("out of shared memory" / "sLog is full"), a bulk INSERT that exceeds MaxBackends * recno_slog_entries_per_backend fails the whole transaction -- a data-load-size-dependent cliff. pg_usleep-based backoff under shared-memory contention also does not scale. This is a footgun for OLTP-plus-bulk workloads; document the hard limit prominently and/or provide the spill path the header comment promises rather than relying on a fixed 3x1ms retry.
📄 src/backend/access/recno/recno_slog.c (L980-L986)
Phase 1 acquires ALL RECNO_SLOG_PARTITIONS locks simultaneously and pre-allocates sizeof(RecnoSLogKey) * RecnoSLogNumEntries() keys (up to MaxBackends262144 bounded, but MaxBackends2048 typically) in one palloc. In the UNDO/revert worker this is a large transient allocation on every call. Consider processing partition-by-partition (collect+remove within each single partition lock) to bound the allocation to one partition's worth and avoid establishing a global all-partitions lock point.
📄 src/backend/access/recno/recno_slot.c (L469-L472)
getsysattr is invoked from generic executor code and here opens the relation (try_relation_open under AccessShareLock) and does a ReadBuffer+lock per call. If this fires per-tuple during a scan (e.g. ExecOnConflictLockRow, ANALYZE, tooling that reads xmin), it is a hot-path relation reopen and buffer lock acquisition -- a serious performance hazard and a potential lock-escalation/deadlock source, and it assumes IsTransactionState(). Confirm this cannot be reached per-tuple; if it can, the xmin must come from data already attached to the slot rather than reopening the relation.
📄 src/backend/access/recno/recno_overflow.c (L502-L508)
On this error the buffers already pushed into overflow_buffers (from prior loop iterations) remain content-LOCKED and their record_data remains palloc'd; nothing here releases them. ResourceOwner cleanup at abort will release the buffer pins/locks, but the palloc'd record_data blocks leak for the transaction lifetime, and the "caller must WAL-log and release" contract is silently broken mid-store. Given the deferred-logging design, an ERROR between the first stored buffer and the caller's XLogInsert leaves the caller with a partially-populated overflow_buffers it never sees. Consider failing the whole store atomically (release everything collected so far) rather than leaving locked buffers for the resource owner.
📄 src/backend/access/recno/recno_vm.c (L72-L79)
RecnoVMInit() is an empty function whose only content is a comment describing what it does NOT do. This is dead scaffolding (YAGNI). If it has no meaningful callers, remove it; if a caller expects a no-op today, at least document why the empty stub must exist rather than leaving a function that reads as unfinished.
📄 src/backend/access/recno/recno_xlog.c (L208-L214)
RecnoReplicaHandleUncertainty() busy-waits with pg_usleep in 10ms steps, and recno_redo_handle_hlc() calls it from the INSERT/UPDATE/DELETE redo handlers. When recno_uncertainty_wait is on, this blocks WAL replay in the startup/recovery process for up to the uncertainty window per record, stalling hot-standby apply and delaying failover. A spin-wait in the redo path is a poor pattern; at minimum this must not run in the recovery process, and the wait should use WaitLatch rather than pg_usleep so it responds to shutdown/interrupts promptly.
📄 src/backend/access/recno/recno_stats.c (L247-L252)
Project convention is INT64_FORMAT / UINT64_FORMAT for int64/uint64 in format strings, not %lld/%llu with (long long) casts. While the casts are portable, they diverge from the surrounding tree style. Prefer INT64_FORMAT here (and UINT64_FORMAT for the HLC values below).
📄 src/include/access/recno_xlog.h (L63-L69)
WAL determinism/portability: this struct has an implicit 4-byte padding hole between flags (offset 4) and commit_ts (8-byte aligned, offset 8). RecnoXLogInsert() fills only offnum/flags/commit_ts on a stack xl_recno_insert and then does XLogRegisterData(&xlrec, sizeof(xl_recno_insert)) (recno_xlog.c ~L458), so the uninitialized padding bytes are written into WAL. That defeats wal_consistency_checking masking and produces non-reproducible WAL. Note the sibling xl_flux_insert avoids this by carrying a uint32 tuple_len that fills the hole. Either reorder/add an explicit zeroed pad field here, or MemSet the record before filling it. High confidence.
📄 src/include/access/recno_xlog.h (L89-L94)
Same uninitialized-padding-in-WAL problem as xl_recno_insert: a 4-byte hole between flags and commit_ts. RecnoXLogDelete registers sizeof(xl_recno_delete) after setting only offnum/flags/commit_ts. Add an explicit zeroed pad field or MemSet before filling.
📄 src/include/access/recno_xlog.h (L107-L112)
Uninitialized WAL padding: uint16 nunused leaves a 6-byte hole before the 8-byte-aligned commit_ts. RecnoXLogDefrag() sets only nunused/commit_ts and registers sizeof(xl_recno_defrag) (recno_xlog.c ~L886-890), so 6 uninitialized bytes reach WAL. Reorder (put commit_ts first) or add an explicit zeroed pad, or MemSet the record.
📄 src/include/access/recno_xlog.h (L71-L75)
Uninitialized WAL padding: flags (offset 4) then a 4-byte hole before old_commit_ts (offset 8). Same emit pattern writes the hole into WAL. Reorder to place a uint32 field (e.g. old_tuple_len) after flags, or MemSet before filling.
📄 src/include/access/recno_xlog.h (L165-L171)
Uninitialized WAL padding: comp_level (offset 5) is followed by a 2-byte hole before the 4-byte-aligned orig_size (offset 8). The compress record is registered with sizeof(xl_recno_compress) after selective field assignment, so the hole reaches WAL. Add an explicit zeroed pad (e.g. uint16 pad;) after comp_level, or MemSet the record.
📄 src/include/access/recno_xlog.h (L177-L181)
Uninitialized WAL padding: after flags (offset 4) a 4-byte hole precedes the 8-byte-aligned commit_ts. Reorder (commit_ts first) or add an explicit zeroed pad, or MemSet before filling, before this record is registered with sizeof().
📄 src/include/access/recno_xlog.h (L183-L191)
Uninitialized WAL padding: fields sum to 12 bytes (dst_block+src_offnum+dst_offnum+tuple_len), leaving a 4-byte hole before the 8-byte-aligned commit_ts. Reorder or add an explicit zeroed pad, or MemSet, before registering with sizeof().
📄 src/include/access/recno_xlog.h (L201-L205)
Trailing WAL padding: these structs are 5 bytes of data (BlockNumber + uint8) but sizeof rounds up to 8, so 3 uninitialized trailing bytes are written to WAL when registered with sizeof(). Same applies to xl_recno_lock (6 bytes -> 8). Add an explicit zeroed pad field, or MemSet the record before filling.
📄 src/backend/access/recno/recno_tuple.c (L930-L935)
Memory leak: values and isnull are palloc'd but never freed before returning. RecnoFormTuple copies the data it needs, so both arrays are leaked into the caller's MemoryContext on every call. Compare with RecnoComputeSlotSize() just below, which correctly pfrees both. (This function also currently has no callers - if it is genuinely unused, prefer deleting it per YAGNI rather than shipping a leaking helper.)
💡 Suggested change
Before:
slot_getallattrs(slot);
memcpy(values, slot->tts_values, tupdesc->natts * sizeof(Datum));
memcpy(isnull, slot->tts_isnull, tupdesc->natts * sizeof(bool));
return RecnoFormTuple(tupdesc, values, isnull, NULL, NULL);
}
After:
slot_getallattrs(slot);
memcpy(values, slot->tts_values, tupdesc->natts * sizeof(Datum));
memcpy(isnull, slot->tts_isnull, tupdesc->natts * sizeof(bool));
{
RecnoTuple tuple = RecnoFormTuple(tupdesc, values, isnull, NULL, NULL);
pfree(values);
pfree(isnull);
return tuple;
}
}
📄 src/backend/access/recno/recno_xlog.c (L2075-L2077)
Dead code and a latent redo bug. RecnoXLogCompress()/XLOG_RECNO_COMPRESS has no producer anywhere in the tree (verified: only the definition here and the prototype in recno_xlog.h). Worse, recno_xlog_compress_redo() only flips RECNO_TUPLE_COMPRESSED / RECNO_INFOMASK_COMPRESSED and never copies the logged comp_data onto the tuple, so if this record type were ever emitted its replay would leave the on-disk tuple's uncompressed payload with the COMPRESSED flag set - guaranteed corruption / divergence from the primary. Per YAGNI, remove the unused WAL op (logger, redo handler, opcode, and rmgrdesc/decode cases) rather than shipping an unexercised, incorrect redo path.
📄 src/backend/access/undo/undo.c (L63-L63)
PendingUndoShmemSize() returns int, but this value is passed to add_size() (which takes Size) and the function body itself computes a Size internally (see pbu_undorequest.c:1008-1019, which builds up a Size via hash_estimate_size/add_size/mul_size then returns it through an int). Narrowing Size->int here is lossy and, on a large shared_buffers/MaxBackends configuration, can truncate or sign-flip the rollback hash-table + queue sizing, under-reserving shared memory. Every sibling shmem-size function (UndoLogShmemSize, XactUndoShmemSize, PbuUndoLogShmemSize, UndoLauncherShmemSize) returns Size; this one should too. Fix the declared return type at the definition/header and here.
(high confidence)
💡 Suggested change
Before:
extern int PendingUndoShmemSize(void);
After:
extern Size PendingUndoShmemSize(void);
📄 src/backend/access/undo/undo.c (L60-L63)
These functions are already declared in proper headers (PbuUndoLogShmemSize/PbuUndoLogShmemInit/UndoLogInit/PbuUndoLogShmemRequest in access/perbackend/pbu_undolog.h:294-297, PendingUndoShmemSize in pbu_undorequest.h:124, UndoLauncherShmemSize/UndoLauncherShmemInit in pbu_undoworker.h:20, PbuPerformUndoRecovery in pbu_undorequest.h). Redeclaring them as local externs means the compiler no longer cross-checks against the real prototype: if a signature drifts (as it already has for PendingUndoShmemSize, declared int here vs a Size-producing body), the mismatch compiles cleanly and fails at runtime. Include the real headers instead; if they collide on types, that header collision is the actual defect to fix, not to route around with shadow prototypes. On Windows/MSVC, any of these extern variables/functions consumed cross-module also need PGDLLIMPORT in the real header.
(high confidence)
📄 src/backend/access/recno/recno_xlog.c (L1077-L1079)
Dead code (YAGNI). The four HLC-explicit WAL variants (RecnoXLogInsertHLC / RecnoXLogUpdateHLC / RecnoXLogDeleteHLC) plus RecnoFillHLCInfo and RecnoReplicaAdvanceHLC have no callers anywhere in the tree - verified only definitions here and prototypes in recno_xlog.h. The forward operation paths use RecnoXLogInsert/Update/Delete, which compute HLC info inline. Unexercised WAL-emitting code is a footgun (it will silently rot out of sync with the real redo handlers) and pure scaffolding; remove it along with the corresponding prototypes.
📄 src/backend/access/recno/recno_overflow.c (L1552-L1554)
Dead scaffolding (YAGNI). The 'Legacy interface' block (RecnoStoreOverflow / RecnoFetchOverflow / RecnoDeleteOverflow, plus the RecnoOverflowRef type in recno.h) has no callers - verified across the tree. The comment itself admits it is 'kept for compatibility during transition', and RecnoFetchOverflow unconditionally ereport(ERROR)s 'not supported'. A committed patch should not carry a parallel-but-broken API; delete these three functions and RecnoOverflowRef, or wire them in. Shipping an exported function that always errors is a footgun for any extension that finds it in the header.
📄 src/backend/access/undo/xactundo.c (L949-L952)
This PG_CATCH does not reset the cached static WAL reader. ApplyUndoChainFromWAL() -> UndoReadBatchFromWAL() uses a module-static undo_batch_reader (undo_xlog.c) that persists in TopMemoryContext across calls. If an error escapes mid-read (e.g. a nested palloc failure inside XLogReadRecord), the reader is left with a stale segment FD / partial decode buffer, and the next inline abort reuses that corrupted state. The logical revert worker's equivalent PG_CATCH explicitly calls UndoResetBatchReader() for exactly this reason (logical_revert_worker.c:276, with a comment noting the next read would otherwise SIGSEGV). Add the same reset here.
(high confidence)
💡 Suggested change
Before:
BufferLockReleaseAll();
LeaveInlineUndoApplyState(saved_trans_state);
FlushErrorState();
undo_applied = false;
After:
BufferLockReleaseAll();
LeaveInlineUndoApplyState(saved_trans_state);
UndoResetBatchReader();
FlushErrorState();
undo_applied = false;
📄 src/backend/access/recno/recno_slog.c (L474-L479)
Comment/code mismatch. The comment says stale slots are reclaimed when their transaction 'has already committed ... or aborted and been marked ABORTED', but the code only reclaims committed transactions (TransactionIdDidCommit). An aborted-but-not-yet-cleaned op is left occupying its slot, contributing to the very overflow ERROR that follows. Either drop the 'or aborted' clause from the comment, or also reclaim slots where !TransactionIdIsInProgress && TransactionIdDidAbort (only if the visibility contract allows the ABORTED marker to be dropped early - which per the abort/UNDO design it likely does not, so the comment is the thing to fix).
📄 src/backend/access/undo/undo_xlog.c (L802-L804)
deferred->dboid = MyDatabaseId is set during the crash-recovery undo phase. PerformUndoRecovery() runs from the startup process at xlogrecovery.c:1899, before any database is selected, so MyDatabaseId is InvalidOid here. FlushDeferredUndoXacts() then enqueues these entries into the ATM with dboid=InvalidOid. The logical revert worker only processes entries where entry_dboid == MyDatabaseId (logical_revert_worker.c:241), and the launcher spawns per-database workers from the ATM's distinct dboids -- no worker ever connects to database InvalidOid. Result: any transaction deferred here (the any_skipped path) is orphaned in the ATM and never reverted. The UndoRecoveryEntry struct does not carry a dboid, so the correct database is not knowable from current tracking state -- the deferral mechanism needs the dboid captured at XLOG_UNDO_BATCH redo time (e.g. from the batch's relation) rather than read from MyDatabaseId.
(medium confidence)
📄 src/backend/access/undo/undo_xlog.c (L87-L89)
Stale comment: MAX_UNDO_LOGS is 100 (access/undolog.h:60), not 64. The loop below iterates MAX_UNDO_LOGS so the reasoning about scan cost still holds, but the hardcoded "64" is wrong and misleads a reader about the array bound. Reword to reference the macro rather than a literal, or correct it to 100.
(high confidence)
💡 Suggested change
Before:
* Find the log control structure. O(MAX_UNDO_LOGS) scan:
* with MAX_UNDO_LOGS=64 this is acceptable at recovery
* time (called once per record).
After:
* Find the log control structure. O(MAX_UNDO_LOGS) scan,
* acceptable at recovery time (called once per record).
📄 src/backend/access/undo/undo_bufmgr.c (L102-L107)
This entire module is dead code: none of UndoMakeBufferTag, ReadUndoBuffer, ReadUndoBufferExtended, ReleaseUndoBuffer, UnlockReleaseUndoBuffer, MarkUndoBufferDirty, InvalidateUndoBuffers, or InvalidateUndoBufferRange has any caller in the tree. The README states cluster-wide UNDO records are embedded in the WAL stream ("There are NO separate UNDO segment files"), so the shared_buffers integration this file implements is unused by the shipped design. Per YAGNI, unwired scaffolding should not land -- drop the file (and its header) until a consumer exists, or wire it in. If it is intentionally staged for a later patch, it belongs in that patch so each commit is independently justified.
(high confidence)
📄 src/backend/access/undo/undo_bufmgr.c (L235-L237)
last_block is a dead parameter: the body never uses it, and the comment itself admits DropRelationBuffers drops all blocks >= first_block, so the function silently over-invalidates beyond the documented [first_block, last_block] range. A signature that promises a range but ignores the upper bound is a footgun. Combined with the fact that this function has no caller at all, either remove the parameter and rename to reflect the actual "drop from first_block onward" semantics, or drop the function entirely.
(high confidence)
📄 src/backend/access/recno/recno_stats.c (L155-L163)
This reads a RecnoCompressionHeader from the wrong offset. In the RECNO tuple layout (see RecnoFormTuple/RecnoDeformTuple), RECNO_TUPLE_OVERHEAD is immediately followed by the null bitmap (MAXALIGN(bitmap_len)) and then the aligned attribute data; a compressed attribute's RecnoCompressionHeader lives inside its varlena payload (after VARDATA_ANY), not at header+RECNO_TUPLE_OVERHEAD. So orig_size/comp_size here are actually the first bytes of the null bitmap / first attribute, making the reported compression_ratio garbage. Since the tuple can hold multiple compressed attributes (and the header offset depends on bitmap length and per-attribute alignment), this cannot be recovered by a fixed offset - the code must walk attributes like RecnoDeformTuple does, or drop the ratio estimate entirely.
📄 src/backend/access/undo/slog_tuple.c (L2733-L2736)
The header states callers "MUST NOT hold any buffer content lock or other page-level critical section" because this walks every partition taking each writer_lock LW_EXCLUSIVE, but nothing enforces the precondition. A future AM caller that violates it (e.g. calling from a DML path while holding a buffer content lock) would silently convoy or self-deadlock against the sLog write path, with no failure at the point of misuse. Add an Assert() that no relevant LWLock/buffer content lock is held (there are currently zero Asserts in this 2994-line file). Same applies to SLogTupleCleanupRetained(). This is a documented lock-ordering invariant that should be checked in assert-enabled builds.
(medium confidence)
📄 src/include/access/recno.h (L144-L144)
Misleading comment: t_pad[3] does NOT make t_attrs_bitmap MAXALIGN'd. Counting offsets, t_infomask ends at byte 64 and t_pad[3] pushes offsetof(t_attrs_bitmap) to 68, which is only 4-aligned, not MAXALIGN (8). RECNO_TUPLE_OVERHEAD is therefore 68. This happens to be harmless because RecnoFormTuple/RecnoDeformTuple both anchor data at header + RECNO_TUPLE_OVERHEAD + MAXALIGN(bitmap_len) and then call att_align_nominal() on the absolute pointer for each datum, so alignment stays self-consistent - but the stated rationale is wrong. Compare the sibling FluxTupleHeader, which is deliberately a 40-byte (MAXALIGN'd) header guarded by a StaticAssertDecl. Suggest fixing the comment (the pad is just to reach a fixed, consistent overhead) or, better, adding a StaticAssertDecl so the invariant is enforced. Medium confidence.
📄 src/backend/access/undo/README (L841-L845)
This README uses reStructuredText/Sphinx markup -- .. code-block:: sql directives and double-backtick inline literals -- throughout. PostgreSQL's src/backend/access/*/README files (nbtree, gin, gist, hash, ...) are plain ASCII text; this markup renders nowhere and just adds noise. Convert to the plain-text convention used by the sibling READMEs. Also note the ASCII-only rule for the tree: verify no smart quotes / em-dashes / ellipsis characters slipped in when converting.
(medium confidence)
📄 src/backend/access/undo/undo.c (L230-L233)
Aspirational/future-tense comment describing behavior that has not shipped. Comments must describe what the code does now; "is wired into ... once a per-backend AM can produce non-empty undo-log state" (and the parenthetical instructing a future author to "create the pg_undo dir, call CheckPointUndoLogs from CheckPointGuts, call StartupUndoLogs from the recovery redo point") documents a TODO for a not-yet-existing consumer. If CheckPointUndoLogs/StartupUndoLogs are not actually invoked in the checkpoint/startup path in this patch, that is unshipped scaffolding: state plainly what is and isn't wired, or drop the note until the consumer lands.
(medium confidence)
📄 src/include/access/recno.h (L105-L105)
Typo in a plain C comment: 100%% uses a printf-style percent escape that has no meaning here. It should read 100%.
💡 Suggested change
Before:
* A page packed to 100%% therefore makes every row on it permanently
After:
* A page packed to 100% therefore makes every row on it permanently
📄 src/include/access/recno.h (L323-L326)
These four externs are redundant: storage/subsystems.h already emits extern const ShmemCallbacks <name>; for every PG_SHMEM_SUBSYSTEM entry in subsystemlist.h (RecnoHLCShmemCallbacks..RecnoClockShmemCallbacks are all listed there, L109-112). Any .c that needs them includes subsystems.h. Duplicating the declaration here is a maintenance hazard: if the two declarations ever diverge on linkage (e.g. one gains PGDLLIMPORT), MSVC flags inconsistent dllimport. Drop these four lines. Low confidence (harmless today, and flux.h duplicates the same way).
📄 src/include/access/recno.h (L302-L305)
Empty section: this banner comment ("WAL / undo resource-manager ids ...") has no declarations under it. Dead scaffolding - either remove the empty section header or move the content it was meant to hold here.
📄 src/backend/access/undo/xactundo.c (L1034-L1042)
Two defects here:
-
The comment references
AtCleanup_XactUndo, which does not exist anywhere in the tree. The function that actually clears the slot at backend exit isAtProcExit_XactUndo()(viaUndoClearBatchLSN()). Fix the reference (identity accuracy). -
More seriously, this is a silent-data-loss footgun. When both inline UNDO apply and
ATMAddAborted()fail (ATM full), the code keeps the per-backend batch-LSN slot but the transaction's index/heap changes are never reverted. The slot is eventually cleared at backend exit (or by the undo worker's dead-backend scan in perform_undo_discard()) without the UNDO ever being applied, so the aborted changes persist as if committed while only anelog(DEBUG1)records the loss. There is no recovery path that later applies this transaction's UNDO. At minimum this should be a WARNING (not DEBUG1) that clearly states rollback did not occur; ideally the abort should not be allowed to complete without a durable rollback obligation recorded somewhere.
📄 src/backend/access/undo/undoworker.c (L395-L398)
Portability defect: retained_mb is uint64, but the format uses %lu with a cast to (unsigned long). On LLP64 platforms (Windows/MSVC) unsigned long is 32-bit, so values >= 4 GB are truncated. Use UINT64_FORMAT and drop the cast, per the project's int64 formatting convention.
💡 Suggested change
Before:
if (retained_mb > (uint64) undo_max_wal_retention_size)
ereport(WARNING,
(errmsg("UNDO WAL retention (%lu MB) exceeds undo_max_wal_retention_size (%d MB)",
(unsigned long) retained_mb, undo_max_wal_retention_size),
After:
if (retained_mb > (uint64) undo_max_wal_retention_size)
ereport(WARNING,
(errmsg("UNDO WAL retention (" UINT64_FORMAT " MB) exceeds undo_max_wal_retention_size (%d MB)",
retained_mb, undo_max_wal_retention_size),
📄 src/backend/access/undo/undoworker.c (L124-L130)
Async-signal-safety violation: this SIGTERM handler writes UndoWorkerShmem->shutdown_requested, a plain bool living in shared memory. Writing shared memory from a signal handler is not async-signal-safe, and a non-volatile sig_atomic_t object gives no atomicity/visibility guarantee across the handler/main-loop boundary. Also, neither this handler nor undo_worker_sighup saves/restores errno (contrast the logical_revert_worker handlers, which do). Use the standard pattern: a file-scope volatile sig_atomic_t flag set by the handler (or reuse SignalHandlerForShutdownRequest/die), save+restore errno, and have the main loop copy it into the shmem field if it must be shared.
📄 src/backend/access/undo/atm.c (L386-L388)
rec is declared uninitialized and then filled field-by-field. AtmStateRecord has internal padding (a 4-byte gap before the 8-byte-aligned last_batch_lsn, plus trailing padding after the bool revert_complete). Both COMP_CRC32C(crc, &rec, sizeof(rec)) and write(fd, &rec, sizeof(rec)) therefore consume uninitialized stack bytes. This yields a non-reproducible CRC across runs and trips Valgrind/MemorySanitizer on an on-disk format. Zero the struct first: memset(&rec, 0, sizeof(rec)); before assigning fields. (The same applies conceptually to hdr, though AtmStateHeader has no padding.)
💡 Suggested change
Before:
AtmStateRecord rec;
rec.xid = entries[i].xid;
After:
AtmStateRecord rec;
memset(&rec, 0, sizeof(rec));
rec.xid = entries[i].xid;
📄 src/backend/access/undo/logical_revert_worker.c (L559-L560)
Liveness concern: next_known accumulates both carried-forward already-tracked databases and newly-started ones, and this ceiling check counts all of them. When the number of databases simultaneously holding unreverted ATM entries exceeds max_logical_revert_workers, databases past the ceiling never get a worker until a currently-tracked database's work fully drains and it drops out of get_revertable_database_list(). That indefinitely delays guaranteed rollback for the starved databases, silently. Consider round-robining across revertable databases each scan, or documenting that max_logical_revert_workers must be >= the number of databases expected to hold concurrent aborted-transaction UNDO.
📄 src/backend/access/undo/undoworker.c (L608-L609)
Style/consistency: use snprintf with the field size instead of sprintf for bgw_library_name/bgw_function_name, matching the bounded-string convention and the snprintf already used for bgw_name/bgw_type below. (logical_revert_worker.c's LogicalRevertLauncherRegister uses snprintf for these same fields, so this is also an internal inconsistency.)
📄 src/backend/access/undo/undoinsert.c (L86-L88)
xl_undo_batch is declared uninitialized and only its named fields are assigned; the 4-byte alignment padding between xid (offset 0) and the 8-byte-aligned chain_prev (offset 8) is never written. XLogRegisterData((char *) &xlrec, SizeOfUndoBatch) then copies that uninitialized stack padding into the WAL stream, which is archived and shipped to standbys. This leaks uninitialized backend memory into durable WAL and makes the emitted WAL non-deterministic (a problem for wal_consistency_checking and reproducible-WAL comparisons). Zero the struct first, per the standard convention for WAL record structs. (moderate confidence)
💡 Suggested change
Before:
xl_undo_batch xlrec;
XLogRecPtr batch_lsn;
Oid primary_reloid = InvalidOid;
After:
xl_undo_batch xlrec;
XLogRecPtr batch_lsn;
Oid primary_reloid = InvalidOid;
MemSet(&xlrec, 0, sizeof(xlrec));
📄 src/backend/access/undo/slog_tuple.c (L488-L490)
This ShmemAlloc(sizeof(SLogFlatPartition) * SLogNumPartitions) is not accounted for in SLogTupleShmemSize() or SLogTupleShmemRequest() (both size only the flat-hash block via SLogFlatHashPartitionedShmemSize). With up to SLOG_FLAT_MAX_PARTITIONS (256) partitions of LWLockPadded+pointer each, this is a non-trivial unrequested allocation drawn from shared-memory slop, and can trip "out of shared memory" if the slop is exhausted. Add this array's size to SLogTupleShmemSize() (and register it) so the segment is sized correctly. (moderate confidence)
📄 src/backend/access/undo/README (L546-L546)
This claim -- that XLOG_UNDO_APPLY_RECORD CLRs "Always include REGBUF_FORCE_IMAGE (full page image)" and are restored purely by full-page image on redo -- no longer matches the code. undo_xlog.c's XLOG_UNDO_APPLY_RECORD handler has a BLK_NEEDS_REDO branch that physiologically replays the change (UNDO_CLR_LP_DEAD marks the line pointer dead; UNDO_CLR_HAS_TUPLE memcpys a before-image from block data), and the actual CLR producers (e.g. nbtree_undo.c) register the buffer with REGBUF_STANDARD, not REGBUF_FORCE_IMAGE. Section 7's CLR pattern and section 8/9's "restored full page image via XLogReadBufferForRedo ... without re-reading UNDO data" describe superseded behavior. Update the README to reflect physiological CLR replay. (high confidence)
📄 src/backend/access/undo/slog_tuple.c (L917-L921)
The "WAIT-FREE: uses the seqlock read-side" claim (repeated on many read functions in this file: SLogTupleLookup, SLogTupleLookupFiltered, SLogTupleHasEntry, SLogTupleGetDirtyXid, SLogTupleGetDirtyWriterXid, SLogTupleGetWriteConflictXid, SLogTupleIterateByTid, ...) is no longer accurate. The seqlock was replaced by a plain LWLock: SLOG_SEQ_READ_BEGIN/END now take the partition writer_lock in LW_SHARED, and SLogSeqWriteBegin/End are no-ops (see slog_flathash.h). These reads block against exclusive writers and are not wait-free. Update the comments to describe the LW_SHARED read path so they don't mislead. (high confidence)
📄 src/backend/access/undo/xactundo.c (L924-L925)
Memory-context leak and stale CurrentMemoryContext on the invalid-LSN path. At this point MemoryContextSwitchTo(undo_ctx) has already run (CurrentMemoryContext == undo_ctx). The goto inline_undo_done jumps to the label, which sits after the MemoryContextSwitchTo(old_ctx) / MemoryContextDelete(undo_ctx) cleanup pair. As a result: (1) undo_ctx is never deleted -- and since its parent is TopMemoryContext, it survives the transaction, so every abort with an invalid perm_lsn leaks an AllocSet context (unbounded backend growth); (2) execution continues (ATM registration, UndoRecordSetFree, ResetXactUndo, ...) with CurrentMemoryContext still pointing at the leaked context. This is a MUST-NOT-FAIL post-abort path. Restore the context and delete undo_ctx before the goto (or move the validate check ahead of the context creation, mirroring the correct AtSubAbort_XactUndo path which uses an else branch instead of a goto).
💡 Suggested change
Before:
undo_applied = false;
goto inline_undo_done;
After:
undo_applied = false;
MemoryContextSwitchTo(old_ctx);
MemoryContextDelete(undo_ctx);
goto inline_undo_done;
📄 src/backend/access/undo/logical_revert_worker.c (L241-L242)
Head-of-line blocking / starvation. ATMGetNextUnreverted() (backed by SLogTxnGetNextUnreverted) always returns the single lowest-xid unreverted ATM entry regardless of database -- it does not take MyDatabaseId into account. If that first entry belongs to a different database, this worker hits goto sleep, wakes, gets the same entry again, and sleeps forever, never advancing to reach unreverted entries that DO belong to MyDatabaseId. So an entry for this worker's database is starved for as long as any lower-xid entry for another database stays unreverted (e.g. if that other database's worker is absent, wedged, or its database was dropped). Either make ATMGetNextUnreverted accept/filter by dboid so each worker can skip foreign entries and find its own, or have the worker iterate past non-matching entries rather than bailing out on the first mismatch.
📄 src/backend/access/undo/perbackend/pbu_recovery.c (L386-L397)
This PG_CATCH swallows the error and continues the loop (and re-enters PbuRegisterRecoveredRollbackReq, which takes RollbackRequestLock and may palloc), but it never releases resources held at the throw point. execute_undo_actions() -> UndoRecordBulkFetch()/UndoFetchRecord() pins undo buffers and transiently takes log->discard_lock; if an ereport fires mid-fetch (e.g. corrupted undo, elog in UndoLogGet), those buffer pins and any held LWLock are leaked. This runs during the ARIES undo phase inside PerformWalRecovery() with no enclosing transaction/subtransaction abort to clean them up, so a leaked LWLock here can wedge recovery. A swallow-and-continue PG_CATCH must perform the cleanup that AbortTransaction would normally do -- at minimum LWLockReleaseAll() (and drop any buffer pins) before proceeding. Consider running each transaction's inline apply inside a subtransaction, or add explicit LWLockReleaseAll()/UnlockReleaseUndoBuffers() to the catch.
📄 src/backend/access/undo/perbackend/pbu_undoinsert.c (L311-L317)
xact_urec_info[xact_urec_info_idx] is written and xact_urec_info_idx incremented with no bound check against MAX_XACT_UNDO_INFO (2), and the inner idx_undo_buffers[index++] is written with no bound check against MAX_BUFFER_PER_UNDO (2). PrepareUpdateUndoActionProgress() has the same unguarded pattern. In the log-switch path UndoRecordAllocate() calls UndoRecordPrepareTransInfo() twice, which fills exactly both slots -- so today the array is not overflowed, but nothing here enforces that: a third prepared header (or a transaction header split across more than 2 blocks) would silently corrupt adjacent statics (undo_buffer/prepared_undo). UndoRecordUpdateTransInfo() already guards its index; add matching Assert(xact_urec_info_idx < MAX_XACT_UNDO_INFO) before the write and Assert(index < MAX_BUFFER_PER_UNDO) in the loop.
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L1202-L1206)
get_undo_log_by_number() is documented to return NULL for a stale/entirely-discarded logno, and many callers in this file were hardened to check for it (e.g. UndoLogGetLastXactStartPoint, UndoRecordPrepareTransInfo). This function dereferences log->mutex without that check. It is reachable from the discard loop (UndoDiscardOneLog) with a logno derived from an on-disk next_urecptr, where a concurrently-discarded log could yield NULL and segfault. Add the same defensive if (log == NULL) return InvalidUndoRecPtr; guard used by the sibling accessors. The same missing check exists in UndoLogSetLastXactStartPoint() and UndoLogRewind().
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1098-L1104)
Same one-element-only bug as the Xid queue: sizeof(UndoSizeQueue) and sizeof(UndoErrorQueue) clear a single struct, not the full ROLLBACK_REQUEST_QUEUE_SIZE array. Use the corresponding *ShmSize() helpers. (high confidence)
💡 Suggested change
Before:
binaryheap_init_shm(UndoSizeBinaryHeap, ROLLBACK_REQUEST_QUEUE_SIZE,
undo_size_comparator, NULL);
memset(UndoSizeQueueElems, 0, sizeof(UndoSizeQueue));
binaryheap_init_shm(UndoErrorBinaryHeap, ROLLBACK_REQUEST_QUEUE_SIZE,
undo_err_time_comparator, NULL);
memset(UndoErrorQueueElems, 0, sizeof(UndoErrorQueue));
After:
binaryheap_init_shm(UndoSizeBinaryHeap, ROLLBACK_REQUEST_QUEUE_SIZE,
undo_size_comparator, NULL);
memset(UndoSizeQueueElems, 0, UndoSizeQueueElemsShmSize());
binaryheap_init_shm(UndoErrorBinaryHeap, ROLLBACK_REQUEST_QUEUE_SIZE,
undo_err_time_comparator, NULL);
memset(UndoErrorQueueElems, 0, UndoErrorQueueElemsShmSize());
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1035-L1037)
Hash-table keysize mismatch. The hash key used at every lookup/insert site is RollbackHashKey (full_xid + start_urec_ptr, 16 bytes), but the table is registered with keysize = sizeof(TransactionId) (4 bytes) plus HASH_BLOBS. dynahash hashes and compares exactly keysize bytes (see dynahash.c: hash(keyPtr, keysize) / match(..., keysize)), so it only considers the low 4 bytes of the 8-byte full_xid and completely ignores the epoch and start_urec_ptr. Two distinct requests differing only in epoch or start_urec_ptr collide, and on HASH_ENTER only 4 bytes of the key are copied into the entry, leaving start_urec_ptr uninitialized. The whole rollback-dedup mechanism is unsound. Register with info.keysize = sizeof(RollbackHashKey). (high confidence)
💡 Suggested change
Before:
MemSet(&info, 0, sizeof(info));
info.keysize = sizeof(TransactionId);
info.entrysize = sizeof(RollbackHashEntry);
After:
MemSet(&info, 0, sizeof(info));
info.keysize = sizeof(RollbackHashKey);
info.entrysize = sizeof(RollbackHashEntry);
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1182-L1184)
Inconsistent hash key and unchecked NULL deref. Every other call site builds a RollbackHashKey (full_xid + start_urec_ptr) as the search key, but here a bare FullTransactionId (&urinfo->full_xid) is passed. Even once the keysize is corrected to sizeof(RollbackHashKey), this reads only 8 bytes where a 16-byte key is expected, so the trailing start_urec_ptr bytes are read out of the stack past full_xid and the lookup will not match the entry inserted by RegisterRollbackReq(). The result rh is then unconditionally dereferenced (rh->in_progress = false), which is a NULL-pointer write when the entry is not found. Build a RollbackHashKey and check rh for NULL before writing. (high confidence)
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L66-L66)
max_undo_workers is a hardcoded file-static (= 5) that is never registered as a GUC nor bound to guc_parameters.dat/guc_tables.c (grep confirms it exists only in this file). It drives the worker-slot array sizing (UndoLauncherShmemSize), every iteration bound, the launcher-registration guard, and PbuEnsureUndoLauncher()'s WARNING which tells the admin to "Raise max_undo_workers and restart" -- a knob that does not exist and cannot be changed. Either wire it to a real GUC or remove the misleading configurability language. The same applies to undo_worker_quantum_ms (documented as a knob in comments but hardcoded). (high confidence)
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L601-L602)
The errhint refers to "max_undo_workers" as if it were a configuration parameter, but it is a hardcoded file-static in this module (see line 66), not a GUC. As written the operator cannot act on this hint. Fix the underlying config (make it a GUC) or reword the message to not reference a non-existent parameter. (high confidence)
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L314-L318)
These helpers return int but compute mul_size(ROLLBACK_REQUEST_QUEUE_SIZE, sizeof(...)), which yields Size; assigning Size to int truncates. It is safe today (1024 * 24 = 24576), and the consumers (PendingUndoShmemSize, ShmemRequestStruct .size which is ssize_t) expect a Size, so the narrowing is a latent overflow footgun that violates the Size discipline for size math. Return Size. Note PendingUndoShmemSize() is also declared to return int in the header. (moderate confidence)
💡 Suggested change
Before:
static int
UndoXidQueueElemsShmSize(void)
{
return mul_size(ROLLBACK_REQUEST_QUEUE_SIZE, sizeof(UndoXidQueue));
}
After:
static Size
UndoXidQueueElemsShmSize(void)
{
return mul_size(ROLLBACK_REQUEST_QUEUE_SIZE, sizeof(UndoXidQueue));
}
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L476-L477)
In the PG_CATCH path, InsertRequestIntoErrorUndoQueue() acquires RollbackRequestLock before AbortOutOfAnyTransaction() runs. If execute_undo_actions() errored while still holding an LWLock (e.g. an undo log discard_lock or a buffer content lock), that lock is not released until the abort below, so RollbackRequestLock is acquired while another LWLock is held. This is a lock-ordering hazard on the error path; the safer pattern is to abort the transaction first, then register the error request. Please confirm execute_undo_actions() cannot leave any LWLock held on error. (low confidence)
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L9-L9)
IDENTIFICATION header path is wrong: this file is src/backend/access/undo/perbackend/pbu_undorequest.c, not src/backend/access/undo/undorequest.c. (high confidence)
💡 Suggested change
Before:
* src/backend/access/undo/undorequest.c
After:
* src/backend/access/undo/perbackend/pbu_undorequest.c
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L10-L10)
IDENTIFICATION header path is wrong: this file is src/backend/access/undo/perbackend/pbu_undoworker.c, not src/backend/postmaster/undoworker.c. (high confidence)
💡 Suggested change
Before:
* src/backend/postmaster/undoworker.c
After:
* src/backend/access/undo/perbackend/pbu_undoworker.c
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L139-L139)
Typos: "rollabck" -> "rollback". Also "acquring" -> "acquiring" appears in the InsertRequestIntoUndoQueues and CanPushReqToUndoWorker comments, and "accomodate" -> "accommodate" in UndoRollbackHashTableSize(). (high confidence)
💡 Suggested change
Before:
/* This is the hash table to store all the rollabck requests. */
After:
/* This is the hash table to store all the rollback requests. */
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1587-L1587)
Typo: "resuests" -> "requests". (high confidence)
💡 Suggested change
Before:
* that we push the resuests only when there is some space in
After:
* that we push the requests only when there is some space in
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L699-L699)
Typo: "wroker" -> "worker". (high confidence)
💡 Suggested change
Before:
* Get the dbid where the wroker should connect to and get the worker
After:
* Get the dbid where the worker should connect to and get the worker
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L441-L441)
Copy-paste comment drift: this is the undo worker's on-exit callback, not a logical replication worker. (moderate confidence)
💡 Suggested change
Before:
* Called on logical replication worker exit.
After:
* Called on undo worker exit.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L859-L864)
Stray double blank line between Case 1 and Case 2. (high confidence)
💡 Suggested change
Before:
break;
}
/*
* Case 2: The transaction ended in the same undo log, but this is not
After:
break;
}
/*
* Case 2: The transaction ended in the same undo log, but this is not
📄 src/include/access/undormgr.h (L45-L48)
This comment is factually wrong and dangerous for a WAL-durable ID space. It claims "2, 4, and 5 are reserved by out-of-core consumers", but in this same change set IDs 2 (UNDO_RMID_FILEOPS, storage/fileops.h), 5 (UNDO_RMID_FLUX, access/flux_undo.h) and 7 (UNDO_RMID_RECNO, access/recno_undo.h) are all defined and registered in-core via access/undormgrlist.h. Since these RM IDs are stamped into WAL-durable UNDO records, the authoritative allocation must be documented accurately here (or, better, all built-in IDs should be defined centrally). As written, a reader following this comment could hand ID 5 to a new consumer and silently misdispatch FLUX rollback. Fix the comment to reflect the real allocation (0=INVALID, 1=NBTREE, 2=FILEOPS, 3=HASH, 5=FLUX, 7=RECNO; 4 and 6 free), and note that FLUX/RECNO/FILEOPS are in-core, not out-of-core. Confidence: high.
📄 src/include/access/undormgr.h (L107-L107)
UndoRmgrs[] is an exported global table read cross-module: GetUndoRmgr() is called from access/flux and access/recno (separate modules), and RegisterUndoRmgr() from fileops/nbtree/hash. On Windows/MSVC an exported variable needs PGDLLIMPORT in its declaration to be linkable from another module or an extension (the tree's convention -- see access/xlog.h, access/tableam.h). Add PGDLLIMPORT here.
💡 Suggested change
Before:
extern const UndoRmgrData *UndoRmgrs[MAX_UNDO_RMGRS];
After:
extern PGDLLIMPORT const UndoRmgrData *UndoRmgrs[MAX_UNDO_RMGRS];
📄 src/include/access/undolog.h (L147-L151)
These GUCs are declared in a public access header expressly so table/index AMs (flux, recno) can read them, but they lack PGDLLIMPORT. Every other exported GUC in access/*.h uses extern PGDLLIMPORT int ... (e.g. access/xlog.h, access/gin.h, access/tableam.h). Without PGDLLIMPORT, any cross-module or extension reference fails to link on Windows/MSVC. Add PGDLLIMPORT to each.
💡 Suggested change
Before:
extern int undo_worker_naptime;
extern int undo_rollback_overflow_size;
extern int undo_max_wal_retention_size;
extern int undo_batch_size_kb;
extern int undo_batch_record_limit;
After:
extern PGDLLIMPORT int undo_worker_naptime;
extern PGDLLIMPORT int undo_rollback_overflow_size;
extern PGDLLIMPORT int undo_max_wal_retention_size;
extern PGDLLIMPORT int undo_batch_size_kb;
extern PGDLLIMPORT int undo_batch_record_limit;
📄 src/include/access/undolog.h (L139-L139)
UndoLogShared is an exported global pointer. If it is ever referenced from another module or extension it needs PGDLLIMPORT on Windows/MSVC (tree convention: access/transam.h extern PGDLLIMPORT TransamVariablesData *TransamVariables;). It is currently only used inside the undo module, but as a public header symbol it should carry the marker to be safe against future cross-module use.
💡 Suggested change
Before:
extern UndoLogSharedData *UndoLogShared;
After:
extern PGDLLIMPORT UndoLogSharedData *UndoLogShared;
📄 src/include/access/undo.h (L50-L50)
UndoContext is an exported global. If any cross-module code or extension references it, it needs PGDLLIMPORT on Windows/MSVC (same convention as CurrentSession in access/session.h). Add the marker or document that it is undo-module-private.
💡 Suggested change
Before:
extern MemoryContext UndoContext;
After:
extern PGDLLIMPORT MemoryContext UndoContext;
📄 src/include/access/undo_xlog.h (L223-L227)
xl_undo_batch is a WAL-durable record: it is written by a raw struct copy (XLogRegisterData((char *) &xlrec, SizeOfUndoBatch) in undoinsert.c) and read back by casting the raw WAL bytes (xlrec = (xl_undo_batch *) record_data in undo_xlog.c). Storing UndoPersistenceLevel persistence -- a bare enum -- as the trailing field makes the on-disk width compiler-dependent (an enum is not guaranteed to be a fixed 4 bytes; -fshort-enums or a differing compiler can change it), and SizeOfUndoBatch = offsetof(...) + sizeof(UndoPersistenceLevel) then differs between the writer and a reader built differently. This breaks WAL portability/replay across the platforms and toolchains PostgreSQL targets. Use a fixed-width field (e.g. uint8) for persistence in the on-disk struct and cast at use. The same concern applies to Oid primary_reloid immediately preceding the enum, whose trailing padding depends on the enum width. Confidence: high.
📄 src/include/access/undorecord.h (L75-L75)
urec_prev sits in every serialized UNDO record header (8 bytes each) and is written by the record builders (undorecord.c:318,371) but never read by any code -- the header's own comment states it is "NOT read by the apply path" and the chain is between batches via xl_undo_batch.chain_prev. Carrying an unread 8-byte back-link in every WAL-durable UNDO record wastes space on the hot insert path and is a documented footgun. Per YAGNI, drop it from the serialized format unless a concrete reader is planned; "format stability" for a field with no reader is not justification.
📄 src/include/access/undolog.h (L186-L194)
These "legacy no-op stub" externs are still invoked from live paths (ExtendUndoLogFile from the undo_xlog.c redo path; UndoLogSealAndRotate/UndoLogDeleteSegmentFile from undostats.c and undoworker.c; UndoFlushResetMaxWritePtr/UndoLogCloseFiles from xactundo.c), so this is not merely dead scaffolding in a header -- it is a set of no-op functions that real code still calls, which is a maintainability hazard and contradicts the minimalism discipline. Either remove the stubs and their call sites, or, if a call site must stay, replace the no-op with the real behavior. Leaving no-op stubs wired into the discard/redo paths silently drops work a future reader will assume happens.
📄 src/include/access/undo_bufmgr.h (L95-L96)
Comment drift and aspirational phrasing violate the comment-accuracy discipline (comments must describe what the code does now). UndoLogGetRelFileLocator documents "log_number (0..16M)" but the type is uint32 and MAX_UNDO_LOGS is 100, so the real bound is 0..99; the 16M figure is the UndoRecPtr encoding capacity, not the supported log count. Fix the range to match the actual bound.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L699-L704)
Stray cur_undo_queue++ side effect in a query-only helper. In the first (scheduling) loop of UndoGetWork, the caller already does cur_undo_queue++; continue; when this returns false (see lines 1244-1248), so an empty ERROR_QUEUE increments the round-robin cursor twice and skips the following queue. Worse, the peek loop iterates a local cur_queue (0..MAX_UNDO_WORK_QUEUES) and never touches the global cursor; when it probes an empty ERROR_QUEUE this silently advances cur_undo_queue, corrupting the scheduling position for the next UndoGetWork call. The XID/SIZE branches correctly just return false. Drop the increment here and let the caller own the cursor.
💡 Suggested change
Before:
/* check if there is a work in the next queue */
if (GetErrorQueueSize() <= n)
{
cur_undo_queue++;
return false;
}
After:
/* check if there is a work in the next queue */
if (GetErrorQueueSize() <= n)
return false;
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L695-L696)
last_xact_processed_at is a file-static TimestampTz, so it starts at 0 in each freshly-forked undo worker. If the initial UndoGetWork (line 722) finds no work for this database (the request was already stolen by another in-progress worker), it is never assigned. The main loop's linger path then computes timeout = TimestampTzPlusMilliseconds(0, UNDO_WORKER_LINGER_MS), which is far in the past, so timeout <= GetCurrentTimestamp() is immediately true and the worker exits at once instead of lingering for UNDO_WORKER_LINGER_MS. This defeats the linger optimization the file header describes. Initialize it to the worker start time.
💡 Suggested change
Before:
ResetUndoRequestInfo(&urinfo);
started_at = GetCurrentTimestamp();
After:
ResetUndoRequestInfo(&urinfo);
started_at = GetCurrentTimestamp();
last_xact_processed_at = started_at;
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L149-L156)
Signal handler must save and restore errno. SetLatch() can call write() on the self-pipe and clobber errno; interrupting arbitrary code that later inspects errno then observes a corrupted value. The sibling handler UndoLauncherSighup (just below) correctly does int save_errno = errno; ...; errno = save_errno; around the same SetLatch() call. Apply the same pattern here.
💡 Suggested change
Before:
static void
UndoworkerSigtermHandler(SIGNAL_ARGS)
{
got_SIGTERM = true;
/* Waken anything waiting on the process latch */
SetLatch(MyLatch);
}
After:
static void
UndoworkerSigtermHandler(SIGNAL_ARGS)
{
int save_errno = errno;
got_SIGTERM = true;
/* Waken anything waiting on the process latch */
SetLatch(MyLatch);
errno = save_errno;
}
📄 src/include/access/undo_xlog.h (L249-L254)
These three 2PC callbacks are declared here but have no definition and no caller anywhere in the tree (verified by search: only this declaration exists). This is dead scaffolding for a 2PC UNDO path that is not wired up. Either implement/wire them or drop the declarations (YAGNI). Note xl_undo_chain_state below is similarly defined-but-unused.
📄 src/include/access/undo_xlog.h (L49-L50)
This comment inverts the actual bit layout. The info codes below (0x00, 0x10, ... 0x60) put the operation type in the UPPER 4 bits and leave the LOW 4 bits free -- and undo_redo() reads them via XLogRecGetInfo(record) & ~XLR_INFO_MASK (0xF0), confirming the op code lives in the high nibble. Per PG convention the low nibble is reserved for the xlog system (XLR_INFO_MASK). As written, a reader trusting this comment could allocate a new op code in the low nibble and collide with XLR_INFO_MASK.
💡 Suggested change
Before:
* These are the info codes for UNDO WAL records. The low 4 bits are used
* for operation type, leaving the upper 4 bits for flags.
After:
* These are the info codes for UNDO WAL records. The high 4 bits carry the
* operation type; the low 4 bits are reserved for the xlog system
* (XLR_INFO_MASK).
📄 src/include/access/undo_xlog.h (L18-L19)
This file-header comment says crash safety is provided "via WAL replay of the CLR's full page image", but the xl_undo_apply documentation further down explicitly states this is a physiological CLR that does NOT store a full page image ("Instead of storing a full 8KB page image (REGBUF_FORCE_IMAGE), we log just the operation and its data"). The two descriptions contradict each other; fix the header to describe the physiological replay.
📄 src/include/access/xact.h (L154-L154)
These new hook function-pointer variables are extern and are read from other modules (xact.c defines them; a table AM/subsystem in a separate translation unit or extension assigns them). On Windows/MSVC an extern variable referenced cross-module needs PGDLLIMPORT or the reference fails to link. Every other exported variable in this header (DefaultXactIsoLevel, XactReadOnly, MyXactFlags, ...) already carries PGDLLIMPORT; these must too. (Function externs below do not need it, only these variables.)
💡 Suggested change
Before:
extern void (*TableAMPrepare_hook) (void);
After:
extern PGDLLIMPORT void (*TableAMPrepare_hook) (void);
📄 src/include/access/xact.h (L169-L172)
Same PGDLLIMPORT gap as TableAMPrepare_hook: these exported hook variables are assigned/read cross-module and need PGDLLIMPORT for the Windows/MSVC build.
💡 Suggested change
Before:
extern void (*PendingPhysOpsDo_hook) (bool isCommit);
extern void (*PendingPhysOpsPostPrepare_hook) (void);
extern void (*PendingPhysOpsAtSubCommit_hook) (void);
extern void (*PendingPhysOpsAtSubAbort_hook) (void);
After:
extern PGDLLIMPORT void (*PendingPhysOpsDo_hook) (bool isCommit);
extern PGDLLIMPORT void (*PendingPhysOpsPostPrepare_hook) (void);
extern PGDLLIMPORT void (*PendingPhysOpsAtSubCommit_hook) (void);
extern PGDLLIMPORT void (*PendingPhysOpsAtSubAbort_hook) (void);
📄 src/include/access/atm_xlog.h (L27-L35)
WAL-record layout must not depend on struct padding, and must not leak uninitialized bytes into WAL. With this field order the 8-byte last_batch_lsn after the 4-byte xid forces 4 bytes of internal padding on typical ABIs. atm.c writes the record with a raw XLogRegisterData((char *) &xlrec, SizeOfXlAtmAbort) (no memset), so that padding gap goes into WAL uninitialized -- non-deterministic WAL content across backends/platforms (a wal_consistency_checking / Valgrind hazard, and a portability concern). Put the 8-byte member first to eliminate the internal pad (or memset the struct before filling it).
Separately, reloid is dead: ATMAddAborted() always sets it to InvalidOid and atm_redo() feeds that InvalidOid straight through -- the comment even says it is "kept for struct layout." Per YAGNI it should be removed rather than shipped as an always-invalid WAL field.
💡 Suggested change
Before:
typedef struct xl_atm_abort
{
TransactionId xid;
XLogRecPtr last_batch_lsn; /* LSN of last UNDO batch for this xid */
Oid dboid;
Oid reloid; /* InvalidOid (kept for struct layout) */
} xl_atm_abort;
#define SizeOfXlAtmAbort (offsetof(xl_atm_abort, reloid) + sizeof(Oid))
After:
typedef struct xl_atm_abort
{
XLogRecPtr last_batch_lsn; /* LSN of last UNDO batch for this xid */
TransactionId xid;
Oid dboid;
} xl_atm_abort;
#define SizeOfXlAtmAbort (offsetof(xl_atm_abort, dboid) + sizeof(Oid))
📄 src/include/access/logical_revert_worker.h (L25-L25)
Installed backend headers under src/include must not include "postgres.h"; the .c file is responsible for including it first. Doing it here breaks the tree's header convention and can cause include-order problems for extensions/frontend consumers. The Size/Datum types this header needs come in via the .c's prior postgres.h include; the other new headers in this feature (undoworker.h, xactundo.h, atm.h) correctly avoid it. Remove this line. (Note the same mistake exists in the sibling flux/recno headers.)
📄 src/include/access/undoworker.h (L49-L50)
undo_worker_naptime is already declared extern int undo_worker_naptime; in access/undolog.h (and defined in undolog.c). Declaring it again here creates two competing declarations of the same global across two headers, which drift independently. Either drop it here and include undolog.h (already included above), or move the single declaration to one canonical header.
📄 src/include/access/undo.h (L40-L41)
This claims UndoShmemSize()/UndoShmemInit() are called from ipci.c, but they are not: ipci.c contains no reference to undo, and undo.c registers them through the PG_SHMEM_SUBSYSTEM(UndoShmemCallbacks) mechanism in subsystemlist.h (request_fn/init_fn/attach_fn). Update the comment to reference the ShmemCallbacks/subsystemlist.h registration path.
📄 src/include/access/undolog.h (L143-L145)
This comment calls MAX_UNDO_LOGS a "legacy define retained for compile-time compatibility", but MAX_UNDO_LOGS is load-bearing: it sizes UndoLogControl logs[MAX_UNDO_LOGS] in UndoLogSharedData and bounds active loops throughout undolog.c/undo_xlog.c/undostats.c/undoworker.c. Only UNDO_LOG_SEGMENT_SIZE is truly legacy. Correct the comment so MAX_UNDO_LOGS is not mislabeled.
📄 src/include/access/undorecord.h (L107-L112)
UndoRecordSetChunkHeader (and SizeOfUndoRecordSetChunkHeader) is defined only here and referenced only in the README/typedefs.list -- it has zero code uses anywhere. It describes a multi-chunk / multi-log spanning design that is not implemented in the UNDO-in-WAL architecture. This is dead scaffolding; drop it per YAGNI or wire it up.
📄 src/include/access/undormgrlist.h (L20-L23)
This header comment ("No built-in resource managers exist yet... this file starts empty and grows...") directly contradicts the file contents: five resource managers are registered below (Nbtree, Hash, Flux, Recno, Fileops). The stale "starts empty" text must be removed/rewritten to match the actual list.
📄 src/include/access/undormgrlist.h (L42-L42)
"Phase 10" is internal project-phase jargon that does not belong in a committed source comment (it means nothing to a pgsql-hackers reader). Drop the phase reference.
💡 Suggested change
Before:
/* recno (Phase 10): resurrected RECNO in-place-MVCC table AM UNDO rmgr */
After:
/* recno: in-place-MVCC table AM UNDO rmgr */
📄 src/include/access/undostats.h (L19-L20)
fmgr.h is included but no fmgr types (Datum, PG_FUNCTION_ARGS, FunctionCallInfo) are used in this header -- the only V1 function is deliberately NOT declared here (see the note below). undolog.h already supplies the UndoRecPtr/UndoLogState types. Drop the unnecessary include.
💡 Suggested change
Before:
#include "access/undolog.h"
#include "fmgr.h"
After:
#include "access/undolog.h"
📄 src/include/access/undo_bufmgr.h (L195-L196)
The entire buffer read/tag/invalidation API declared in this header -- ReadUndoBuffer, ReadUndoBufferExtended, ReleaseUndoBuffer, UnlockReleaseUndoBuffer, MarkUndoBufferDirty, UndoMakeBufferTag, InvalidateUndoBuffers, InvalidateUndoBufferRange, plus UndoLogGetRelFileLocator/IsUndoRelFileLocator and the UndoRecPtrGetFileOffset/UndoLogicalToFileSize/UNDO_USABLE_BYTES_PER_PAGE macros -- has no callers anywhere outside undo_bufmgr.c itself (verified by search). The header itself states this is "retained only for the buffer invalidation API used during segment discard", but in the UNDO-in-WAL design segment discard no longer occurs, and even Invalidate* has zero callers. This is a whole header of dead scaffolding; per YAGNI it should be removed rather than shipped as an unused API surface.
📄 src/include/access/undo_bufmgr.h (L73-L76)
Aspirational future-tense comment plus internal-tracker jargon ("Task #5") for a constant that is defined but currently unused. The comment discipline flags future-tense ("reserved for future use", "when the smgr layer is extended") and task-tracker references in committed comments. Either wire up UNDO_FORKNUM or drop this note.
📄 src/backend/access/undo/perbackend/pbu_undoinsert.c (L503-L504)
The bounds assertion checks the wrong variable. The loop indexes xact_urec_info[idx].idx_undo_buffers[i] (line 461) using the loop counter i (incremented at line 501), and idx_undo_buffers[] has MAX_BUFFER_PER_UNDO entries. But this Assert checks idx, the function parameter (an xact_urec_info[] slot index, constant across the loop), so it never guards the actual array access and will never fire on overflow. In the recovery else branch the loop only breaks when InsertUndoRecord() returns true, so i can advance past MAX_BUFFER_PER_UNDO and read idx_undo_buffers[i] out of bounds undetected. Assert on the loop counter instead.
💡 Suggested change
Before:
Assert(idx < MAX_BUFFER_PER_UNDO);
} while (true);
After:
Assert(i < MAX_BUFFER_PER_UNDO);
} while (true);
📄 src/backend/access/undo/perbackend/pbu_undofile.c (L432-L433)
Off-by-one in the per-segment block count. md.c's mdwriteback computes the blocks remaining in the current segment as RELSEG_SIZE - (blocknum % RELSEG_SIZE) with no + 1. Here the extra 1 + makes nflush one too large: when blocknum % UNDOSEG_SIZE == 0 and nblocks > UNDOSEG_SIZE, nflush becomes UNDOSEG_SIZE + 1, so FileWriteback is asked to flush one block past the end of this segment file (a separate file) and blocknum += nflush skips the first block of the next segment, mis-aligning all subsequent iterations. Drop the 1 +.
💡 Suggested change
Before:
nflush = Min(nblocks,
1 + UNDOSEG_SIZE - (blocknum % UNDOSEG_SIZE));
After:
nflush = Min(nblocks,
UNDOSEG_SIZE - (blocknum % UNDOSEG_SIZE));
📄 src/backend/access/undo/perbackend/pbu_undofile.c (L557-L562)
Checkpointer sync of a missing/discarded undo segment throws instead of failing gracefully. undofile_syncfiletag runs in the checkpointer (InRecovery == false), so undofile_get_segment_file() -> undofile_open_segment_file(..., missing_ok = InRecovery = false) does elog(ERROR) on ENOENT rather than returning a negative File. The if (file <= 0) return -1; guard is therefore dead outside recovery, and the "errno set by undofile_get_segment_file()" comment is wrong. If a segment is unlinked by a concurrent discard between the SYNC request and the sync pass (SYNC_FORGET_REQUEST delivery is not guaranteed to have won the race), the checkpoint fails. md.c's mdsyncfiletag opens with EXTENSION_RETURN_NULL/DONT_OPEN and returns -1 for a missing file; this path should tolerate ENOENT the same way.
📄 src/backend/access/undo/perbackend/pbu_undoaction.c (L360-L360)
Integer overflow: maintenance_work_mem is an int in KB (up to INT_MAX KB ~= 2 TB). maintenance_work_mem * 1024L is computed as long (64-bit) but then truncated back into int undo_apply_size. When maintenance_work_mem exceeds ~2 GB worth of KB, the byte count overflows int32 and undo_apply_size becomes negative or wrong, which corrupts the total_size >= undo_apply_size batching decision below (either stopping immediately or never stopping). Use a 64-bit type (Size/int64) for undo_apply_size, matching how core code that does x * 1024L stores the result in a long.
💡 Suggested change
Before:
int undo_apply_size = maintenance_work_mem * 1024L;
After:
Size undo_apply_size = (Size) maintenance_work_mem * 1024L;
📄 src/backend/access/undo/perbackend/pbu_recovery.c (L342-L345)
Misleading type and name. MakeUndoRecPtr(logno, log->meta.insert) returns a full UndoRecPtr (log number in the high bits, offset in the low bits), yet it is cast to UndoLogOffset and stored in a variable named next_insert as if it were a bare offset. Because UndoLogOffset is also uint64 the cast truncates nothing, so next_insert actually holds the complete UndoRecPtr and the subsequent UndoRecPtrGetPageOffset()/-= UndoLogBlockHeaderSize/assignment to end_ptr all happen to be correct. But the label and cast invite a future reader to treat it as an offset (e.g. re-wrap with MakeUndoRecPtr), which would be a bug. Declare it as UndoRecPtr and drop the (UndoLogOffset) cast.
💡 Suggested change
Before:
UndoLogOffset next_insert;
next_insert = (UndoLogOffset) MakeUndoRecPtr(logno,
log->meta.insert);
After:
UndoRecPtr next_insert;
next_insert = MakeUndoRecPtr(logno, log->meta.insert);
📄 src/include/access/xactundo.h (L140-L140)
This GUC-backed extern int is referenced cross-module (the generated GUC table machinery in the backend, and potentially extensions). On Windows/MSVC an extern variable used from another translation unit must be declared PGDLLIMPORT or the reference fails to link. Every existing GUC extern int in installed headers (see xact.h, xlog.h, miscadmin.h) uses PGDLLIMPORT; this one must too. (high confidence)
💡 Suggested change
Before:
extern int undo_instant_abort_threshold;
After:
extern PGDLLIMPORT int undo_instant_abort_threshold;
📄 src/include/access/logical_revert_worker.h (L40-L41)
These GUC-backed extern int variables are read cross-module (GUC machinery, and extensions). On Windows/MSVC an extern variable referenced from another module needs PGDLLIMPORT or linking fails. Match the tree convention used by all other GUC declarations in installed headers (xlog.h, miscadmin.h, etc.). (high confidence)
💡 Suggested change
Before:
extern int logical_revert_naptime;
+extern int max_logical_revert_workers;
After:
extern PGDLLIMPORT int logical_revert_naptime;
extern PGDLLIMPORT int max_logical_revert_workers;
📄 src/include/access/xact.h (L415-L415)
SizeOfXactPrepare is defined here but has no callers anywhere in the tree (xactdesc.c and the redo/prepare paths all use sizeof(xl_xact_prepare)/MAXALIGN(sizeof(...)) directly). Dead, unused macro -- drop it to keep the change minimal, or convert the existing sizeof(xl_xact_prepare) uses to it if a named macro is intended. (moderate confidence)
📄 src/include/access/perbackend/pbu_compat.h (L149-L151)
These three tranche-ID macros are dead code. The engine actually uses the properly-registered tranche LWTRANCHE_UNDO_LOG_PBU (declared via PG_LWLOCKTRANCHE(UNDO_LOG_PBU, ...) in storage/lwlocklist.h and used at every LWLockInitialize() site in pbu_undolog.c); LWTRANCHE_UNDOLOG, LWTRANCHE_UNDODISCARD, and LWTRANCHE_DISCARD_UPDATE are never referenced anywhere in the tree. Beyond being unused scaffolding, the fixed offsets from LWTRANCHE_FIRST_USER_DEFINED are a collision hazard, and the comment is factually wrong: it claims the IDs are "Registered dynamically via LWLockRegisterTranche() in pbu_undolog.c", but LWLockRegisterTranche is never called. Remove these three macros and the comment.
📄 src/include/access/perbackend/pbu_compat.h (L198-L200)
#define RelFileNode RelFileLocator, #define ShmemVariableCache TransamVariables, #define GetOldestXmin(...), #define RBM_ZERO ..., #define target_prefetch_pages ..., #define ONCOMMIT_TEMP_DISCARD ... etc. are unscoped, object-like/function-like macros that textually rewrite common backend identifiers for every translation unit that includes this header. Today the blast radius is contained because pbu_compat.h is only #included directly by the pbu_.c files, but nothing enforces that: it is an installed header with a plain #ifndef PBU_COMPAT_H guard, so any future #include (or transitive pull-in) would silently rewrite unrelated code referencing RelFileNode/ShmemVariableCache/GetOldestXmin. This is a footgun; at minimum add a prominent "include only from pbu_.c, never from another header" warning, and prefer fixing the drift at the (few) call sites over global macro aliasing.
📄 src/include/access/perbackend/pbu_compat.h (L43-L45)
Reconstructing the 64-bit width modifier as "ll" is a portability/convention violation. int64 is not guaranteed to be long long on every platform the tree targets, and the project convention is to build format strings from INT64_FORMAT/UINT64_FORMAT (c.h) rather than hand-rolling a width modifier. UndoRecPtrFormat in pbu_undolog.h uses this as "%016" INT64_MODIFIER "X"; since it is printing an unsigned 64-bit value in hex, express it via the tree's 64-bit hex convention (e.g. reuse the pattern used by UINT64_FORMAT/X_UINT64_HEX_FORMAT) instead of INT64_MODIFIER "X".
📄 src/include/access/perbackend/pbu_undolog.h (L190-L191)
The do { ... } while (false) idiom carries a trailing semicolon inside the macro body. Call sites already terminate the invocation with their own ; (e.g. UndoRecPtrAssignRelFileNode(rnode, xact_urp); in pbu_undoinsert.c), so this expands to do{...}while(false);;, producing a stray empty statement. The standard PostgreSQL idiom omits the trailing ; from the macro so it behaves as a single statement and does not break inside an unbraced if/else.
💡 Suggested change
Before:
(rfn).relNumber = UndoRecPtrGetRelNode(urp); \
} while (false);
After:
(rfn).relNumber = UndoRecPtrGetRelNode(urp); \
} while (false)
📄 src/include/access/perbackend/pbu_undoaction_xlog.h (L3-L4)
Stale identity header. The banner names the file undoaction_xlog.h and the path comment reads src/include/access/undoaction_xlog.h, but the file is src/include/access/perbackend/pbu_undoaction_xlog.h. Also carries only the 2019 zheap copyright with no current-year GDG line. Fix the file name and path comment to match the actual location.
📄 src/include/access/perbackend/pbu_undodiscard.h (L3-L4)
Stale identity header: the banner names the file undoinsert.h and describes "undo discard definitions", and the path comment reads src/include/access/undodiscard.h, but the file is src/include/access/perbackend/pbu_undodiscard.h. Correct the file name and path.
📄 src/include/access/perbackend/pbu_undoinsert.h (L3-L4)
Stale identity header: banner says undoinsert.h and the path comment reads src/include/access/undoinsert.h, but the file is src/include/access/perbackend/pbu_undoinsert.h. Correct the file name and path comment.
📄 src/include/access/perbackend/pbu_undolog.h (L12-L12)
Stale path comment: reads src/include/access/undolog.h but the file is src/include/access/perbackend/pbu_undolog.h. Correct the path.
📄 src/include/access/perbackend/pbu_undolog_xlog.h (L3-L4)
Stale identity header: banner says undolog_xlog.h and the path comment reads src/include/access/undolog_xlog.h, but the file is src/include/access/perbackend/pbu_undolog_xlog.h. Correct the file name and path comment.
📄 src/include/access/perbackend/pbu_undorecord.h (L3-L4)
Stale identity header: banner says undorecord.h and the path comment reads src/include/access/undorecord.h, but the file is src/include/access/perbackend/pbu_undorecord.h. Correct the file name and path comment.
📄 src/include/access/perbackend/pbu_helpers.h (L43-L44)
"provided as a no-op for now" is aspirational/deferred-work wording, but the implementation is intentionally and permanently a no-op: pbu_helpers.c states it is "intentionally a no-op rather than deferred work" because master's elog machinery has no per-error client toggle. The header comment contradicts the implementation and violates the comment-accuracy rule against "for now"/future-tense wording. Drop "for now" and state that master has no equivalent so this is a permanent no-op.
📄 src/include/access/perbackend/pbu_undoinsert.h (L62-L64)
This prototype uses the type name RelFileNode, which no longer exists on master; it only compiles because pbu_compat.h textually aliases #define RelFileNode RelFileLocator. But this header does not include pbu_compat.h, so it silently depends on the includer having already pulled pbu_compat.h in first -- an ordering-dependent include contract that will break with a confusing "unknown type name 'RelFileNode'" error if this header is included on its own. Prefer using RelFileLocator directly in the public prototype (fixing the drift at the declaration) rather than relying on a global macro alias defined in a different header.
📄 src/include/access/perbackend/pbu_undorequest.h (L31-L34)
These module-global externs lack PGDLLIMPORT. Both are referenced across translation units (e.g. undo_rollback_overflow_size is defined in pbu_undorequest.c and read elsewhere), so on Windows/MSVC the symbols won't be exported from the backend and any cross-module/extension reference fails to link. The tree-wide convention places the marker right after extern (grep confirms extern PGDLLIMPORT int ... throughout src/include/access). [high]
💡 Suggested change
Before:
extern TransactionId latestRecentGlobalXmin;
/* GUC: rollback-request size threshold (defined in pbu_undorequest.c) */
extern int undo_rollback_overflow_size;
After:
extern PGDLLIMPORT TransactionId latestRecentGlobalXmin;
/* GUC: rollback-request size threshold (defined in pbu_undorequest.c) */
extern PGDLLIMPORT int undo_rollback_overflow_size;
📄 src/include/access/perbackend/pbu_undorequest.h (L33-L34)
undo_rollback_overflow_size is also declared in src/include/access/undolog.h:148. Duplicate extern declarations of the same GUC in two headers are a maintainability hazard: if the two ever diverge in type/qualifier (e.g. one gains PGDLLIMPORT and the other doesn't) the build breaks or the ODR/linkage intent becomes ambiguous. Keep a single canonical declaration and include that header here. [medium]
📄 src/include/access/perbackend/pbu_undorequest.h (L3-L8)
Stale/incorrect file header. The self-described name is undorequest.h and the path is src/include/access/undorequest.h, but the actual file is src/include/access/perbackend/pbu_undorequest.h. The copyright line reads 1996-2019; new files should carry the current year. This will draw objections on -hackers for identity/path drift. [medium]
📄 src/include/access/perbackend/pbu_undorequest.h (L141-L141)
Comment references a non-existent file. These functions are defined in pbu_undoaction.c, not undoaction.c. Fix the reference to avoid misleading readers. [medium]
💡 Suggested change
Before:
/* functions exposed from undoaction.c */
After:
/* functions exposed from pbu_undoaction.c */
📄 src/include/access/perbackend/pbu_undorequest.h (L48-L52)
Key-layout contract hazard. RollbackHashKey is a 16-byte key (FullTransactionId is { uint64 value; } = 8 bytes, plus 8-byte UndoRecPtr), and lookups pass a full RollbackHashKey. But the hash is created in pbu_undorequest.c:1036 with info.keysize = sizeof(TransactionId) (4 bytes) under HASH_BLOBS, so only the low 4 bytes of full_xid.value participate in the byte-for-byte key comparison and start_urec_ptr is entirely ignored. Distinct requests that share the low 32 bits of the xid (or differ only in start_urec_ptr) will collide/alias. Either the keysize must be sizeof(RollbackHashKey) or this struct's documented layout is wrong; as written the two disagree. [high]
📄 src/include/access/perbackend/pbu_undorequest.h (L114-L121)
Unsafe function-like macro. SetUndoRequestInfoFromRHEntry dereferences urinfo->/rh-> without parenthesizing the parameters, unlike ResetUndoRequestInfo just above which does parenthesize (urinfo). Passing anything other than a plain identifier (e.g. &arr[i]) will misparse. It also omits request_size, so it silently relies on every caller having run ResetUndoRequestInfo first (currently true in UndoGetWork, but fragile). Parenthesize the args for consistency and safety. [low]
💡 Suggested change
Before:
#define SetUndoRequestInfoFromRHEntry(urinfo, rh, cur_queue) \
( \
urinfo->full_xid = rh->full_xid, \
urinfo->start_urec_ptr = rh->start_urec_ptr, \
urinfo->end_urec_ptr = rh->end_urec_ptr, \
urinfo->dbid = rh->dbid, \
urinfo->undo_worker_queue = cur_queue \
)
After:
#define SetUndoRequestInfoFromRHEntry(urinfo, rh, cur_queue) \
( \
(urinfo)->full_xid = (rh)->full_xid, \
(urinfo)->start_urec_ptr = (rh)->start_urec_ptr, \
(urinfo)->end_urec_ptr = (rh)->end_urec_ptr, \
(urinfo)->dbid = (rh)->dbid, \
(urinfo)->undo_worker_queue = (cur_queue) \
)
📄 src/include/access/perbackend/pbu_undorequest.h (L28-L28)
InvalidUndoWorkerQueue -1 is stored into UndoWorkerQueueType fields (e.g. pbu_undoworker.c:435). The enum's only enumerators are non-negative (XID_QUEUE=0..ERROR_QUEUE=2), so the compiler may choose an unsigned underlying type, making the stored value implementation-defined. It happens to be safe today only because nothing compares the field back against InvalidUndoWorkerQueue. Prefer a signed sentinel that is a real enumerator, or store the queue index in a plain int. [low]
📄 src/include/access/perbackend/pbu_undoworker.h (L17-L18)
UndoWorkerDelay is declared here but has no definition anywhere in the tree and no reader (grep finds only this declaration). Dead scaffolding: either wire up the GUC and its backing variable, or drop the declaration. Also, if it is meant to be a real cross-module GUC, it must be extern PGDLLIMPORT int for the MSVC/extension build. [medium]
📄 src/include/access/perbackend/pbu_undoworker.h (L3-L9)
Stale/incorrect file header: self-described name undoworker.h and path src/include/access/undoworker.h do not match the actual src/include/access/perbackend/pbu_undoworker.h, and the copyright 1996-2019 is stale for a newly added file. [medium]
📄 src/include/access/perbackend/pbu_undoworker.h (L13-L20)
This header is not self-contained: it uses Size, Datum, Oid, and pg_noreturn but includes no headers. PostgreSQL headers must compile stand-alone (enforced by headerscheck/cpluspluscheck). The analogous src/include/access/undoworker.h includes fmgr.h (Datum) and storage/shmem.h (Size). Add the required includes so the file is self-sufficient. [moderate confidence]
💡 Suggested change
Before:
#ifndef PBU_UNDOWORKER_H
#define PBU_UNDOWORKER_H
/* GUC options */
/* undo worker sleep time between rounds */
extern int UndoWorkerDelay;
extern Size UndoLauncherShmemSize(void);
After:
#ifndef PBU_UNDOWORKER_H
#define PBU_UNDOWORKER_H
#include "fmgr.h"
#include "postgres_ext.h"
#include "storage/shmem.h"
/* GUC options */
/* undo worker sleep time between rounds */
extern int UndoWorkerDelay;
extern Size UndoLauncherShmemSize(void);
📄 src/include/access/perbackend/pbu_undorequest.h (L30-L31)
Dead scaffolding: latestRecentGlobalXmin is defined in pbu_undorequest.c and declared here, but is never read or written anywhere in the tree (grep finds only the definition and this declaration). Drop the declaration and its backing variable. If it is intended to be a real cross-module symbol, it additionally needs PGDLLIMPORT for Windows/MSVC. [high confidence]
📄 src/include/access/perbackend/pbu_discardworker.h (L17-L22)
This header is not self-contained: it references Datum (from fmgr.h/postgres.h) and pg_noreturn (from c.h) but has no #include directives, so it only compiles when the includer happens to have pulled those in first. PostgreSQL headers are expected to be independently includable, and the sibling worker headers already follow this: access/undoworker.h includes fmgr.h and access/logical_revert_worker.h includes postgres.h. Add #include "postgres.h" (or at minimum #include "fmgr.h") here. (moderate confidence)
💡 Suggested change
Before:
#ifndef PBU_DISCARDWORKER_H
#define PBU_DISCARDWORKER_H
extern void DiscardWorkerRegister(void);
pg_noreturn extern void DiscardWorkerMain(Datum main_arg);
extern bool IsDiscardProcess(void);
After:
#ifndef PBU_DISCARDWORKER_H
#define PBU_DISCARDWORKER_H
#include "postgres.h"
extern void DiscardWorkerRegister(void);
pg_noreturn extern void DiscardWorkerMain(Datum main_arg);
extern bool IsDiscardProcess(void);
📄 src/include/access/slog.h (L239-L239)
These extern globals are declared in an installed header that other backend modules and out-of-tree AMs/extensions include (e.g. the flux AM, guc_tables.c). On Windows/MSVC, any extern variable referenced from a dynamically-loaded module must be marked PGDLLIMPORT or the load/link fails. Per PostgreSQL convention, extern globals exposed in installed headers should carry PGDLLIMPORT. Apply to slog_dsa_max_size_mb, slog_num_partitions, and (in slog_flathash.h) SLogNumPartitions, and (in slog_internal.h) SLogState.
💡 Suggested change
Before:
extern int slog_dsa_max_size_mb;
After:
extern PGDLLIMPORT int slog_dsa_max_size_mb;
📄 src/include/access/slog.h (L238-L239)
slog_dsa_max_size_mb is documented as a "GUC" here (and slog_num_partitions below), but neither is ever registered via guc_tables.c or DefineCustomIntVariable anywhere in the tree. They are plain globals that a user cannot set, so the documented auto-sizing/tuning knobs do not actually exist. Either register them as real GUCs or drop the "GUC" wording and the runtime knob framing from the comments.
📄 src/include/access/slog.h (L185-L186)
SLogAmDescriptor and SLogRegisterAmDescriptor() are exported here as a per-AM opt-in registration API, but SLogRegisterAmDescriptor() has zero call sites anywhere in the tree (no AM, including flux/recno, registers a descriptor). Consequently before_image_max stays 0 forever and the entire before-image path (SLogTupleStoreBeforeImage/GetBeforeImage) is dead. This is speculative scaffolding (YAGNI): a struct + registration API wrapping a single scalar that no consumer sets. Remove the descriptor/registration machinery until a caller actually needs it, or wire up the intended consumer.
📄 src/include/access/slog_flathash.h (L61-L62)
Each SLogFlatBucket embeds a full SLogTupleEntry by value, which itself embeds SLogTupleOp ops[SLOG_MAX_TUPLE_OPS] (128 slots). SLogTupleOp is ~40 bytes (int64 commit_ts forces 8-byte alignment), so each bucket is ~5 KB. With the flat-hash capacity clamped up to 2*1048576 buckets (see SLogFlatHashCapacity), the tuple hash can demand on the order of 10 GB of shared memory. This is a large, hard-to-predict shmem footprint driven by a compile-time constant. Consider whether 128 embedded op slots per TID is necessary, or store ops out-of-line, to bound worst-case shmem.
📄 src/include/access/slog_internal.h (L56-L56)
This comment is inaccurate. In SLogShmemInit() the DSA is attached, the ATM tree is created, then SLogState->dsa_area is explicitly detached and reset to NULL at the end of init; each backend re-attaches lazily through a separate per-backend handle (slog_dsa_handle), not through this shared field. So in normal operation this field is NULL, not "set during SLogShmemInit". Update the comment to describe the actual lifecycle (transiently set during init, reset to NULL; backends attach via their own handle).
📄 src/include/access/slog_flathash.h (L188-L191)
These macros acquire/release an LWLock (writer_lock LW_SHARED) across a caller-supplied block, and the block cannot return/goto/elog out or the lock leaks. This is a maintenance footgun: a future edit that adds an ereport, elog, or early return inside the block (or a callee that raises an error) leaks the shared LWLock, since there is no PG_TRY/ResourceOwner-based release tied to the macro. Current call sites are disciplined (locals-only memcpy), but the pattern is fragile. Prefer explicit LWLockAcquire/Release with the body factored into a helper, or document a hard rule and add an Assert-based guard.
📄 src/include/access/slog.h (L132-L136)
This SLogTupleKey design relies on struct padding participating in byte-wise hashing/comparison (SLogFlatHashComputeHash hashes sizeof(SLogTupleKey) raw bytes, and probe/insert use memcmp over the whole struct). This is a correctness footgun: any construction site that forgets memset(&key,0,sizeof(key)) yields lookups that miss or create duplicate entries, depending on uninitialized stack padding. Consider hashing/comparing only the meaningful fields (relid + ItemPointer) via a dedicated key routine, or pack the struct to eliminate padding, so correctness does not hinge on every caller remembering to zero the struct.
📄 src/include/access/slog_internal.h (L52-L52)
tuple_partitions is allocated with ShmemAlloc() in SLogTupleShmemInit(), not palloc(). Calling it "palloc'd" is misleading for a shared-memory structure. Suggest: /* ShmemAlloc'd array (in shmem) */.
📄 src/include/access/slog_internal.h (L50-L50)
Same phantom-GUC issue as slog.h: this comment states the partition count is "determined at startup by slog_num_partitions GUC", but slog_num_partitions is never registered as a GUC anywhere in the tree. Reconcile the comment with reality (it is a plain global defaulting to auto-size) or actually register the GUC.
📄 src/include/access/slog_flathash.h (L157-L161)
Header comment describes a "per-partition seqlock" / "seqlock reader/writer scheme", but the same block and the SLogSeqWriteBegin/End no-ops make clear the seqlock was replaced by plain LW_SHARED/LW_EXCLUSIVE locking. Retaining the seqlock naming (SLOG_SEQ_READ_BEGIN, SLogSeqWrite*, "seqvar") for a mechanism that is no longer a seqlock is misleading. Consider renaming to reflect the LWLock reality, or at least remove the residual seqlock terminology to avoid confusing future readers.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L54-L60)
These frontend-copy structs do not match the actual on-disk WAL record layout, so pg_waldump prints garbage. The real INSERT/DELETE records (xl_flux_insert / xl_flux_delete in flux_xlog.h) are laid out as offnum(u16), flags(u16), tuple_len(u32), commit_ts(u64) -- there is NO xact_ts field, and tuple_len sits where these copies expect the start of commit_ts. The WAL emit path (flux_xlog.c:300 XLogRegisterData(&xlrec, sizeof(xl_flux_insert))) writes the header layout, so reading it back as {offnum, flags, commit_ts(u64), xact_ts(u64)} misinterprets tuple_len + padding as commit_ts and the real commit_ts as a bogus xact_ts. The UPDATE copy is likewise wrong (missing old_tuple_len/new_tuple_len). Note flux_xlog.h already exposes FRONTEND-safe copies of these structs under #ifdef FRONTEND; reuse those (include the header) instead of maintaining a third, divergent, hand-kept copy. Confidence: high.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L28-L30)
Duplicating the XLOG_FLUX_* opcodes with a "keep in sync with flux_xlog.h" comment is a footgun: flux_xlog.h is a frontend-safe header (guarded by #ifndef FRONTEND only around the backend structs/prototypes) and already defines these opcodes plus FRONTEND-safe record structs. Include flux_xlog.h and drop the local copies so an opcode/struct change cannot silently diverge and cause misparsing in pg_waldump. Confidence: high.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L379-L381)
Parsing the CAS_UPDATE record by hardcoded byte offsets (data+4, data+6) and a magic datalen >= 14 ("- 2 padding") bakes in an assumption about struct padding/field placement of xl_flux_cas_update. This is fragile and hostile to portability (padding/alignment can differ). Prefer parsing via the documented field positions (or a shared frontend struct) rather than raw offsets and a hand-computed minimum size. At minimum, replace the numeric 14 with a named size derived from the field offsets. Confidence: moderate.
📄 src/backend/access/rmgrdesc/undodesc.c (L76-L84)
The op-name mapping is wrong for codes 0x0002-0x0004. xl_undo_apply.operation_type is set to the RM's undo subtype (xlrec.operation_type = info; in flux_undo.c:152 and recno_undo.c:131), and those subtypes are: 0x0001=INSERT, 0x0002=UPDATE, 0x0003=DELETE, 0x0004=UPDATE_DELTA (see flux_undo.h / recno_undo.h). This switch labels 0x0002 as "DELETE" and 0x0003 as "UPDATE" (swapped) and 0x0004 as "PRUNE", so pg_waldump reports the wrong operation. Map these to the canonical FLUX_UNDO_/RECNO_UNDO_ meanings (or reference the shared constants) instead of hardcoded, misordered literals. Confidence: high.
📄 src/backend/access/rmgrdesc/undodesc.c (L37-L41)
UndoRecPtr is uint64 (undodefs.h), new_size is uint64. Project convention formats 64-bit types with UINT64_FORMAT rather than "%llu" + (unsigned long long) casts (compare undolog_desc in this same patch, which uses UndoLogOffsetFormat). Use UINT64_FORMAT for start_ptr/discard_ptr/new_size/urec_ptr/old_seal_ptr. Confidence: moderate.
📄 src/backend/access/rmgrdesc/pbu_undoactiondesc.c (L36-L37)
urec_ptr is UndoRecPtr (uint64). Use UINT64_FORMAT rather than "%016llX" + (unsigned long long); the project convention for 64-bit values is UINT64_FORMAT (e.g. undologdesc.c in this same patch uses UndoLogOffsetFormat). If a fixed-width hex form is desired, prefer "%016" INT64_MODIFIER "X". Confidence: moderate.
📄 src/backend/access/rmgrdesc/Makefile (L12-L20)
The OBJS list was previously alphabetized; these insertions break it. atmdesc.o/fileopsdesc.o/fluxdesc.o/recnodesc.o are placed before genericdesc.o (out of order: after dbasedesc.o the alphabetical successor is genericdesc.o), and pbu_undoactiondesc.o/pbu_undologdesc.o/undodesc.o are appended after tblspcdesc.o instead of in alphabetical position. Note meson.build mirrors this same non-alphabetical placement, so the two build systems are at least consistent, but restoring alphabetical order in both keeps the diff minimal and matches the file's existing convention. Confidence: high.
| flux_pbu.o \ | ||
| flux_pvs.o | ||
|
|
||
| include $(top_srcdir)/src/backend/common.mk No newline at end of file |
There was a problem hiding this comment.
Missing trailing newline at end of file. Every other backend access Makefile (e.g. gin, gist, nbtree) ends with a newline after the final include line, and a missing final newline trips git diff --check conventions/POLA for text files. Add a trailing newline.
| include $(top_srcdir)/src/backend/common.mk | |
| +include $(top_srcdir)/src/backend/common.mk |
| OBJS = \ | ||
| flux_dirtymap.o \ | ||
| flux_handler.o \ | ||
| flux_tuple.o \ | ||
| flux_mvcc.o \ | ||
| flux_stats.o \ | ||
| flux_xlog.o \ | ||
| flux_fsm.o \ | ||
| flux_operations.o \ | ||
| flux_lock.o \ | ||
| flux_slot.o \ | ||
| flux_vm.o \ | ||
| flux_undo.o \ | ||
| flux_pbu.o \ | ||
| flux_pvs.o |
There was a problem hiding this comment.
OBJS ordering is not alphabetical and does not match the sibling meson.build file list (meson orders flux_dirtymap, flux_fsm, flux_handler, flux_operations, flux_tuple, flux_mvcc, ... while the Makefile has flux_dirtymap, flux_handler, flux_tuple, flux_mvcc, flux_stats, flux_xlog, flux_fsm, flux_operations, ...). Existing access Makefiles (e.g. gin) keep OBJS alphabetically sorted. Sort OBJS alphabetically so the Makefile and meson.build read consistently and stay easy to keep in sync. Low-severity style nit.
| OBJS = \ | |
| flux_dirtymap.o \ | |
| flux_handler.o \ | |
| flux_tuple.o \ | |
| flux_mvcc.o \ | |
| flux_stats.o \ | |
| flux_xlog.o \ | |
| flux_fsm.o \ | |
| flux_operations.o \ | |
| flux_lock.o \ | |
| flux_slot.o \ | |
| flux_vm.o \ | |
| flux_undo.o \ | |
| flux_pbu.o \ | |
| flux_pvs.o | |
| +OBJS = \ | |
| + flux_dirtymap.o \ | |
| + flux_fsm.o \ | |
| + flux_handler.o \ | |
| + flux_lock.o \ | |
| + flux_mvcc.o \ | |
| + flux_operations.o \ | |
| + flux_pbu.o \ | |
| + flux_pvs.o \ | |
| + flux_slot.o \ | |
| + flux_stats.o \ | |
| + flux_tuple.o \ | |
| + flux_undo.o \ | |
| + flux_vm.o \ | |
| + flux_xlog.o |
| (github.event_name == 'issue_comment' && github.event.issue.pull_request && | ||
| (startsWith(github.event.comment.body, '/open-code-review') || | ||
| startsWith(github.event.comment.body, '@open-code-review') || | ||
| startsWith(github.event.comment.body, '/pg-history'))) |
There was a problem hiding this comment.
The issue_comment trigger invokes the reusable review workflow whenever any PR comment starts with /open-code-review, @open-code-review, or /pg-history. Comment bodies are attacker-controllable and the commenter need not be a trusted collaborator, yet the called workflow is handed vars.AWS_ROLE_ARN and assumes an OIDC role that spends on Bedrock. This stub applies no author-association / permission gate, so any user can trigger Bedrock spend by commenting. Confirm the called gburd/ci-workflows workflow enforces an actor-permission gate (e.g. github.event.comment.author_association in ('OWNER','MEMBER','COLLABORATOR')); if it does not, add a gate here. (high confidence the gate is absent from this stub; cannot verify the external workflow.)
| (startsWith(github.event.comment.body, '/open-code-review') || | ||
| startsWith(github.event.comment.body, '@open-code-review') || | ||
| startsWith(github.event.comment.body, '/pg-history'))) | ||
| uses: gburd/ci-workflows/.github/workflows/ocr-review.yml@v1 |
There was a problem hiding this comment.
Third-party reusable workflow is pinned to the mutable tag @v1. A tag can be repointed at malicious code by the owner or an attacker who compromises gburd/ci-workflows, and this workflow hands the callee an OIDC role. For third-party reusable workflows, pin to a full commit SHA (optionally with a # v1 comment) rather than a floating tag.
| # Needs repo variables AWS_ROLE_ARN, AWS_REGION, OCR_BEDROCK_MODEL. Auth is | ||
| # GitHub OIDC; no static AWS keys. See the ci-workflows README. | ||
|
|
||
| name: OCR AI Review |
There was a problem hiding this comment.
No top-level permissions: block is declared here (unlike fork-sync-upstream.yml and pg-ci.yml, which both set permissions: contents: read). The default GITHUB_TOKEN scope is therefore inherited and passed to the reusable workflow, which is broader than least-privilege for an AI reviewer. Declare an explicit least-privilege scope (e.g. contents: read, pull-requests: write if it posts review comments) unless the called workflow already constrains its own permissions.
| bool | ||
| FluxHoldsTupleLock(Relation rel, ItemPointer tid, LockTupleMode mode) |
There was a problem hiding this comment.
Dead code (YAGNI). FluxHoldsTupleLock, FluxLockPage, FluxUnlockPage, FluxLockMultipleTuples, and FluxLockRelationForDDL have no callers anywhere in the tree (verified by search: only the definitions here plus prototypes in flux.h and redundant externs in flux_operations.c). PostgreSQL rejects speculative scaffolding; remove these five functions and their prototypes. FluxLockRelationForDDL is additionally just a one-line wrapper around LockRelationOid(RelationGetRelid(rel), lockmode) -- needless indirection even if it did have a caller. Confidence: high.
| /* Acquire locks in sorted order */ | ||
| for (i = 0; i < ntids; i++) | ||
| { | ||
| bool have_lock; | ||
|
|
||
| if (!FluxLockTuple(rel, &tids[i], mode, wait, &have_lock)) |
There was a problem hiding this comment.
When wait == true, FluxLockTuple always returns true (it calls LockAcquire(..., dontWait=false) and unconditionally sets result = true), so all_locked can never become false and the entire rollback branch below is dead for the common wait=true case. Also, the header comment claims the TID sort "ensures all callers acquire locks in the same global order" and prevents deadlocks, but the sort only orders TIDs within a single relation; cross-relation lock ordering is not addressed, so the deadlock-freedom claim is overstated. Combined with the fact that this function has no callers, prefer removing it. Confidence: high.
|
|
||
| if (flux_serlock_n >= flux_serlock_cap) | ||
| { | ||
| int newcap = flux_serlock_cap * 2; |
There was a problem hiding this comment.
newcap = flux_serlock_cap * 2 has no overflow guard or upper bound, and the dedup scan in FluxTrackSerializeLock is O(n) per insert (O(n^2) for the transaction). A bulk UPDATE touching many distinct rows accumulates one long-lived entry per row in TopMemoryContext and pays quadratic dedup cost. If the expected cardinality is small this is acceptable, but the growth is unbounded; consider a hash set or at least documenting/bounding the expected size. Confidence: moderate.
| * This implements proper locking for FLUX operations to ensure | ||
| * data consistency under concurrent access. Uses both buffer locks | ||
| * and tuple-level locks with deadlock detection. |
There was a problem hiding this comment.
Misleading file-header comment: the NOTES claim this file uses "both buffer locks and tuple-level locks with deadlock detection." This file implements neither buffer locks nor any deadlock detection -- it only wraps the heavyweight lock manager (LockAcquire/LockRelease with LOCKTAG_TUPLE/LOCKTAG_PAGE) and relies on the lock manager's own deadlock detector. Comments must describe what the code does now. Confidence: high.
| scan->rs_vistuples = (OffsetNumber *) | ||
| MemoryContextAlloc(TopMemoryContext, | ||
| MaxOffsetNumber * sizeof(OffsetNumber)); |
There was a problem hiding this comment.
rs_vistuples is allocated in TopMemoryContext (process-lifetime) but the only pointer to it lives in the FluxScanDescData, which is palloc0'd in the caller's (query) context and freed by context reset on error. flux_scan_end() (the only place that pfrees this array) is not guaranteed to run when a scan errors out between allocation and end — transaction abort resets the query context, dropping the scan struct, but the TopMemoryContext block leaks permanently. Repeated scan errors accumulate ~MaxOffsetNumber * sizeof(OffsetNumber) (~4 KB) leaks for the process lifetime.
Heap avoids this entirely by embedding OffsetNumber rs_vistuples[MaxHeapTuplesPerPage] inline in the scan descriptor (see heapam.h). Recommend embedding the array inline in FluxScanDescData (sized MaxFluxItemsPerPage), or at minimum allocating it in a per-scan/query context tied to a ResourceOwner rather than TopMemoryContext. The same allocation appears in the bitmap-scan path and should be fixed too.
There was a problem hiding this comment.
🔍 OCR found 273 issue(s).
- 25 inline, 248 in summary (inline capped at 25)
⚠️ 2 warning(s) during review
📄 src/backend/access/flux/flux_lock.c (L162-L170)
2PC footgun: the backend-local tracking set (flux_serlocks) is a static that persists across transactions in a backend, but it is only cleared on COMMIT (via FluxReleaseSerializeLocks) and ABORT (FluxForgetSerializeLocks). I verified FluxXactCallback in flux_mvcc.c: XACT_EVENT_PREPARE only sets MyFluxXactState = NULL and does NOT clear flux_serlocks. At PREPARE, PostPrepare_Locks() transfers the LOCKTAG_TUPLE lock to the prepared transaction's dummy proc and removes this backend's LOCALLOCK entries. The stale flux_serlocks entries then leak into the next transaction run by this backend. Because FluxTrackSerializeLock dedups against the stale entries, a later in-place update of the same (dbid,rel,blk,off) in a subsequent transaction is silently skipped ("already tracked"), so its real lock is never recorded. Correctness today is only rescued by the LockHeldByMe-guarded drain in FluxReleaseSerializeLocks, which is fragile and non-obvious. The set must be reset at PREPARE (and arguably defensively at transaction start) rather than relying on the drain loop to paper over stale state. At minimum, add a FluxForgetSerializeLocks() call to the XACT_EVENT_PREPARE path.
📄 src/backend/access/flux/flux_lock.c (L151-L152)
The drain while (LockHeldByMe(&tag, ExclusiveLock, false)) LockRelease(...) unconditionally releases ALL ExclusiveLock references held by this backend on that tuple tag. LockTupleNoKeyExclusive maps to ExclusiveLock (confirmed against heapam.c tupleLockExtraInfo), so if the same transaction also holds a legitimate SELECT ... FOR NO KEY UPDATE row lock on that exact row for an unrelated reason, this loop drops it too, dropping a lock the transaction may still need. The header comment claims only the serialization lock uses this tag, but nothing enforces that. Consider releasing exactly the number of refs this module acquired (track a per-row acquire count) instead of draining to zero.
📄 src/backend/access/flux/flux_lock.c (L478-L487)
FluxHoldsTupleLock uses a lock-mode mapping that is incompatible with FluxLockTuple/FluxUnlockTuple. Here KeyShare/Share collapse to ShareLock and NoKeyExclusive/Exclusive collapse to ExclusiveLock, but the acquire path maps the four tuple modes to four distinct LOCKMODEs (AccessShareLock / RowShareLock / ExclusiveLock / AccessExclusiveLock, matching heap). As a result this held-check would miss locks actually held (e.g. a Share lock is held as RowShareLock but checked as ShareLock; an Exclusive lock is held as AccessExclusiveLock but checked as ExclusiveLock), returning false negatives. I confirmed FluxHoldsTupleLock currently has zero callers in the entire tree, so this is latent dead code -- but if wired up it is a correctness bug. Either delete it (preferred, YAGNI) or fix the mapping to match FluxLockTuple.
📄 src/backend/access/flux/flux_lock.c (L387-L390)
Dead code (YAGNI). FluxLockMultipleTuples, FluxLockPage, FluxUnlockPage, FluxLockRelationForDDL, and FluxHoldsTupleLock have zero callers anywhere in src/ (verified). Per the minimalism discipline these speculative helpers should be removed along with their flux.h declarations (and the redundant forward externs in flux_operations.c). Note additionally that FluxLockMultipleTuples ships an O(n^2) bubble sort plus a per-call palloc0 and sorts the caller's tids array in place -- a footgun for any future caller that correlates tids with a parallel results array -- which is further reason not to keep it unused. FluxLockRelationForDDL is just a one-line wrapper over LockRelationOid that adds nothing.
📄 src/backend/access/flux/flux_lock.c (L76-L78)
The header comments of the two cooperating functions contradict each other on the refcount lifecycle. FluxTrackSerializeLock claims (lines 71-74) that it releases "exactly one ref per distinct row" and that "the extra refs release naturally at LockReleaseAll ... so we only track and release the first." But FluxReleaseSerializeLocks does while (LockHeldByMe(...)) LockRelease(...) and justifies it (lines 145-149) with the opposite claim: that leftover refs do NOT drain at LockReleaseAll but instead "trip the held-at-commit assertion." Both cannot be true. Reconcile these comments to match the actual code (the while loop drains all refs), since the stale "release the first" wording describes behavior the code no longer implements and misleads future maintainers about the held-at-commit invariant.
📄 src/backend/access/flux/flux_handler.c (L3877-L3880)
snapshotConflictHorizon is initialized to InvalidTransactionId and never updated, so this always returns InvalidTransactionId. The nbtree caller _bt_delitems_delete_check passes this value straight into the XLOG_BTREE_DELETE record's xlrec_delete.snapshotConflictHorizon (nbtpage.c _bt_delitems_delete), and only forces it to Invalid itself when !XLogStandbyInfoActive(). On a hot standby, btree_xlog_delete uses this horizon via ResolveRecoveryConflictWithSnapshot to cancel standby queries whose snapshots can still see the index entries being physically removed. Returning InvalidTransactionId suppresses that conflict entirely, so a standby query holding an older snapshot can keep using index entries that have been removed on the standby -> wrong results / dangling TIDs. heap_index_delete_tuples computes the newest-xmax horizon across the deleted tuples; FLUX must do the same (e.g. advance the horizon with FluxTupleGetXmax(tuple_hdr) for each tuple marked knowndeletable). This is a WAL/replica-consistency defect, not merely an optimization.
📄 src/backend/access/flux/flux_handler.c (L3083-L3089)
The primary stamps xmax with FluxTupleSetXmax(tuple_hdr, abort_xid), which writes only the LOW 32 bits of the t_commit_ts word and preserves the high 32 bits. But the WAL body sets xlrec.commit_ts = (uint64) abort_xid and the delete redo (flux_xlog_delete_redo) applies it as a FULL 64-bit store tuple->t_commit_ts = xlrec->commit_ts, which zeroes the high 32 bits. This is only consistent because the high bits of t_commit_ts happen to be 0 for speculative tuples (and are documented as 'reserved'). The primary and redo use two different field-access conventions for the same word, so any future use of the high bits silently diverges primary vs standby and trips wal_consistency_checking. Make the redo use FluxTupleSetXmax(tuple, (TransactionId) xlrec->commit_ts) so both sides touch the same bits, or document the invariant at the redo site.
📄 src/backend/access/flux/flux_handler.c (L4837-L4837)
~9%% has a stray doubled percent sign (this is a plain C comment, not a printf format), and should read ~9%. Separately, this file pervasively uses non-ASCII em-dashes and arrow glyphs in comments; the project requires ASCII-only in source. git diff --check / pgindent conventions and the ASCII-only gate must be satisfied before list submission.
💡 Suggested change
Before:
* which showed up as ~9%% of CPU under high-concurrency pgbench. A stale
After:
* which showed up as ~9% of CPU under high-concurrency pgbench. A stale
📄 src/backend/access/flux/flux_handler.c (L493-L495)
rs_vistuples is allocated in TopMemoryContext (both here and in the bitmap path) but is only freed in flux_scan_end. Any scan abandoned without reaching scan_end (error longjmp past table_endscan, or a scan descriptor leaked on an aborted plan) leaks a MaxOffsetNumber*2-byte block for the backend's lifetime, and repeated short scans in a long-lived backend accumulate. heap allocates its equivalent rs_vistuples inline in the scan descriptor (fixed array), avoiding a separate allocation entirely. Consider embedding the array in FluxScanDescData, or at minimum allocating it in a scan-scoped context that is reset on error.
📄 src/backend/access/flux/flux_mvcc.c (L760-L768)
Dead shared state plus a needless exclusive LWLock on the serializable-commit hot path.
serializable_horizon is initialized to 1 and only ever updated here via Min(serializable_horizon, xact_commit_ts). Since xact_commit_ts is a wall-clock microsecond value (always >> 1), the Min() keeps the field pinned at 1 forever. Nothing in the FLUX tree ever reads serializable_horizon (grep confirms writes only). mvcc_lock is likewise acquired only here and never for any reader, so this LW_EXCLUSIVE acquisition on every serializable commit buys nothing but contention.
SSI is already handled entirely by predicate.c (per the file header). Remove serializable_horizon, mvcc_lock, and this whole block; also drop the matching init lines (294-296 / 402-405) and the mvcc_lock field. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L278-L286)
FluxMvccShmemInit() is dead code. Shmem for this subsystem is set up exclusively through the PG_SHMEM_SUBSYSTEM(FluxMvccShmemCallbacks) entry in subsystemlist.h, which invokes FluxMvccShmemRequest/FluxMvccShmemInit_cb. This classic-shmem variant has no callers anywhere in src/backend (only a leftover .gdbinit breakpoint references it) and duplicates the init/on_shmem_exit logic in FluxMvccShmemInit_cb. If it were ever wired alongside the callback path it would double-register FluxShmemExit and re-run ShmemInitStruct. Remove this function (and its extern in flux.h) to leave a single init path. (high confidence)
📄 src/backend/access/flux/flux_mvcc.c (L584-L588)
The two mirrored slot-release functions use divergent cache-invalidation predicates: FluxCleanupTransactionState() uses strict equality (my_ts == oldest_active_ts), while FluxResolvePreparedSlot() uses slot_ts <= oldest_active_ts. The strict-equality reasoning here is actually sound (oldest_active_ts is a minimum, so a slot ts strictly below it cannot be the real oldest), but having the prepared-slot resolver fall back to <= for the identical bookkeeping is inconsistent and will invite confusion / divergent maintenance. Pick one predicate and apply it in both places. (moderate confidence)
📄 src/backend/access/flux/flux_mvcc.c (L1160-L1162)
Durability hazard: FLUX sets the committed hint bit (FLUX_TUPLE_XMIN_COMMITTED / FLUX_TUPLE_XMAX_COMMITTED) without the commit-LSN flush interlock that heap enforces in SetHintBitsExt(). FLUX uses heap-compatible XIDs/CLOG, and PostgreSQL supports asynchronous commit (synchronous_commit=off), so a transaction's commit WAL record may not be flushed when a page dirtied by this hint is written out. After a crash the page survives carrying FLUX_TUPLE_XMIN_COMMITTED while CLOG has no record of the commit -> the tuple appears committed when it was actually lost (and symmetrically a wrongly-visible deleter via XMAX_COMMITTED). The comment's claim "a torn/lost hint is harmless" is false for the committed case: a lost hint is harmless, but a surviving committed hint for a transaction whose commit record was never flushed is a correctness bug. Heap guards exactly this with TransactionIdGetCommitLSN(xid) + XLogNeedsFlush(commitLSN) && BufferGetLSNAtomic(buffer) < commitLSN before setting a committed hint on a permanent buffer. FLUX must apply the same interlock (gate committed-hint writes on BufferIsPermanent + commitLSN flush), or restrict hints to aborted/unlogged cases.
📄 src/backend/access/flux/flux_mvcc.c (L854-L857)
Stale/contradictory comment. This claims FluxGetOldestActiveTimestamp is "the FLUX analog of GetOldestNonRemovableTransactionId" and that VACUUM uses it to decide which deleted tuples can be reclaimed ("a deleted tuple whose commit timestamp is older than this value ... can be reclaimed"). That no longer describes the code: reclamation is now XID-driven via FluxGetOldestXminHorizon()/FluxTupleDeadToAll(), and the shmem struct comment above explicitly states oldest_active_ts is "NOT a vacuum horizon." Leaving this comment is a footgun -- it invites someone to use this wall-clock timestamp as a pruning horizon, which is not a correctness horizon. Update the comment to describe the actual consumers (defrag WAL record / VM all-visible hint).
📄 src/backend/access/flux/flux_handler.c (L3831-L3836)
RelationCreateStorage() already WAL-logs the main-fork creation (log_smgrcreate(..., MAIN_FORKNUM) for needs_wal/RELPERSISTENCE_PERMANENT; see catalog/storage.c). This extra log_smgrcreate(newrlocator, MAIN_FORKNUM) emits a second, redundant XLOG_SMGR_CREATE record for the same fork on every permanent-relation rewrite. heapam_relation_set_new_filelocator() does not do this. Drop these two lines.
💡 Suggested change
Before:
/* Create the storage file (empty, no blocks yet) */
srel = RelationCreateStorage(*newrlocator, persistence, true);
/* WAL-log the file creation */
if (persistence == RELPERSISTENCE_PERMANENT)
log_smgrcreate(newrlocator, MAIN_FORKNUM);
After:
/* Create the storage file (empty, no blocks yet) */
srel = RelationCreateStorage(*newrlocator, persistence, true);
📄 src/backend/access/flux/flux_handler.c (L2875-L2877)
Unlike every other error path in this function (and in flux_tuple_insert), this fresh-page failure throws without first releasing the overflow buffers (still EXCLUSIVE-locked and pinned) or freeing tuple. Transaction abort will reclaim them via ResourceOwner/MemoryContext, so this is not a hard leak, but it breaks the explicit cleanup pattern established just above and is easy to copy incorrectly. Clean up before erroring for consistency.
📄 src/backend/access/flux/flux_operations.c (L583-L589)
Return-type/contract mismatch. FluxPagePruneOpt is declared int and documented "Returns the number of tuples pruned", but the success path returns true (==1) rather than the actual ndead count, while every early-exit returns 0. All callers (here in flux_tuple_insert and in flux_handler.c) use it strictly as a boolean. Either change the declared return type to bool (and the header prototype), or return ndead consistently so the documented count contract holds. As written the "number of tuples pruned" doc is false and will mislead future callers.
📄 src/backend/access/flux/flux_operations.c (L741-L746)
Stale cached target block is not validated against the current relation size before ReadBuffer. RelationGetTargetBlock returns a relcache hint that can point past EOF if the relation was truncated by a concurrent VACUUM (FluxTruncateRelation runs under AccessExclusiveLock and calls RelationTruncate). Unlike the FSM-returned blocks, this cached block is read/locked with no target_block < RelationGetNumberOfBlocks(relation) guard, so ReadBuffer can read past EOF ("unexpected data beyond EOF in block N"). Heap's RelationGetBufferForTuple validates/clamps the cached target against the real size. Validate the cached block here before using it.
📄 src/backend/access/flux/flux_handler.c (L3978-L3984)
actualfreespace is declared and zeroed inside the per-entry loop body, so it never accumulates across deletable entries -- actualfreespace += istatus->freespace only ever holds the current entry's freespace. The early-stop check therefore fires only if a single entry's freespace alone meets bottomupfreespace, which almost never happens, so bottom-up deletion scans the entire deltids array instead of stopping once enough space is reclaimable. heap_index_delete_tuples() declares the accumulator outside the loop and accumulates (heapam.c lines 8366/8552). Hoist the declaration out of the loop so it accumulates.
📄 src/backend/access/flux/flux_operations.c (L6475-L6480)
Cross-page defrag can move a tuple that is still being modified by an in-progress transaction, breaking rollback. The suitability guard skips DELETED, overflow, and update-chain tuples (t_ctid != self) but NOT FLUX_TUPLE_UNCOMMITTED. VACUUM holds only ShareUpdateExclusiveLock, so a concurrent backend may have an uncommitted in-place UPDATE on this (indexless) page: that tuple has t_ctid==self, is not DELETED, and carries a live t_verptr pointing at a per-backend UNDO record keyed to the OLD (src) TID. PageAddItem copies the header verbatim to the new TID, then ItemIdSetUnused drops the old slot. On ROLLBACK, flux_undo_apply restores the before-image to the OLD TID (now LP_UNUSED -> "item not normal, skipping"), so the prior committed version that was overwritten in place is permanently lost and the aborted new value remains on the page. Add a (tuple_hdr->t_flags & FLUX_TUPLE_UNCOMMITTED) check to skip_page, mirroring the DELETED guard, so only stable committed tuples are relocated.
📄 src/backend/access/flux/flux_operations.c (L6352-L6353)
Dead/contradictory code. FluxVacuumCrossPageDefrag returns 0 unconditionally when nindexes > 0 (the comment above explains tail reclamation is not attempted for indexed relations). As a result every downstream nindexes > 0 branch -- building indexInfoArray, the deferred_moves list, the FluxDefragMove struct, and the entire deferred index_insert/index_insert_cleanup replay loop -- is unreachable. This machinery can never run. For a minimal, committable patch, remove the dead index-maintenance code paths rather than leaving them to imply behavior that never executes.
📄 src/backend/access/flux/flux_operations.c (L3248-L3248)
Process/aspirational comments must be removed before commit. This file is pervaded by internal process notes that do not belong in committed source: an explicit FIXME(flux hot-row scaling) marker with a "Deferred ... See TASK B analysis in the handoff" reference, plus many "Phase 8c"/"Phase 8b", "BUG 5 fix"/"BUG 5:", "CANDIDATE A", and "WS-PVS1" markers scattered throughout. Per the contribution standards, comments must describe what the code does now (the "why"), not reference internal development phases, bug numbers, candidate labels, or handoff documents; stale TODO/FIXME markers are a rejection reason on -hackers. Replace these with intent-focused comments and drop the FIXME or convert it into a tracked item outside the source.
📄 src/backend/access/flux/flux_pbu.c (L392-L397)
The redo scan bound is wrong: it conflates "max undo blocks" with "max blocks to scan from first_block_id". Every flux_redo() caller passes first_block_id=1 (flux_xlog.c:1461,1989,2056,2569), but on the DO side the undo pages are registered at flux_undo_block_id, which in FluxXLogUpdate/FluxXLogInsert is placed AFTER block 0 plus all overflow buffers (up to MAX_OVERFLOW_BUFFERS = 32) plus the cross-page destination block (flux_xlog.c:296,610). This loop only probes blocks 1..first_block_id+4 (= 1..5), so when a FLUX record carries more than 4 forward blocks the undo block lands at block_id >= 5 and is never reached. data stays NULL, FluxPbuRedoUndo returns without re-running the undo insert, and the replayed transaction's before-image is silently lost -- a crash-recovery rollback can no longer restore it. The loop must scan up to XLR_MAX_BLOCK_ID (the real cap on forward blocks) rather than first_block_id + FLUX_PBU_MAX_UNDO_BLOCKS; the undo region itself is only ever on the first undo block, so the small bound belongs on the registration/count side, not on the forward-skip scan.
📄 src/backend/access/flux/flux_pbu.c (L88-L88)
The file-scope state machine (flux_pbu_prepared, flux_pbu_uur, flux_pbu_urp, flux_pbu_walhdr) has no abort-time reset. FluxPbuPrepareInsert() runs BEFORE the critical section and callers do error-throwing work between prepare and FluxPbuInsertUndo/FluxPbuFinishUndo (e.g. GetCurrentTransactionId() in the DELETE path at flux_operations.c:1880, or PrepareUndoInsert itself ereporting). If any of that throws, flux_pbu_prepared is left true: FluxXactCallback (flux_mvcc.c) only resets MVCC state, and ResetUndoBuffers() is never wired to xact abort. Consequences: (a) the uur_payload StringInfo is leaked (its pfree only runs in Finish/Cancel); (b) the next DML in the same backend trips Assert(!flux_pbu_prepared) in assert builds; (c) in production builds FluxPbuUndoPending() can report true against already-released undo buffers, so a forward-op WAL emitter calls FluxPbuRegisterUndoBuffers on invalid buffers -> corruption. Register an AtAbort-style reset (e.g. via RegisterXactCallback on XACT_EVENT_ABORT, or a subxact-abort hook) that clears flux_pbu_prepared and frees uur_payload.data; the already-present but never-called FluxPbuCancelUndo() cannot cover an unexpected ereport in this window.
📄 src/backend/access/flux/flux_pbu.c (L239-L239)
payload_len is a uint16 (xl_flux_undo.payload_len) but uur_payload.len is a StringInfo int, and tuple_len is uint32 (FluxUndoPayloadHeader.tuple_len). This narrowing cast silently truncates any payload >= 65536 bytes. The DO side would then write a short payload_len while XLogRegisterBufData attaches the full (truncated-reported) bytes, and FluxPbuRedoUndo's consistency check datalen != SizeOfFluxUndo + undohdr.payload_len elog(PANIC)s -- i.e. a corruption that only manifests as a PANIC during WAL replay/recovery, the worst place to find it. On-page before-images are bounded today, but there is no guard enforcing it. Add an explicit bound check (ereport(ERROR) before the critical section, or at least an Assert) that uur_payload.len <= PG_UINT16_MAX before this cast, so an oversized before-image fails loudly at write time rather than at replay.
📄 src/backend/access/flux/flux_pbu.c (L304-L304)
FluxPbuCancelUndo() is declared (flux_pbu.h:52) and defined here but has no caller anywhere in the tree (only a .gdbinit breakpoint references it); the documented no-op CAS path uses FluxPbuFinishUndo(InvalidXLogRecPtr) instead (flux_operations.c:3866). Per the minimalism/YAGNI discipline this is dead code -- either wire it into the no-op/abort path it was intended for or drop it along with its header prototype.
📄 src/backend/access/flux/flux_pvs.c (L329-L331)
Out-of-bounds read on the reconstructed image. candidate_len/next_len are set from untrusted on-disk UNDO values (phdr.tuple_len for FLUX_UNDO_UPDATE, delta->old_len for FLUX_UNDO_UPDATE_DELTA). On the next loop iteration FluxTupleGetVersionPtr(hdr, candidate_len) dereferences hdr->t_verptr (offset 8) and the end-of-chain branch reads FluxTupleGetXmin(hdr) = t_xmin (offset 24); the served buffer is later handed to FluxSlotStoreMaterializedTuple, which reads t_xmin/t_xmax too. Nothing validates that the reconstructed image is at least FLUX_TUPLE_OVERHEAD (40) bytes, so a short or corrupted payload yields an OOB read of the palloc'd buffer. Validate next_len >= FLUX_TUPLE_OVERHEAD after each reconstruction (and bail out as a malformed chain if not) before it becomes the new candidate.
📄 src/backend/access/flux/flux_pvs.c (L173-L175)
Best-effort semantics can surface a tuple version no snapshot should ever observe. When the chain is truncated (FluxPbuFetchVersion returns false for a discarded/unreadable record, a malformed payload breaks out of the switch, or the depth cap is hit), the loop breaks and the function returns true with the deepest reconstructed intermediate image. For an MVCC reader that is a correctness hazard: the served image was produced by a writer the reader already determined to be invisible, so it is NOT a snapshot-consistent before-image. The legacy sLog "best-effort" rationale in the header comment does not justify returning a wrong-visibility row here. Consider returning false (or raising ERROR) on a truncated/malformed chain rather than serving an intermediate version.
📄 src/backend/access/flux/flux_pvs.c (L101-L103)
No CHECK_FOR_INTERRUPTS in a loop that can perform up to FLUX_PVS_MAX_CHAIN_DEPTH (10000) UndoFetchRecord calls, each potentially doing buffer I/O. A long legitimate chain runs uninterruptibly, so a reader cannot be cancelled. Other FLUX scan/undo loops call CHECK_FOR_INTERRUPTS(); add one at the top of this loop.
📄 src/backend/access/flux/flux_pbu.c (L224-L227)
FluxPbuRegisterUndoBuffers() returns int nblocks, but no caller consumes the return value (flux_xlog.c:314, 703, 756, 1018 all discard it). The analogous RecnoPbuRegisterUndoBuffers() returns void. Per the minimalism/YAGNI discipline this unused return is dead flexibility; make it void (and update the prototype in flux_pbu.h).
📄 src/backend/access/flux/flux_pbu.c (L569-L571)
*out_payload_len is written only inside the if (out_payload != NULL) branch. A caller that passes a non-NULL out_payload_len together with a NULL out_payload would leave its length variable uninitialized on return, an easy-to-misuse API footgun. The sole current caller (flux_pvs.c:173) passes both, so no live bug, but the two out-params should be handled independently.
📄 src/backend/access/flux/flux_operations.c (L5008-L5010)
Memory leak / orphaned-TOAST hazard: the in-place UPDATE path calls flux_toast_tuple() into upd_ttc but never calls flux_toast_cleanup(&upd_ttc) on any exit path (TM_Ok, EPQ bounce, error, or out-of-place fallback). upd_toasted is set here but never read. The INSERT path correctly pairs the two (flux_toast_cleanup is only invoked once, in flux_tuple_insert). toast_tuple_cleanup() both frees the compression/externalization temporaries (leaked here into the caller's context) and deletes superseded external datums, so skipping it leaks memory and can orphan TOAST rows. Add the paired cleanup, e.g. if (upd_toasted) flux_toast_cleanup(&upd_ttc); before each return once the new tuple is durably stored (and on the error/fallback paths). (high confidence)
📄 src/backend/access/flux/flux_operations.c (L1638-L1646)
Deadlock-avoidance inconsistency: this DELETE wait site (and the two others at the visibility and UNCOMMITTED-recheck blocks) call XactLockTableWait() WITHOUT first acquiring the heavyweight per-tuple lock. The UPDATE path and this function's own final conflict gate deliberately take FluxLockTuple(..., LockTupleExclusive/NoKeyExclusive) before XactLockTableWait precisely to serialize competing writers into a FIFO queue; the surrounding comments state that two backends each seeing the other's in-progress write would otherwise mutually XactLockTableWait and deadlock (matching heap_delete/heap_update 'establish our priority'). Acquire the tuple lock here before sleeping, consistent with the final gate below. (moderate confidence)
📄 src/include/access/flux.h (L16-L18)
An installed PostgreSQL header must never include postgres.h; it is a hard project convention that each .c file includes postgres.h/postgres_fe.h first and headers only include the specific headers they need. No existing access-method header (e.g. heapam_xlog.h, nbtxlog.h) does this. Including it here breaks any potential frontend/extension consumer and is a reviewer red flag. Remove it and add the specific headers actually required (e.g. c.h types come transitively via the other includes).
💡 Suggested change
Before:
#include "postgres.h"
#include "access/heapam.h"
After:
#include "access/heapam.h"
📄 src/include/access/flux_xlog.h (L16-L18)
This header is included by src/bin/pg_waldump/rmgrdesc.c, a FRONTEND program (it does #define FRONTEND 1 then #include "postgres.h"). A header must never include postgres.h -- in a frontend build postgres.h is the wrong umbrella header (frontend code uses postgres_fe.h), and no other *_xlog.h (nbtxlog, hash_xlog, gistxlog, heapam_xlog) does this. Drop the include; the frontend-safe struct section is already guarded by #ifndef FRONTEND, and xlogreader.h/lib/stringinfo.h pull in the needed base types for both builds.
💡 Suggested change
Before:
#include "postgres.h"
#include "access/xlogreader.h"
After:
#include "access/xlogreader.h"
📄 src/include/access/flux.h (L270-L272)
This flag's comment is stale and now contradicts the implementation: the surrounding struct comment (and FluxTupleGetVersionPtr) state that t_verptr is a fixed header field and the trailing-UndoRecPtr-after-column-data scheme was removed. In practice the flag is unconditionally set on every tuple at birth (flux_operations.c sets it right after allocation for INSERT, UPDATE, multi-insert, CAS), so the one remaining reader (new_tuple->t_data->t_flags & FLUX_TUPLE_HAS_VERSION_PTR) is always true -- the flag carries no information. Either remove the dead flag and its check (YAGNI) or at minimum fix the comment to describe current behavior; as written it will mislead a future reader into believing the version pointer is a trailer located after the column data.
💡 Suggested change
Before:
#define FLUX_TUPLE_HAS_VERSION_PTR 0x0100 /* trailing UndoRecPtr
* (version-chain head) follows
* column data */
After:
#define FLUX_TUPLE_HAS_VERSION_PTR 0x0100 /* tuple reserves a t_verptr header
* field (set at birth for every
* tuple) */
📄 src/backend/access/recno/recno_fsm.c (L292-L295)
RecnoMarkPageForDefrag calls RecnoGetFSMState(), which palloc0s a RecnoFSMState and creates a child MemoryContext ("RECNO FSM Context") plus a defrag-queue allocation, then returns without ever freeing them. Unlike RecnoOpportunisticDefrag (which deletes fsm_state->fsm_context and pfrees the state), this path leaks a MemoryContext on every call. Since RecnoRecordFreeSpace -> RecnoMarkPageForDefrag fires on every delete/update that fragments a page, this is an unbounded per-transaction leak in CurrentMemoryContext.
Separately, the whole queue machinery is a no-op: RecnoGetFSMState builds a fresh, empty state each call (see its own comment "create a new state each time"), so the block number pushed by RecnoScheduleDefrag here can never be observed by the independent fresh state that RecnoOpportunisticDefrag/RecnoBatchDefrag later allocate. The defrag queue is written and discarded, never drained. Confidence: high.
📄 src/backend/access/recno/recno_fsm.c (L324-L327)
RecnoGetFSMState allocates a brand-new state (with its own MemoryContext and defrag queue) on every call -- the comment admits "create a new state each time" / "In practice, this would be cached per relation." Because the queue is never persisted across calls, the entire defrag-queue subsystem (RecnoScheduleDefrag, RecnoGetNextDefragPage, defrag_queue*) is dead/ineffective scaffolding, and callers that don't free the returned state (notably RecnoMarkPageForDefrag) leak it. Per YAGNI, either wire up a real per-relation cache or drop the queue machinery; shipping a non-functional, leaky subsystem is a footgun. Confidence: high.
📄 src/backend/access/recno/recno_clock.c (L368-L369)
%lu is not portable for uint64: on 32-bit platforms and on Windows/LLP64 unsigned long is 32-bit, so error_ms (uint64) is printed with a format mismatch (wrong value, and a -Wformat warning that breaks -Werror buildfarm members). Use UINT64_FORMAT, matching HLCToString elsewhere in this change. Both occurrences (80%% and 50%% messages) are affected. Confidence: high.
💡 Suggested change
Before:
(errmsg("recno clock error bound %lu ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
After:
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
📄 src/backend/access/recno/recno_clock.c (L378-L379)
Same %lu-for-uint64 portability defect in the 50%% warning. Use UINT64_FORMAT. Confidence: high.
💡 Suggested change
Before:
(errmsg("recno clock error bound %lu ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
After:
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
📄 src/backend/access/recno/recno_clock.c (L204-L206)
Three defects in this bgworker main loop:
-
pqsignal(SIGTERM, PG_SIG_DFL)installs the default disposition for SIGTERM, so a normal smart/fast shutdown TERMs this worker and the kernel kills it outright instead of letting it exit gracefully. Standard bgworkers install adie/SIGTERMhandler (e.g.pqsignal(SIGTERM, die)) beforeBackgroundWorkerUnblockSignals(). -
The
WaitLatchloop only breaks onshutdown_pendingor exits viaWL_EXIT_ON_PM_DEATH; it never responds to a shutdown request, so there is no clean stop path short of postmaster death.ProcessInterrupts()/CHECK_FOR_INTERRUPTS()is also never called in the loop. -
The trailing
ereport(FATAL, ...)after the loop is only reachable viashutdown_pending, i.e. it turns a drift-triggered self-shutdown into a FATAL every time the loop is asked to stop. Confidence: high.
📄 src/backend/access/recno/recno_clock.c (L255-L256)
RecnoReadClockBound does memcpy(bounds, RecnoClockShmem->clockbound_map, sizeof(ClockBoundData)) directly from an mmap of /dev/shm/clockbound written by an external daemon, and trusts the raw in-memory struct layout -- struct timespec, trailing padding, field widths, and native endianness -- as if it were a stable shared/wire format. A daemon built with a different arch/ABI, or a cross-arch mount, yields arbitrary earliest/latest/error_bound that then drive MVCC timestamp bounds and (via RecnoCheckClockHealth) can force a server FATAL. There is no layout version check: only segment_id != 0 and tv_sec != 0. The /* atomic read */ comment is also false -- a multi-field memcpy from a concurrently-written mapping is neither atomic nor barrier-protected, so a torn read of earliest/latest/error_bound is possible. Confidence: high.
📄 src/backend/access/recno/recno_clock.c (L358-L360)
RecnoCheckClockHealth can ereport(FATAL) from a dynamic bgworker (bgw_restart_time = 5) based on an integer error bound compared against a floating-point threshold (recno_max_clock_offset_ms * 0.8), using clock data sourced from an untrusted external mmap (see RecnoReadClockBound). A FATAL in a restartable bgworker that keeps re-reading the same bad value produces a 5-second FATAL restart loop; with recno_fatal_on_clock_drift the stated intent is to take the server down. Driving a FATAL off an unvalidated external value plus a float comparison is a server-availability footgun. Use integer arithmetic for the threshold and treat implausible/unvalidated daemon input as "unavailable" rather than a fatal condition. Confidence: moderate.
📄 src/backend/access/recno/recno_mvcc.c (L1360-L1364)
RecnoCanVacuumTimestamp reads the cached RecnoMvccShmem->oldest_active_ts under mvcc_lock, but that field is maintained lock-free (per-backend slots + oldest_active_generation) and is only recomputed inside RecnoGetOldestActiveTimestamp(). This function never calls that refresh, so it can observe a stale (too-new) horizon and return true for a tuple a running transaction can still see. Its callers in recno_handler.c -- the index_delete_tuples all_dead decision (~L1958), copy_for_cluster (~L3544), and index_build_range_scan (~L3787/L3938) -- then classify a still-visible deleted tuple as removable/dead, i.e. index entries get dropped or the tuple is skipped during CLUSTER/REINDEX, an MVCC violation / data loss. Compute the horizon via RecnoGetOldestActiveTimestamp() here instead of reading the cached field. Confidence: moderate.
💡 Suggested change
Before:
LWLockAcquire(&RecnoMvccShmem->mvcc_lock, LW_SHARED);
can_vacuum = (vacuum_ts < RecnoMvccShmem->oldest_active_ts);
LWLockRelease(&RecnoMvccShmem->mvcc_lock);
After:
can_vacuum = (vacuum_ts < RecnoGetOldestActiveTimestamp());
📄 src/backend/access/recno/recno_lock.c (L429-L442)
RecnoLockMultipleTuples orders the TID array with an O(n^2) bubble sort before locking. This runs before any lock is held, so it is pure wasted work on a potentially large array; use qsort() with an ItemPointerCompare wrapper (the idiomatic PostgreSQL choice) instead. Confidence: high (performance).
📄 src/backend/access/recno/recno_lock.c (L391-L395)
RecnoCheckDeadlock is a stub that always returns false after computing tag/lockmode and discarding them with (void) casts. A function named "CheckDeadlock" that never checks for deadlock is a POLA violation / footgun: any caller that trusts it will silently skip real deadlock avoidance. Either remove this dead function (YAGNI) or implement it; do not ship a check that no-ops. Confidence: high.
📄 src/backend/access/recno/recno_handler.c (L4593-L4595)
Dead scaffolding: minimal_tuple_from_recno_tuple builds an all-NULL placeholder tuple (comment: "prevents crashes") and has no real caller; it is kept alive only by __attribute__((unused)). Besides being misleading dead code in an already-5000-line file (YAGNI), __attribute__((unused)) is not the tree's convention -- use pg_attribute_unused(). Remove the function. Confidence: high.
📄 src/backend/access/recno/recno_handler.c (L4629-L4631)
Dead scaffolding: recno_tuple_from_slot builds a "basic" RECNO tuple with no attribute data and has no real caller, kept only by __attribute__((unused)). Remove it (YAGNI); if ever genuinely needed, use the tree's pg_attribute_unused() rather than the raw GCC attribute. Confidence: high.
📄 src/backend/access/recno/recno_compress.c (L1214-L1214)
Dead speculative scaffolding with a real hazard if ever wired up: RecnoCompressLZ4Dict/RecnoDecompressLZ4Dict are gated on HAVE_LZ4_DICT, which is defined nowhere in the tree, and each declares char dict_buf[65536] on the stack. A 64KB stack frame risks stack overflow in a backend (PostgreSQL guards against deep stacks precisely because the limit is modest), and the functions are unreachable pg_attribute_unused() code anyway. Per YAGNI, drop the LZ4 (and matching ZSTD) dictionary-accelerated paths rather than shipping a disabled, overflow-prone helper. Confidence: high.
📄 src/backend/access/recno/recno_compress.c (L957-L962)
Dictionary compression is unsafe for any persisted datum. RecnoCompressDictionary writes only a 4-byte backend-local dict_id to the output, while the dictionary itself lives in CacheMemoryContext and is explicitly NOT persisted (see the file's own TODO). RecnoDecompressDictionary then indexes compression_dict->entries[dict_id] from whatever that backend happens to have cached -- a different backend, or the same backend after RecnoResetCompressionDict, reads an unrelated entry or raises "invalid dictionary ID in compressed data". That is silent data corruption / data-loss for stored tuples. RecnoChooseCompressionType currently returns only ZSTD/DELTA so the auto path is safe, but RecnoCompressAttribute still honors an explicit RECNO_COMP_DICTIONARY, and RecnoDecompressAttribute still decodes it, so the hazard is one caller away. This compression method should not be reachable on the on-disk path. Confidence: moderate.
📄 src/backend/access/recno/recno_diff.c (L92-L94)
RecnoComputeTupleDiff truncates offsets/lengths into uint16 (segments[nsegments].offset = (uint16) start; / .length = (uint16) (end - start)) without first verifying old_len <= 65535. A tuple larger than 64KB silently wraps the stored offset, so RecnoApplyDiffReverse later writes the old bytes at the wrong location -- undetected diff corruption on rollback / version reconstruction. Add an explicit guard that returns NULL (fall back to full-tuple logging) when old_len > UINT16_MAX, since RecnoDiffSegment.offset/length and RecnoDiffRecord.total_size are all uint16. Confidence: moderate.
📄 src/backend/access/recno/recno_handler.c (L4698-L4699)
RECNO sets am_undo_engine = UNDO_ENGINE_PERBACKEND and am_supports_undo = true, but per recno_mvcc.c the sLog (in-progress-xact state) is shared-memory only and "LOST on crash". Crash/standby visibility therefore depends entirely on the t_xmin+CLOG fallback reconstructing the right answer for (a) a committed tuple whose sLog entry is gone and (b) an aborted plain INSERT whose UNCOMMITTED flag is still set. This is the correctness crux of the whole AM; it must be backed by a redo path that re-establishes t_xmin/flags and by recovery tests (crash + standby) exercising both cases. Flag for explicit verification before this is commit-ready. Confidence: moderate.
📄 src/backend/access/flux/flux_stats.c (L50-L52)
This entire module is dead code. FluxCollectRelationStats() and FluxLogRelationStats() are declared in flux.h but have no caller anywhere in the backend (only a .gdbinit breakpoint references them). A patch for pgsql-hackers must not ship speculative scaffolding (YAGNI); drop flux_stats.c, its two prototypes, and the FluxRelationStats typedef until a consumer (e.g. an ANALYZE hook or planner cost adjustment) is actually wired up. Also note the double-zeroing: the header comment states "The caller must pass a zeroed FluxRelationStats struct" yet the function unconditionally memset(stats, 0, ...) below, so the two contracts contradict each other.
📄 src/backend/access/flux/flux_stats.c (L148-L150)
Dead branch that still dereferences an assumed-present header. The module header and the FLUX tuple paths state "FLUX does not compress attributes" and the only writer of FLUX_TUPLE_COMPRESSED (flux_tuple.c) is gated by a has_compressed local that is never set true. So this branch is unreachable for any FLUX-written page; on a legacy/foreign page it would blindly cast PageGetItem to FluxCompressionHeader* and read orig_size/comp_size. Remove the compression accounting along with the rest of this unused module.
📄 src/backend/access/flux/flux_tuple.c (L355-L357)
Dead scaffolding on the hot insert path. is_compressed/is_overflowed are allocated (palloc'd for >16 attrs), memset/palloc0'd, scanned in the trailing free loop (if (is_compressed[i] || is_overflowed[i]) pfree(...)), yet are never set true anywhere -- the comments just below confirm "FLUX has no on-page overflow mechanism" and "FLUX does not compress attributes", so work_values always mirrors values and has_compressed/has_overflow stay false. For wide tuples this adds three palloc/pfree calls per insert with no functional effect. Drop the compression/overflow bookkeeping and copy or alias values directly.
📄 src/backend/access/flux/flux_tuple.c (L245-L249)
Aspirational / inaccurate documentation. This comment describes a compression path ("flux_enable_compression GUC", "Compressed attributes are stored with a FluxCompressionHeader prefix and the tuple's FLUX_INFOMASK_COMPRESSED bit is set") and on-page overflow ("large attributes exceeding FLUX_OVERFLOW_THRESHOLD are automatically stored in overflow pages") that the implementation explicitly does not have -- the body states "FLUX has no on-page overflow mechanism ... FLUX does not compress attributes." Comments must describe current behavior; rewrite to match the TOAST-only, no-compression reality.
📄 src/backend/access/flux/flux_vm.c (L72-L74)
Unused API surface (YAGNI). FluxVMInit has an empty body (only a comment) and FluxVMGetPageSize/FluxVMMapHeapToVM/FluxVMPinBuffer have no caller anywhere in the backend (only .gdbinit breakpoints). Thin one-line wrappers and an empty init function with no consumer should not ship in a pgsql-hackers patch; drop these functions and their flux.h prototypes until something actually calls them.
📄 src/backend/access/flux/flux_vm.c (L551-L551)
Dead parameter with aspirational comment. tuple is immediately (void)-cast away and the comment says "reserved for future timestamp checking". A patch should not carry an unused parameter justified by future-tense intent; drop the parameter (and update callers) until the timestamp check is implemented.
📄 src/backend/access/flux/flux_slot.c (L860-L865)
Memory leak / asymmetry with tts_flux_clear. On the same-buffer fast path, when TTS_SHOULDFREE is set this frees rslot->tuple but not rslot->values_block, and never resets rslot->values_block = NULL before overwriting rslot->tuple below. tts_flux_clear() frees both; this path leaks any owned values block and leaves a stale pointer. Free values_block here too (and NULL it), mirroring tts_flux_clear.
💡 Suggested change
Before:
if (unlikely(TTS_SHOULDFREE(slot)))
+ {
+ if (rslot->tuple)
+ pfree(rslot->tuple);
+ slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
+ }
After:
if (unlikely(TTS_SHOULDFREE(slot)))
{
if (rslot->tuple)
pfree(rslot->tuple);
if (rslot->values_block)
{
pfree(rslot->values_block);
rslot->values_block = NULL;
}
slot->tts_flags &= ~TTS_FLAG_SHOULDFREE;
}
📄 src/backend/access/flux/flux_slot.c (L743-L745)
tts_flux_copyslot copies tts_tid but not tts_tableOid into the destination. tts_flux_getsysattr's cmin/cmax branch reads slot->tts_tableOid (and tts_tid) via FluxLocalCidGet/SLogTupleLookupFiltered. A system-column read on a copied slot would then query the sLog with a zero/garbage tableOid. Core heap/virtual copyslot ops don't carry tableOid either, but those AMs don't key a sLog lookup off it in getsysattr the way FLUX does -- so either copy tts_tableOid here or guard the cmin/cmax path against an invalid (tableOid==InvalidOid || !ItemPointerIsValid) slot. Confidence: moderate.
💡 Suggested change
Before:
dstslot->tts_nvalid = desc->natts;
+ dstslot->tts_flags &= ~TTS_FLAG_EMPTY;
+ dstslot->tts_tid = srcslot->tts_tid;
After:
dstslot->tts_nvalid = desc->natts;
dstslot->tts_flags &= ~TTS_FLAG_EMPTY;
dstslot->tts_tid = srcslot->tts_tid;
dstslot->tts_tableOid = srcslot->tts_tableOid;
📄 src/backend/access/recno/recno_fsm.c (L292-L294)
MemoryContext leak on a hot path. RecnoMarkPageForDefrag calls RecnoGetFSMState(), which palloc0s a RecnoFSMState and AllocSetContextCreates fsm_context, then returns without freeing either. RecnoMarkPageForDefrag is reached from RecnoRecordFreeSpace on nearly every insert/update/delete/vacuum path, so this leaks a MemoryContext per DML operation.
Compounding this: RecnoGetFSMState builds a fresh state with an empty defrag_queue every call, so the page scheduled here is enqueued into a queue that is immediately discarded. RecnoOpportunisticDefrag later allocates its own fresh state (empty queue), so RecnoGetNextDefragPage always returns InvalidBlockNumber -- the entire defrag-queue machinery is dead/ineffective. Either persist FSM state per-relation (as the stale comment in RecnoGetFSMState claims is intended) or remove the queue machinery entirely.
📄 src/backend/access/recno/recno_clock.c (L368-L369)
Non-portable format specifier: error_ms is uint64 but is printed with %lu. On 32-bit platforms and Windows/MSVC (LLP64), unsigned long is 32-bit, so %lu for a uint64 is wrong and triggers compiler warnings / truncation. Use UINT64_FORMAT as the rest of the RECNO code (e.g. HLCToString) already does. Both occurrences (here and the 50%% WARNING below) need fixing.
💡 Suggested change
Before:
(errmsg("recno clock error bound %lu ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
After:
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 80%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms),
📄 src/backend/access/recno/recno_clock.c (L378-L379)
Same %lu-for-uint64 portability defect as the FATAL path above; use UINT64_FORMAT.
💡 Suggested change
Before:
(errmsg("recno clock error bound %lu ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
After:
(errmsg("recno clock error bound " UINT64_FORMAT " ms exceeds 50%% of maximum %d ms",
error_ms, recno_max_clock_offset_ms)));
📄 src/backend/access/recno/recno_clock.c (L358-L360)
Clock-health thresholds compare an integer error bound against floating-point products (recno_max_clock_offset_ms * 0.8, * 0.5), and on breach ereport(FATAL) from a background worker on data that originates from an EXTERNAL daemon mmap (see RecnoReadClockBound). A malformed/mismatched daemon value therefore becomes a server-availability footgun: the worker FATALs, and with bgw_restart_time = 5 it restarts and FATALs again in a loop. Avoid float comparisons for integer thresholds, and reconsider whether externally-supplied clock data should ever be able to force a FATAL.
📄 src/backend/access/recno/recno_clock.c (L255-L262)
RecnoReadClockBound (above) memcpy's sizeof(ClockBoundData) directly out of an mmap of /dev/shm/clockbound written by an external daemon, treating the raw C struct layout -- including struct timespec, native endianness, and compiler padding -- as a stable shared/wire format. A daemon built with a different ABI, or a different architecture/endianness, yields arbitrary earliest/latest/error_bound values that feed MVCC timestamp bounds and (via RecnoCheckClockHealth) can force a server FATAL. The validation only checks segment_id != 0 and tv_sec != 0, which does not establish layout/version compatibility. The 'atomic read' comment is also false: a multi-field struct memcpy from a concurrently-written mmap is neither atomic nor barriered. A versioned, explicitly-sized, endianness-defined header is required before trusting this data.
📄 src/include/access/flux.h (L410-L413)
FLUX_OVERFLOW_MAX_CHUNK_SIZE is dead code: it is defined here but never referenced anywhere in the tree. Beyond being unused, it expands FLUX_MAX_TUPLE_SIZE, which is not defined until ~200 lines further down (line 620). This happens to compile only because the macro is never actually expanded above that point; the moment someone uses it before line 620 it fails to compile. Per YAGNI, drop it (or, if a future use is intended, move it below the FLUX_MAX_TUPLE_SIZE definition).
💡 Suggested change
Before:
/* Maximum column data per overflow record */
#define FLUX_OVERFLOW_RECORD_OVERHEAD MAXALIGN(sizeof(FluxOverflowRecordHeader))
#define FLUX_OVERFLOW_MAX_CHUNK_SIZE \
(FLUX_MAX_TUPLE_SIZE - FLUX_OVERFLOW_RECORD_OVERHEAD)
After:
/* Maximum column data per overflow record */
#define FLUX_OVERFLOW_RECORD_OVERHEAD MAXALIGN(sizeof(FluxOverflowRecordHeader))
📄 src/include/access/flux.h (L509-L523)
Dead code: the FluxFreeSpaceMap struct and the FLUX_FSM_* level macros are defined here (and in typedefs.list) but never referenced by any FLUX source. FLUX's free-space management goes through FluxTargetFreeSpace/FluxGetPageWithFreeSpace/FluxRecordFreeSpace/FluxVacuumFSM, none of which use this struct or these constants. Remove this speculative scaffolding (YAGNI).
📄 src/include/access/flux.h (L865-L868)
These three declarations are duplicated verbatim in the dedicated header flux_dirtymap.h, which is included alongside flux.h by flux_handler.c and flux_operations.c. Identical extern prototypes are legal C so it compiles, but keeping two sources of truth for the same API is a maintainability hazard (they can drift). The dirty-map API already has its own header; drop the copies here and let flux.h's consumers include flux_dirtymap.h (or include it from flux.h).
📄 src/backend/access/flux/flux_undo.c (L502-L505)
The return value of apply_flux_undo_restore_tuple() is discarded, but flux_inplace_index_undo() later runs unconditionally whenever new_hdr_copy != NULL. When the restore is skipped (before-image larger than the current on-page slot, which returns false with a WARNING), the heap tuple is left holding k_new while the index is still reversed to the k_old state: the (k_new,TID) entry is killed and the (k_old,TID) delete-mark cleared. flux_inplace_index_undo's own header explicitly states it must run only "After the table UNDO has restored the before-image tuple in place". The result is an index/heap divergence (k_old scans return TID whose heap row is k_new; k_new scans miss a logically-live row) until VACUUM. Capture the restore result and skip the index reversal when the table restore was skipped.
💡 Suggested change
Before:
apply_flux_undo_restore_tuple(rel, buffer, offnum, blkno,
urec_ptr, xid, info,
image_bytes, (uint32) image_len);
break;
After:
if (!apply_flux_undo_restore_tuple(rel, buffer, offnum, blkno,
urec_ptr, xid, info,
image_bytes, (uint32) image_len))
{
/*
* Table restore was skipped (slot too small); the on-page
* tuple still holds k_new, so reversing the index would
* diverge index from heap. Leave both as-is for VACUUM.
*/
if (new_hdr_copy != NULL)
{
pfree(new_hdr_copy);
new_hdr_copy = NULL;
}
}
break;
📄 src/backend/access/flux/flux_xlog.c (L848-L852)
Dead WAL path. FluxXLogCompress() has no caller anywhere in the backend (only a .gdbinit breakpoint references it), and FLUX never compresses attributes (per the file-level and tuple-path comments; the only has_compressed writer is gated by a local never set true). The emitter, its opcode XLOG_FLUX_COMPRESS, the xl_flux_compress struct, and the matching flux_xlog_compress_redo() handler are all unreachable scaffolding. The same applies to FluxXLogOverflowWrite()/XLOG_FLUX_OVERFLOW_WRITE and the FLUX_WAL_HAS_OVERFLOW_BLK0 / overflow-buffer branches in the INSERT and UPDATE redo handlers, since FluxStoreOverflowColumn() is never called and overflow_buffers.count is never incremented (FLUX uses heap TOAST, not on-page overflow). A pgsql-hackers patch must not ship speculative WAL opcodes and redo handlers with no forward-path emitter; drop the dead compress/overflow logging and redo code.
📄 src/include/access/recno.h (L138-L138)
t_xid_hint and t_xact_ts are dead fields. A codebase search finds zero ->t_xid_hint / ->t_xact_ts accesses anywhere in src/backend/access/recno/, and recno_mvcc.c explicitly documents them as removed: "Since we removed t_xid_hint to save 4 bytes per tuple, the sLog lookup is now mandatory" (recno_mvcc.c:906) and "the tuple header no longer carries t_xmin, t_xmax, or t_xact_ts" (recno_mvcc.c:15). Because this struct is the on-disk/WAL tuple header (logged verbatim), these two fields are not merely cosmetic: they add 12 bytes of pure waste to every stored tuple and directly contradict the implementation's own "saves 4 bytes per tuple" rationale. The sibling flux.h did the opposite correctly, folding t_xid_hint into t_xmin and deleting t_gen. Remove t_xid_hint and t_xact_ts here (and from the form/deform/WAL paths) so the header matches the shipped format.
📄 src/include/access/recno.h (L24-L26)
Public access-method headers must not include postgres.h. It is the backend-only translation-unit prologue that each .c file includes first; pulling it into an installed header breaks any frontend/extension consumer that includes recno.h after its own prologue (e.g. postgres_fe.h), and violates the PostgreSQL header convention (no other core access/*.h does this). Drop the include and rely on each .c including postgres.h first, as nbtree.h / heapam.h do. The same fix applies to recno_diff.h, recno_slog.h, and recno_xlog.h (recno_pbu.h and recno_undo.h correctly omit it).
📄 src/include/access/recno.h (L339-L339)
RelationHasUndo is an unprefixed, generic macro name in an installed public header, and it unconditionally expands to (true). Any other header or shared code that defines or expects a RelationHasUndo with relation-specific semantics will silently conflict/shadow when recno.h is included. Give it a subsystem prefix (e.g. RecnoRelationHasUndo) to avoid namespace collision; the callers are all in recno_*.c and easy to update.
📄 src/include/access/recno.h (L105-L105)
Stray printf escaping in a plain C block comment: "packed to 100%%" renders the doubled percent literally. Use a single '%'. (Appears twice in this comment block.)
📄 src/include/access/recno.h (L227-L227)
The or_magic comment names RECNO_OVERFLOW_MAGIC, but two separate record magics are defined in this header -- RECNO_OVERFLOW_RECORD_MAGIC (0x524F5652 "ROVR") just above and RECNO_OVERFLOW_MAGIC (0x524F5646 "ROVF") just below -- and neither symbol is referenced anywhere in src/backend/access/recno/ (only RECNO_OVERFLOW_PTR_MAGIC is actually used). Dead/duplicate magic constants with a comment that disagrees with the two definitions invites a wrong-magic bug. Remove the unused constants and point the comment at the single magic the record header actually carries.
📄 src/include/access/recno.h (L15-L15)
Per-file "Author: ..." lines are not used in core src/include headers (authorship is tracked in git, not file headers) and the committer owns the copyright year. Drop the Author line; the committer will set the year at commit time. Same note applies to recno_diff.h, recno_pbu.h, recno_slog.h, recno_undo.h, and recno_xlog.h, which carry identical Author/2026 headers.
📄 src/backend/access/recno/recno_overflow.c (L582-L589)
Critical-section violation. START_CRIT_SECTION() opens above PageAddItem and END_CRIT_SECTION() closes well below, but this fallback branch runs inside that span and calls ReadBuffer(), LockBuffer(), and RecnoXLogOverflowWrite() (which performs XLogInsert()). All of these can block, palloc, or ereport, which is forbidden inside a critical section and will PANIC if they fail. The link-update of a previous record on a different page must be done either before entering the crit section or must rely solely on the already-pinned-and-locked buffers tracked in overflow_buffers (the non-fallback branch above already does this).
📄 src/include/access/recno_xlog.h (L163-L163)
Inconsistent macro form: SizeOfXlRecnoHlcInfo above wraps sizeof(...) in parentheses, but SizeOfRecnoUndo does not. It happens to be safe today (sizeof binds tighter than +, as in recno_pbu.c:363 SizeOfRecnoUndo + (Size) undohdr.payload_len), but an unparenthesized function-like-looking macro is a latent footgun. Parenthesize for consistency: (sizeof(xl_recno_undo)).
📄 src/backend/access/recno/recno_overflow.c (L1517-L1525)
Data loss and primary/standby divergence. After Pass 2 deletes only orphaned overflow records from a page, this WAL-logs the page via RecnoXLogInitPage. Its redo handler (recno_xlog_init_page_redo) uses RBM_ZERO_AND_LOCK + RecnoInitPage, i.e. it reinitializes the page to empty on replay, discarding every surviving live tuple and referenced overflow record on that page. On crash recovery or a standby, replaying this record wipes still-live data. VACUUM must emit a record whose redo removes only the same orphan line pointers it deleted here (and takes an FPI), not one that reinitializes the whole page.
📄 src/backend/access/recno/recno_overflow.c (L1538-L1540)
Format-string portability. PostgreSQL requires INT64_FORMAT for int64 rather than %lld plus a (long long) cast; the cast is a non-portable workaround the project convention forbids. Use INT64_FORMAT and drop the casts.
📄 src/backend/access/recno/recno_overflow.c (L1552-L1554)
Dead code / speculative scaffolding, contrary to minimalism. RecnoFetchOverflow always ereports FEATURE_NOT_SUPPORTED, and RecnoStoreOverflow/RecnoDeleteOverflow are described as a transition shim. If these legacy entry points have no live callers they should be removed before posting to -hackers; a stub that only errors out is a footgun. Also remove the "FUTURE ENHANCEMENTS (deferred)" block at the top of this file -- comments must describe what the code does now.
📄 src/backend/access/recno/recno_slog.c (L478-L484)
Lock-ordering hazard. TransactionIdIsInProgress() and TransactionIdDidCommit() acquire ProcArrayLock / CLOG SLRU locks, and here they run while the sLog partition LWLock is held EXCLUSIVE. Any other code path that takes a partition lock after ProcArrayLock/CLOG creates a lock-order inversion and deadlock risk. The transaction-status probes should be done without holding the partition lock, or the lock order must be documented and enforced across all sLog call sites.
📄 src/backend/access/recno/recno_slog.c (L527-L529)
Uninterruptible wait. This retry loop sleeps up to 3ms waiting for other transactions' cleanup but has no CHECK_FOR_INTERRUPTS(), so a backend wedged here on a hot sLog entry cannot be cancelled (SIGINT/statement_timeout). Add CHECK_FOR_INTERRUPTS() in the loop (while no partition lock is held).
📄 src/backend/access/recno/recno_slog.c (L1458-L1463)
MVCC crash/standby consistency bug. This stamps RECNO_TUPLE_DELETED and clears RECNO_TUPLE_UNCOMMITTED on the page and MarkBufferDirty(), but emits NO WAL record -- and it runs at XACT_EVENT_PRE_COMMIT, so the top-level transaction then commits. On crash recovery or on a standby, the page reverts to its pre-mutation state (UNCOMMITTED set, not DELETED), so a row from a rolled-back subtransaction INSERT becomes visible after recovery/on the replica. Unlike a hint bit, this flag transition changes tuple visibility and must be WAL-logged with a redo that reproduces it.
📄 src/backend/access/recno/recno_slog.c (L1609-L1618)
Buffer lock/pin and relcache leak on the error path. The PG_CATCH swallows the error with EmitErrorReport()/FlushErrorState() but does not UnlockReleaseBuffer(buf) or relation_close(rel). If ReadBuffer/LockBuffer or any statement between lines 1582-1605 ereports, buf is left locked+pinned and rel left open, yet the transaction proceeds to commit -- committing while holding a leaked buffer content lock. The CATCH must release buf (if locked/valid) and close rel before swallowing. Separately, deliberately swallowing errors inside a PRE_COMMIT callback is fragile; prefer not throwing here at all.
📄 src/backend/access/recno/recno_slot.c (L285-L288)
Relcache reference leak on error. This opens the relation and calls RecnoFetchOverflowColumn() with no PG_TRY/PG_FINALLY. RecnoFetchOverflowColumn has several ereport(ERROR) corruption paths (invalid offset, bad magic, incomplete read); any of them throws between relation_open and relation_close, leaking the relcache reference. Deform also runs in the hot scan path and may be invoked while the source page buffer is still locked, so opening a relation and reading overflow buffers mid-deform additionally risks lock-order inversion. Fetch overflow after the deform/buffer-lock has been dropped, or protect this with PG_TRY/PG_FINALLY.
📄 src/backend/access/undo/atm.c (L386-L388)
On-disk format depends on struct padding, and uninitialized padding is CRC'd. AtmStateRecord rec; is declared on the stack and only its named fields are assigned; the padding after bool revert_complete (and any inter-field alignment padding) is left uninitialized, then both CRC'd (line 394) and written (line 397) to a durable file. Two problems: (1) the CRC covers garbage stack bytes, so a file written by one backend can fail CRC verification on reload within the same cluster if the padding differs run-to-run; (2) the raw-struct layout (padding, bool size, field alignment) becomes a durable on-disk format that is not portable across compilers/arches. twophase.c avoids this by serializing a defined byte layout. At minimum, memset(&rec, 0, sizeof(rec)) before populating; preferably serialize fixed-width fields explicitly.
💡 Suggested change
Before:
AtmStateRecord rec;
rec.xid = entries[i].xid;
After:
AtmStateRecord rec;
memset(&rec, 0, sizeof(rec));
rec.xid = entries[i].xid;
📄 src/backend/access/undo/atm.c (L500-L502)
Unbounded allocation from an untrusted on-disk count read before the CRC check. hdr.count comes straight from the file and is multiplied by sizeof(AtmStateRecord) to size a palloc with no upper bound, and the CRC that would detect corruption is only verified AFTER this allocation. A corrupt or hostile file with a huge count triggers an enormous allocation (palloc ERROR) or, on 32-bit, integer overflow of the multiply yielding a tiny allocation followed by an out-of-bounds read. ReadTwoPhaseFile (the mirrored path) explicitly bounds stat.st_size > MaxAllocSize first. Add a bound on hdr.count (e.g. derived from file size via fstat, capped at MaxAllocSize/sizeof(AtmStateRecord)) before the multiply/palloc.
📄 src/backend/access/undo/atm.c (L502-L504)
Short read is treated as a hard failure. read() may legitimately return fewer bytes than requested (e.g. interrupted by a signal) without an error; comparing nread != sizeof(hdr) / nread != (ssize_t) bytes rejects a valid file in that case. twophase.c uses the same bare read() pattern but only after fstat-bounding the file and reads the whole slurped buffer at once; here the multi-read sequence is more exposed. Prefer a looped read helper (or at least document why a single read is assumed complete).
📄 src/backend/access/undo/atm.c (L372-L372)
Reusing the two-phase-commit wait events (WAIT_EVENT_TWOPHASE_FILE_WRITE/SYNC/READ) for ATM state-file I/O misreports in pg_stat_activity: a backend doing ATM checkpoint I/O will show a TwoPhaseFileWrite/TwoPhaseFileRead wait, which is misleading. Add dedicated wait events for the ATM state file via src/backend/utils/activity/wait_event_names.txt rather than borrowing an unrelated facility's events.
📄 src/backend/access/recno/recno_slot.c (L523-L527)
Forcing XID assignment on a read path. Returning GetCurrentTransactionId() for xmin/xmax assigns a real transaction id as a side effect of merely reading a system column (e.g. a plain SELECT of xmin/xmax, or ON CONFLICT inspection). RECNO is otherwise XID-free; a read-only statement must not consume an XID. Note the no-tuple branch above correctly reads t_xmin off the page instead. Use GetCurrentTransactionIdIfAny() / return the stored xmin, and do not force assignment.
📄 src/backend/access/recno/recno_vm.c (L72-L79)
Dead code / empty stub. RecnoVMInit has an empty body with only an explanatory comment -- it performs no work. Per minimalism, either remove this function and its callers, or populate it; shipping an empty entry point whose comment says the real work happens elsewhere is speculative scaffolding.
📄 src/backend/access/undo/undo_xlog.c (L803-L803)
Deferred loser transaction is tagged with the wrong database OID. During crash recovery the startup process runs PerformUndoRecovery() while not connected to any user database, so MyDatabaseId is InvalidOid (or stale). The logical revert worker spawns per-database workers from ATMCollectUnrevertedDatabases() keyed on dboid, so a deferred transaction recorded with InvalidOid is never serviced -- a loser transaction's rollback is silently dropped (data-integrity hazard). The target database must be recovered from the UNDO batch/record metadata (urec_reloid -> relation's db, or a dboid carried in the batch header), not taken from MyDatabaseId.
📄 src/backend/access/undo/undo_xlog.c (L244-L247)
Release-build buffer-overread risk: the only guard on datalen >= xlrec->tuple_len before the memcpy is an Assert, which is compiled out in production. If this BLK_NEEDS_REDO path is ever reached with a short/corrupt block (datalen < tuple_len), the memcpy reads past the WAL block buffer. The header documents that CLRs always use REGBUF_FORCE_IMAGE (so this restore-from-block-data path should be unreachable in normal operation), which makes the live code arguably dead -- but if it is kept, the bound must be a real runtime check (ereport, not Assert) since it validates untrusted on-disk WAL data.
💡 Suggested change
Before:
data = XLogRecGetBlockData(record, 0,
&datalen);
Assert(data != NULL);
Assert(datalen >= xlrec->tuple_len);
After:
data = XLogRecGetBlockData(record, 0,
&datalen);
if (data == NULL || datalen < xlrec->tuple_len)
elog(PANIC, "UNDO CLR redo: block data too short (%zu < %u)",
datalen, xlrec->tuple_len);
📄 src/backend/access/undo/undo_xlog.c (L88-L88)
Stale comment about a durable limit. MAX_UNDO_LOGS is 100 (src/include/access/undolog.h:60) and the README says 100 in two places, but this comment says 64. An incorrect value for a compile-time limit that bounds a recovery-time scan is misleading; fix to match the actual define.
💡 Suggested change
Before:
* with MAX_UNDO_LOGS=64 this is acceptable at recovery
After:
* with MAX_UNDO_LOGS=100 this is acceptable at recovery
📄 src/backend/access/recno/recno_vm.c (L482-L485)
Aspirational/future-tense comment for behavior that does not exist. Comments must describe what the code does now; a "TODO: In the future ..." block is exactly the stale/aspirational comment style the project rejects. Either implement the timestamp check or drop the TODO and keep just the one-line statement of current (conservative) behavior.
📄 src/backend/access/recno/recno_pbu.c (L123-L125)
Static-state leak window. recno_pbu_prepared is set true in RecnoPbuPrepareInsert(), and the caller then runs RecnoXLogEnsureLogicalImageCxt() (which creates a MemoryContext and can ereport on OOM) before entering the critical section. If an ereport fires between PrepareInsert and the Insert/Finish/Cancel calls, recno_pbu_prepared stays true and the pinned undo buffers remain reserved; the next DML then trips Assert(!recno_pbu_prepared) in RecnoPbuPrepareInsert (or runs with stale state in a non-assert build). Ensure the transaction abort path unconditionally calls RecnoPbuCancelUndo() to reset this static, or move the ereport-capable work before PrepareInsert.
📄 src/backend/access/undo/undoapply.c (L305-L307)
Inconsistent corruption handling between the two walkers of the same chain format. PerformUndoRecovery() in undo_xlog.c PANICs when chain_prev >= batch_lsn (circular/forward-pointing chain) before following it, but this online-abort applier follows batch->header.chain_prev unconditionally. A corrupt or self-pointing chain_prev here produces an infinite loop (reads the same batch forever) or misapplies, rather than failing safely. Add the same monotonic-LSN guard as undo_xlog.c: require the next LSN to be strictly less than the current batch_lsn or invalid.
💡 Suggested change
Before:
/* Follow chain to previous batch */
batch_lsn = batch->header.chain_prev;
UndoFreeBatchData(batch);
After:
/* Follow chain to previous batch */
{
XLogRecPtr next_lsn = batch->header.chain_prev;
if (XLogRecPtrIsValid(next_lsn) && next_lsn >= batch_lsn)
ereport(ERROR,
(errmsg("UNDO rollback: chain_prev %X/%X >= batch_lsn %X/%X; corrupt UNDO chain",
LSN_FORMAT_ARGS(next_lsn),
LSN_FORMAT_ARGS(batch_lsn))));
UndoFreeBatchData(batch);
batch_lsn = next_lsn;
}
📄 src/backend/access/undo/slog.c (L137-L137)
Windows/MSVC: SLogState is a module-global accessed cross-module (e.g. by the AM's visibility code and by slog_tuple.c inline helpers), but its declaration in access/slog_internal.h has no PGDLLIMPORT. An extension or a separately-linked AM referencing SLogState will fail to link on MSVC. The same applies to slog_dsa_max_size_mb here and to SLogNumPartitions/slog_num_partitions (declared in slog_flathash.h / slog.h) -- note SLogNumPartitions is read from the static inline SLogFlatHashPartitionIndex() in a public header, so every including module references the global directly. Mark these externs PGDLLIMPORT in their headers.
📄 src/backend/access/undo/undo.c (L60-L63)
These per-backend engine entry points are already declared in their proper headers (e.g. PbuUndoLogShmemSize in access/perbackend/pbu_undolog.h, PendingUndoShmemSize in pbu_undorequest.h), yet they are re-declared here as local externs "to avoid including the colliding pbu_*.h type headers." Local extern prototypes bypass the compiler's cross-check between declaration and definition, so a future signature change (return type/params) in the definition would compile silently and break at runtime/ABI -- a footgun. The right fix is to resolve the header-name/type collision (split the needed prototypes into a small collision-free header, or fix the include guards/typedefs that collide), not to hand-maintain shadow externs. If kept short-term, add a comment pointing at each real header so the duplication is auditable.
📄 src/backend/access/undo/slog_tuple.c (L563-L570)
Lock-order / contention concern not reflected in the README lock hierarchy. SLogTupleEvictCommitted() calls TransactionIdIsInProgress()/TransactionIdDidCommit() for every op while holding fp->writer_lock LW_EXCLUSIVE; those probes internally acquire ProcArrayLock and CLOG SLRU locks. The README's documented UNDO lock ordering does not place ProcArrayLock/CLOG relative to the sLog writer locks, so this acquisition order (writer_lock -> ProcArrayLock) is undocumented and must be checked for consistency against every other site that takes these locks together. Although this is the full-hash emergency path, it is reachable from the SLogTupleInsert write hot path (via the final fallback), where it serializes all writers to every partition behind ProcArray/CLOG probes. Please document the ordering in the README and confirm no code path takes writer_lock while already holding ProcArrayLock (which would be an inversion).
📄 src/backend/access/undo/undo_xlog.c (L902-L903)
The cached static XLogReaderState is shared by all callers (inline abort in xactundo.c, the logical revert worker, and recovery) and is reused across aborts, but nothing in undoapply.c (the main consumer, via ApplyUndoChainFromWAL) wraps the walk in PG_TRY/PG_FINALLY. If an ERROR is thrown between XLogBeginRead and record consumption -- e.g. an rm_undo callback error, or the proposed corrupt-chain guard in undoapply.c -- the reader is left with a stale segment FD and/or a partially-consumed decode buffer, and the next abort's UndoReadBatchFromWAL reuses it, risking a crash or misread. UndoResetBatchReader() must be called from the PG_CATCH/abort-cleanup of every caller of ApplyUndoChainFromWAL; only logical_revert_worker.c appears to do so. Also document/assert that the reader is single-threaded and non-reentrant (the inline abort path and recovery must never run concurrently on the same backend).
📄 src/include/access/recno_undo.h (L28-L28)
Inaccurate rmid comment. UNDO_RMID 2 is not "reserved out-of-core": it is actively used by FILEOPS (#define UNDO_RMID_FILEOPS 2 in storage/fileops.h, registered via RegisterUndoRmgr(UNDO_RMID_FILEOPS, ...)). And there is no ZHEAP consumer anywhere in the tree, so "6 = ZHEAP" is fictional. This comment will mislead the next person picking a free rmid into a collision with FILEOPS. State the real assignments: 1 NBTREE, 2 FILEOPS, 3 HASH, 5 FLUX, 7 RECNO (4 and 6 unused).
📄 src/include/access/recno_diff.h (L18-L18)
Installed/public access-method headers must not include postgres.h. postgres.h is the backend-only translation-unit prologue that each .c file includes first; pulling it into a header breaks any frontend/extension consumer that includes recno_diff.h after its own prologue (e.g. postgres_fe.h) and double-defines the backend environment. Drop this include; rely on the includer's prologue and add only the specific headers the declarations need (e.g. c.h types via the postgres.h already pulled by the .c). Same defect exists in recno_slog.h and recno_xlog.h.
📄 src/include/access/recno_slog.h (L19-L19)
Installed/public header must not include postgres.h (backend-only prologue). Same issue as recno.h/recno_diff.h/recno_xlog.h: this breaks frontend/extension consumers that include this header after their own prologue. Remove the postgres.h include and include only what the declarations actually require.
📄 src/include/access/recno_xlog.h (L18-L18)
Installed/public header must not include postgres.h (backend-only prologue). Same defect as recno.h/recno_diff.h/recno_slog.h. Remove it; the headers it already pulls (access/recno.h, access/xlogreader.h, etc.) provide the needed types for a backend .c, and this avoids breaking any non-backend includer.
📄 src/include/access/recno_diff.h (L58-L59)
Dead declarations. The inline-diff fast path is gated on !RelationHasUndo(relation) in recno_operations.c, but recno.h hardcodes #define RelationHasUndo(rel) (true), so that branch is permanently unreachable. As a result RecnoComputeTupleDiff, RecnoDiffIsCompact and RecnoApplyInlineDiffReverse (plus the RecnoInlineDiff struct, RECNO_INLINE_DIFF_MAX_BYTES and RECNO_MAX_DIFF_SEGMENTS) have no live caller -- only .gdbinit references them. Only RecnoApplyDiffReverse is actually used (recno_mvcc.c, recno_undo.c). Drop the unused inline-diff machinery (YAGNI) or wire it to a real caller.
📄 src/include/access/recno_slog.h (L74-L74)
Dead export: RecnoSLogRemoveByXidGlobal is defined in recno_slog.c but has no caller anywhere in the backend (only a .gdbinit breakpoint references it). Remove the declaration and definition, or wire it to its intended caller.
📄 src/backend/access/recno/recno_stats.c (L247-L252)
Non-portable format strings. PostgreSQL requires INT64_FORMAT / UINT64_FORMAT for int64/uint64 values rather than %lld/%llu plus (long long)/(unsigned long long) casts; the cast is the exact workaround the project convention forbids (see the same issue already flagged in recno_overflow.c, and note recno_pbu.c correctly uses UINT64_FORMAT). total_pages, total_live_tuples, total_dead_tuples are int64; hlc_min/hlc_max are uint64. Use INT64_FORMAT/UINT64_FORMAT and drop the casts.
📄 src/backend/access/recno/recno_stats.c (L280-L283)
Same non-portable %llu + (unsigned long long) cast for uint64 HLC values. Use UINT64_FORMAT and drop the casts.
📄 src/backend/access/recno/recno_undo.c (L429-L430)
Non-portable %llu + (unsigned long long) cast for UndoRecPtr (a uint64). Although these are errmsg_internal (untranslated) debug/warning messages, they violate the project's UINT64_FORMAT convention and are inconsistent with recno_pbu.c, which correctly uses UINT64_FORMAT for the same type. Replace %llu/(unsigned long long) with UINT64_FORMAT across all seven sites in this file.
📄 src/backend/access/recno/recno_slog.c (L983-L986)
palloc() while holding all RECNO_SLOG_PARTITIONS (128) partition LWLocks. The size (max_keys = RecnoSLogNumEntries()) does not depend on any locked state, so this allocation can and should be hoisted above the lock-acquisition loop. Allocating under 128 held LWLocks needlessly extends the window during which every sLog partition is blocked cluster-wide, and on OOM the ereport unwinds through LWLock cleanup for all 128 locks. Move the palloc before the for (part = 0; part < RECNO_SLOG_PARTITIONS; part++) LWLockAcquire(...) loop.
📄 src/backend/access/recno/recno_slog.c (L293-L296)
Dead code. RecnoSLogShmemSize() has no callers -- shared-memory allocation is done entirely through ShmemRequestHash()/ShmemRequestStruct() in RecnoSLogShmemRequest(). Per minimalism, remove this function (and its prototype in recno_slog.h) rather than ship an unused size estimator that can silently drift out of sync with the real allocation.
📄 src/backend/access/undo/xactundo.c (L924-L926)
The goto inline_undo_done on the UndoValidateBatchLSN-failure path jumps over the MemoryContextSwitchTo(old_ctx) and MemoryContextDelete(undo_ctx) at the end of the PG_TRY/PG_CATCH block (which only run on the validated path). As a result, when the LSN fails validation:
undo_ctx("Inline UNDO Apply") is never deleted -> leaked for the rest of the abort.- CurrentMemoryContext is still
undo_ctxwhen execution falls throughinline_undo_done:intoATMAddAborted()and, after the enclosing blocks, intoUndoRecordSetFree()/ResetXactUndo(). Those run in the wrong (soon-to-be-abandoned) context during a MUST-NOT-FAIL abort path.
Restore the context and deleteundo_ctxbefore the goto target, e.g. move the switch/delete aboveinline_undo_done:and have the validate-failure branch perform them too, or restructure to avoid the goto entirely.
📄 src/backend/access/undo/slog_tuple.c (L443-L447)
Shared-memory under-request. SLogTupleShmemInit() does ShmemAlloc(sizeof(SLogFlatPartition) * SLogNumPartitions) for tuple_partitions, but SLogTupleShmemSize() returns only SLogFlatHashPartitionedShmemSize(...) (the hash copies) and never accounts for this array. SLogFlatPartition embeds an LWLockPadded, so each entry is cache-line sized; with up to SLOG_FLAT_MAX_PARTITIONS (256) partitions this is ~32KB drawn silently from the general shmem freespace. It happens to fit today only because of the slop; a tight shared_memory configuration or more partitions will FATAL at ShmemAlloc. Size and request this array explicitly (e.g. ShmemRequestStruct for the partition array, or add its size to SLogTupleShmemSize and allocate from the requested block).
📄 src/backend/access/undo/undo_bufmgr.c (L234-L237)
Dead parameter / silent data-loss footgun. InvalidateUndoBufferRange() takes last_block but never uses it beyond an Assert: DropRelationBuffers(srel, &forknum, 1, &first_block) drops ALL blocks >= first_block, i.e. to EOF, not the intended [first_block, last_block] range. Any future caller requesting a bounded range will discard live undo past last_block. Combined with the fact that this function (and the rest of undo_bufmgr.c's read/mark/invalidate API) has no caller in the tree, this is speculative scaffolding that encodes a bug. Remove the unused function/parameter, or implement a real bounded drop.
📄 src/backend/access/undo/undo_bufmgr.c (L102-L107)
Dead code / YAGNI. None of this file's exported functions (ReadUndoBuffer, ReadUndoBufferExtended, ReleaseUndoBuffer, UnlockReleaseUndoBuffer, MarkUndoBufferDirty, InvalidateUndoBuffers, InvalidateUndoBufferRange) have any caller in the tree -- the per-backend engine uses pwrite/pread directly (undo_bufmgr.h itself says the buffer path is bypassed). The header's claim that the module is "retained only for the buffer invalidation API used during segment discard" is also false: InvalidateUndoBuffers/Range are uncalled too. Per minimalism rules, drop this file (and its header counterpart) until a consumer actually needs it.
📄 src/backend/access/undo/slog_tuple.c (L918-L921)
Stale/misleading comments: the sLog read side is no longer wait-free. The seqlock was replaced by taking the partition writer_lock in LW_SHARED (see SLOG_SEQ_READ_BEGIN in slog_flathash.h), yet dozens of functions here still carry "WAIT-FREE: uses the seqlock read-side retry loop" headers (SLogTupleLookup, SLogTupleLookupFiltered, SLogTupleGetDirtyXid, SLogTupleGetDirtyWriterXid, SLogTupleGetWriteConflictXid, SLogTupleHasEntry, SLogTupleIterateByTid, etc.). Comments must describe what the code does now; these describe a removed mechanism and misrepresent the locking/contention profile of hot visibility-path reads.
📄 src/backend/access/undo/slog_tuple.c (L1502-L1507)
Dead batching logic following the seqlock removal. SLogSeqWriteBegin/End are now no-ops (slog_flathash.h), so the SLOG_ABORT_PUBLISH_BATCH "end and re-begin the seqlock every N ops to bound the odd-hold window so wait-free readers make progress" code here (and the identical block in SLogTupleCleanupRetained) does nothing: readers hold writer_lock LW_SHARED and are blocked for the whole LW_EXCLUSIVE write regardless of these calls. The loop counter, periodic End/Begin, and their justifying comments are now misleading churn; drop them or restore a real published-progress mechanism.
📄 src/backend/access/undo/xactundo.c (L731-L735)
AtCommit_XactUndo() calls UndoRecordSetFree() (which reparents the recycled context to TopMemoryContext and stashes it in the file-scope UndoRecordReusableContext) but never calls UndoRecordSetResetCache(). AtPrepare_XactUndo() likewise omits it. undorecord.c documents the recycled context as "cleaned up at transaction end by UndoRecordSetResetCache()", a contract only the abort path honors (xactundo.c:817). After a committing transaction the recycled context survives in TopMemoryContext holding up to one buffer's worth of memory (grown up to undo_batch_size_kb). It is bounded to one context so this is not an unbounded leak, but the behavior diverges from the documented per-transaction-cleanup contract. Either call UndoRecordSetResetCache() on commit/prepare, or update the undorecord.c comment to state the cache deliberately persists across transactions and is reset only on abort.
📄 src/backend/access/undo/undoworker.c (L393-L393)
retained_mb is uint64 but is printed via %lu after a cast to unsigned long. On LLP64 platforms (Windows/MSVC) unsigned long is 32-bit, so a retention size exceeding 4 GB (4096 MB) truncates and reports a wrong, smaller value. Use UINT64_FORMAT with the uint64 directly per the portability rules.
💡 Suggested change
Before:
uint64 retained_mb = (write_ptr - new_horizon) >> 20;
After:
uint64 retained_mb = (write_ptr - new_horizon) >> 20;
📄 src/backend/access/undo/undoworker.c (L397-L398)
%lu with (unsigned long) truncates the uint64 retained_mb on LLP64 (Windows/MSVC, where unsigned long is 32-bit): a WAL-retention warning above 4096 MB prints a bogus value. Use UINT64_FORMAT and pass the uint64 unchanged.
💡 Suggested change
Before:
(errmsg("UNDO WAL retention (%lu MB) exceeds undo_max_wal_retention_size (%d MB)",
(unsigned long) retained_mb, undo_max_wal_retention_size),
After:
(errmsg("UNDO WAL retention (" UINT64_FORMAT " MB) exceeds undo_max_wal_retention_size (%d MB)",
retained_mb, undo_max_wal_retention_size),
📄 src/backend/access/undo/undoworker.c (L312-L315)
This SEALED/DISCARDABLE lifecycle pass is dead code under UNDO-in-WAL. No path ever sets log->state = UNDO_LOG_SEALED or writes log->seal_ptr (grep confirms only undo_xlog.c's redo path sets UNDO_LOG_ACTIVE/in_use, never SEALED), so log->state == UNDO_LOG_SEALED is never true and the SEALED->DISCARDABLE->FREE transitions and UndoLogDeleteSegmentFile() (itself a no-op stub) cannot fire. Likewise sealed_log_count is never incremented, so the adaptive-fast-naptime branch keyed off it is dead too. Per YAGNI, drop the seal/rotate/segment-file scaffolding rather than carrying unreachable state machinery into the subsystem.
📄 src/backend/access/undo/undostats.c (L266-L268)
pg_undo_force_discard() is an effectively no-op maintenance function under UNDO-in-WAL and a POLA violation. UndoLogSealAndRotate() and UndoLogDeleteSegmentFile() are no-op stubs; nothing ever sets log->state to SEALED (so the second-pass transitions never fire); and the first-pass relies on log->oldest_xid/insert_ptr which are never advanced in the WAL-based path. The function will reliably return 0 while implying it reclaimed storage. Either wire this to the real WAL-retention discard horizon (UndoSetDiscardHorizon) or drop the function and its pg_proc.dat entry (oid 9412) rather than exposing dead maintenance SQL.
📄 src/backend/access/undo/undostats.c (L114-L120)
GetUndoBufferStats() hardcodes all counters to zero, so the SQL-callable pg_stat_get_undo_buffers (pg_proc.dat oid 9411) always reports num_buffers=0, zero hits/misses, and a 0.0 hit ratio. Shipping a monitoring SRF that returns permanently meaningless data is a POLA violation -- an operator querying it will conclude the UNDO buffer cache is idle/empty. Since the comment itself says UNDO pages are now in shared_buffers and should be inspected via pg_buffercache, remove this function (and its catalog entry) rather than shipping a zero-returning stub.
📄 src/backend/access/undo/undoworker.c (L608-L609)
Use snprintf(..., BGW_MAXLEN, ...) for bgw_library_name/bgw_function_name, matching the convention used everywhere else in the tree (launcher.c, datachecksum_state.c, pbu_undoworker.c) and the snprintf already used two lines below for bgw_name/bgw_type. The literals fit, so this is a consistency/convention fix rather than an overflow risk.
💡 Suggested change
Before:
sprintf(worker.bgw_library_name, "postgres");
sprintf(worker.bgw_function_name, "UndoWorkerMain");
After:
snprintf(worker.bgw_library_name, BGW_MAXLEN, "postgres");
snprintf(worker.bgw_function_name, BGW_MAXLEN, "UndoWorkerMain");
📄 src/backend/access/undo/undoworker.c (L127-L130)
This SIGTERM handler writes a non-atomic shared-memory bool (UndoWorkerShmem->shutdown_requested) from signal context, and the main loop (while (!UndoWorkerShmem->shutdown_requested)) reads it as a plain, non-volatile bool. A flag set from a signal handler and read in a loop must be volatile sig_atomic_t to be async-signal-safe and to prevent the compiler from hoisting the loop condition; a shared-memory bool satisfies neither. UndoWorkerRequestShutdown() also writes the same field under an LWLock, so there are now two inconsistent synchronization regimes for one flag. Follow the standard pattern: use the common SignalHandlerForShutdownRequest (sets volatile sig_atomic_t ShutdownRequestPending) and check it via the normal interrupt path rather than a bespoke shared bool.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1036-L1037)
The rollback hash table key is RollbackHashKey { FullTransactionId full_xid; UndoRecPtr start_urec_ptr; } (16 bytes), and every lookup/insert builds that full key (e.g. lines 1485-1490, 1569-1578). But keysize is set to sizeof(TransactionId) (4 bytes) with HASH_BLOBS, so dynahash only hashes/compares the first 4 bytes of the key. Two problems: (a) start_urec_ptr is excluded from the key, so distinct (xid, start) requests alias to the same bucket; (b) full_xid is a 64-bit FullTransactionId, so only its low 32 bits are compared on little-endian 64-bit platforms, which is both a correctness and portability defect. Set keysize to sizeof(RollbackHashKey). Note that some call sites pass &urinfo->full_xid/&rh->full_xid (e.g. line 1182) rather than a full RollbackHashKey, which must also be reconciled to the real key type.
💡 Suggested change
Before:
info.keysize = sizeof(TransactionId);
info.entrysize = sizeof(RollbackHashEntry);
After:
info.keysize = sizeof(RollbackHashKey);
info.entrysize = sizeof(RollbackHashEntry);
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1096-L1096)
Each memset zeroes only one struct (sizeof(UndoXidQueue)), but the array reserved by UndoXidQueueElemsShmSize() is ROLLBACK_REQUEST_QUEUE_SIZE elements long. Elements 1..1023 are left as uninitialized shmem (ShmemAlloc does not zero), so UndoXidQueueGetFreeElem() and the Size/Error equivalents may fail to find a slot with InvalidFullTransactionId, firing the Assert(false) "queue full" or reusing a garbage slot. Use the per-array sizes: UndoXidQueueElemsShmSize(), UndoSizeQueueElemsShmSize(), UndoErrorQueueElemsShmSize().
💡 Suggested change
Before:
memset(UndoXidQueueElems, 0, sizeof(UndoXidQueue));
After:
memset(UndoXidQueueElems, 0, UndoXidQueueElemsShmSize());
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1100-L1100)
Same under-sized memset for the size queue array: zeroes one struct instead of the full ROLLBACK_REQUEST_QUEUE_SIZE array.
💡 Suggested change
Before:
memset(UndoSizeQueueElems, 0, sizeof(UndoSizeQueue));
After:
memset(UndoSizeQueueElems, 0, UndoSizeQueueElemsShmSize());
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1104-L1104)
Same under-sized memset for the error queue array: zeroes one struct instead of the full ROLLBACK_REQUEST_QUEUE_SIZE array.
💡 Suggested change
Before:
memset(UndoErrorQueueElems, 0, sizeof(UndoErrorQueue));
After:
memset(UndoErrorQueueElems, 0, UndoErrorQueueElemsShmSize());
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1182-L1184)
hash_search() with HASH_FIND can return NULL, but rh->in_progress = false; dereferences it unconditionally on the next line. If the entry was already removed/discarded by a concurrent worker (or never registered), this is a NULL-pointer crash. Guard the result before dereferencing. Also note the key passed here is &urinfo->full_xid, a bare 8-byte xid rather than a RollbackHashKey, which is inconsistent with the real key type (see the keysize issue).
💡 Suggested change
Before:
rh = (RollbackHashEntry *) hash_search(RollbackHT, (void *) &urinfo->full_xid,
HASH_FIND, NULL);
rh->in_progress = false;
After:
rh = (RollbackHashEntry *) hash_search(RollbackHT, (void *) &urinfo->full_xid,
HASH_FIND, NULL);
if (rh == NULL)
{
LWLockRelease(RollbackRequestLock);
return false;
}
rh->in_progress = false;
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L700-L704)
GetRollbackHashKeyFromQueue() has a side effect only in the ERROR_QUEUE branch: it increments the module-global cur_undo_queue when that queue is empty. The other two branches do not, and the round-robin loop in UndoGetWork() already advances cur_undo_queue itself. Worse, the peek loop (UndoGetWork, allow_peek path) calls this helper with its own local cur_queue iterator, so this hidden mutation of the global corrupts the round-robin cursor shared with the non-peek path. Remove the stray increment from the helper and let callers own cur_undo_queue.
💡 Suggested change
Before:
if (GetErrorQueueSize() <= n)
{
cur_undo_queue++;
return false;
}
After:
if (GetErrorQueueSize() <= n)
return false;
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L85-L87)
binaryheap_remove_nth_unordered() reaches directly into bh_nodes/bh_size/bh_has_heap_property, duplicating lib/binaryheap internals in a consumer. master's binaryheap layout is not a stable contract for direct field pokes; if it changes this silently breaks. Prefer adding this helper (and the shmem sizing/accessor shims above) to lib/binaryheap.c, or at minimum assert the heap is non-empty before accessing bh_size-1 to avoid an out-of-bounds read when n refers to the only element.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L14-L14)
Typos in declaration-site comments/headers: "oldesdXidHavingUndo" (should be oldestXidHavingUndo), "rollabck" (rollback), "accomodate"/"accommodate". These are in header/declaration comments, not call sites.
💡 Suggested change
Before:
* to move oldesdXidHavingUndo forward. A size-based queue which will help
After:
* to move oldestXidHavingUndo forward. A size-based queue which will help
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L139-L139)
Typo: "rollabck" should be "rollback".
💡 Suggested change
Before:
/* This is the hash table to store all the rollabck requests. */
After:
/* This is the hash table to store all the rollback requests. */
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1584-L1587)
Typos in this comment: "resuests" should be "requests", and "don't not want" is a double negative; it should read "do not want".
💡 Suggested change
Before:
* To check if the rollback requests in the hash table are all
* completed or not. This is required because we don't not want to
* expose RollbackHT in xact.c, where it is required to ensure
* that we push the resuests only when there is some space in
After:
* To check if the rollback requests in the hash table are all
* completed or not. This is required because we do not want to
* expose RollbackHT in xact.c, where it is required to ensure
* that we push the requests only when there is some space in
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L930-L930)
Typo in error-message/log context: "wrokers" should be "workers".
💡 Suggested change
Before:
* Returns true, if we can push the rollback request to undo wrokers, false,
After:
* Returns true, if we can push the rollback request to undo workers, false,
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L9-L9)
Stale IDENTIFICATION path and copyright year. The file lives at src/backend/access/undo/perbackend/pbu_undorequest.c, but the header claims "src/backend/access/undo/undorequest.c" and "1996-2019". Update to the real path and current year to satisfy the comment-accuracy rule.
💡 Suggested change
Before:
* src/backend/access/undo/undorequest.c
After:
* src/backend/access/undo/perbackend/pbu_undorequest.c
📄 src/backend/access/undo/logical_revert_worker.c (L241-L242)
Head-of-line starvation: ATMGetNextUnreverted (-> SLogTxnGetNextUnreverted) always returns the OLDEST unreverted ATM entry in ascending xid order, for ANY database, and never advances a cursor. When that oldest entry belongs to a different database, this goto sleep makes the worker spin without progress: the next scan returns the SAME foreign entry again, so entries behind it for this worker's own database are never reverted. Combined with the launcher capping at max_logical_revert_workers (default 2), databases whose entries sit behind a foreign head entry can be starved indefinitely, silently breaking the guaranteed-rollback claim. ATMGetNextUnreverted needs to skip to the next entry matching MyDatabaseId (e.g. accept a starting xid / filter by dboid) rather than always returning the global minimum.
📄 src/backend/access/undo/logical_revert_worker.c (L563-L564)
The ceiling counts tracked databases, not live workers. Carried-forward (already-known) dbs are appended to next_known before this check, so list_length(next_known) reflects how many dbs are tracked, not how many workers are actually running. A db whose worker died and was NOT auto-restarted by the postmaster (e.g. a non-restartable exit, or restart suppressed) stays in known_dbs forever and is never re-registered, so its ATM entries stall. There is no liveness check against the BackgroundWorkerHandle returned by StartLogicalRevertWorker; the launcher assumes bgw_restart_time=60 always revives the worker. Track handles and verify liveness (GetBackgroundWorkerPid) before carrying a db forward.
📄 src/backend/access/undo/perbackend/pbu_undoinsert.c (L503-L506)
This Assert checks the wrong variable. The loop indexes idx_undo_buffers[i] (buf_idx = xact_urec_info[idx].idx_undo_buffers[i]) and increments i each iteration; idx is the record-slot parameter and never changes in this loop. So Assert(idx < MAX_BUFFER_PER_UNDO) can never catch an overrun of idx_undo_buffers[] (which is indexed by i and sized MAX_BUFFER_PER_UNDO). The analogous InsertPreparedUndo loop correctly asserts the loop counter (bufidx). Should be Assert(i < MAX_BUFFER_PER_UNDO).
📄 src/backend/access/undo/perbackend/pbu_discardworker.c (L60-L60)
got_SIGTERM is set from a signal handler (undoworker_sigterm_handler) and read in the main while(!got_SIGTERM) loop, so it must be volatile sig_atomic_t, not plain bool. A plain bool is not guaranteed async-signal-safe and the compiler may cache it in a register, so the worker can miss SIGTERM and never shut down. hibernate/wait_time are only touched in the main loop so they are fine, but got_SIGTERM is the shutdown flag and is a correctness hazard.
📄 src/backend/access/undo/perbackend/pbu_discardworker.c (L9-L10)
Stale vendored header: the IDENTIFICATION path is wrong (this file is src/backend/access/undo/perbackend/pbu_discardworker.c, not src/backend/postmaster/discardworker.c), the file-title comment still says "discardworker.c", and the copyright is "(c) 1996-2019". Update the IDENTIFICATION, title, and copyright year to match this tree's conventions; the master header is "1996-".
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L15-L15)
Stale vendored IDENTIFICATION path and copyright: this file is src/backend/access/undo/perbackend/pbu_undolog.c (not src/backend/access/undo/undolog.c) and the copyright says "(c) 1996-2019". A comment in ResetUndoLogs also references "a bug elsewhere in zheap code"; this tree is not zheap, so such references are inaccurate per project conventions. Fix the header path/year and scrub zheap-specific references.
📄 src/backend/access/undo/perbackend/pbu_recovery.c (L342-L345)
Misleading type/name: MakeUndoRecPtr(logno, log->meta.insert) returns a full UndoRecPtr (logno in the high bits | offset), but it is stored in a variable named next_insert typed UndoLogOffset, and the (UndoLogOffset) cast is a no-op (both are uint64) that does NOT strip the logno. The subsequent UndoRecPtrGetPageOffset()/subtraction and the final end_ptr assignment happen to be correct because end_ptr is a full UndoRecPtr and the header subtraction never crosses into the logno bits, but the name/type/cast suggest an offset and will mislead maintainers. Rename to end_urp and type it UndoRecPtr, dropping the bogus cast.
📄 src/backend/access/undo/perbackend/pbu_undoaction.c (L6-L9)
Stale vendored header: title says "undoaction.c" / wrong module and the copyright is "(c) 1996-2019". Also note the title comment block above is "undoaction.c" which does not match this file (pbu_recovery.c). Align the title, IDENTIFICATION, and copyright year with this tree's conventions.
📄 src/backend/access/undo/perbackend/pbu_undorecord.c (L22-L27)
InsertUndoRecord/UnpackUndoRecord carry multi-page continuation state through file-scope static workspaces (work_hdr/work_rd/work_blk/work_txn/work_payload). This is non-reentrant: any nested or interleaved undo encode/decode on the same backend (e.g. an UndoFetchRecord driven unpack running between two InsertUndoRecord continuation calls, recursion, or a CFI-triggered callback) silently clobbers the in-flight record. The header comment even states "UnpackUndoRecord is not called between successive calls to InsertUndoRecord" but nothing enforces it, and the Assert block only checks the uur fields match, not that no interleaving occurred. The current callers (single-record insert/unpack loops) happen not to interleave, but this is a footgun; consider making the workspace a caller-provided struct passed through the API.
📄 src/backend/access/undo/perbackend/pbu_undorecord.c (L412-L419)
Zero-copy branch: when !copy_data && !is_undo_splited and payload+tuple fit on the page, uur_payload.data/uur_tuple.data are pointed directly into the page buffer and the function returns without ever reading or validating the trailing uint16 undo_len that InsertUndoRecord appends. This makes uur a borrowed view that is only valid while the caller keeps the buffer pinned (UndoGetOneRecord unlocks but keeps the pin for this case); callers that release the pin before using the data get a use-after-unpin dangling pointer. The record length written at the end is never checked on read, so corruption cannot be detected here. Document the pin-lifetime contract on copy_data explicitly and consider validating undo_len.
📄 src/backend/access/undo/perbackend/pbu_undorecord.c (L308-L308)
Typos: "bookkeeeping" (three e's) and "infrormation". Should be "bookkeeping information".
💡 Suggested change
Before:
/* Update bookkeeeping infrormation. */
After:
/* Update bookkeeping information. */
📄 src/backend/access/undo/perbackend/pbu_undorecord.c (L9-L9)
Stale IDENTIFICATION path and copyright year. File is at src/backend/access/undo/perbackend/pbu_undorecord.c, not "src/backend/access/undo/undorecord.c"; and the 2019 copyright should reflect the current year. Update to satisfy the comment-accuracy rule.
💡 Suggested change
Before:
* src/backend/access/undo/undorecord.c
After:
* src/backend/access/undo/perbackend/pbu_undorecord.c
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L854-L855)
kill(w->proc->pid, ...) and SetLatch(&w->proc->procLatch) dereference w->proc without a NULL check. A slot can be in_use with proc==NULL during the window between UndoWorkerLaunch() (sets in_use=true, proc=NULL) and the worker's UndoWorkerAttach() (sets proc=MyProc). In practice lingering is only set after attach, so lingering implies proc!=NULL today, but that invariant is implicit and fragile. Add an explicit w->proc != NULL guard in both loops to be defensive against a future reordering.
💡 Suggested change
Before:
if (w->in_use && w->lingering && w->dbid != dbid)
kill(w->proc->pid, SIGTERM);
After:
if (w->in_use && w->lingering && w->proc != NULL && w->dbid != dbid)
kill(w->proc->pid, SIGTERM);
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L439-L444)
Misleading copy-paste comment from logicalrep: this is the undo worker's on-exit callback, not the logical replication worker's. State what it actually does.
💡 Suggested change
Before:
* Cleanup function.
*
* Called on logical replication worker exit.
*/
static void
UndoWorkerOnExit(int code, Datum arg)
After:
* Cleanup function.
*
* Called on undo apply worker exit.
*/
static void
UndoWorkerOnExit(int code, Datum arg)
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L699-L699)
Typos: "wroker" should be "worker". There is also a second occurrence just below ("the worker request queue" context in UndoWorkerGetSlotInfo / PbuUndoWorkerMain comment uses "wroker").
💡 Suggested change
Before:
* Get the dbid where the wroker should connect to and get the worker
After:
* Get the dbid where the worker should connect to and get the worker
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L10-L10)
Stale IDENTIFICATION path. This file is at src/backend/access/undo/perbackend/pbu_undoworker.c, not "src/backend/postmaster/undoworker.c". Update the path and the 2019 copyright year.
💡 Suggested change
Before:
* src/backend/postmaster/undoworker.c
After:
* src/backend/access/undo/perbackend/pbu_undoworker.c
📄 src/backend/access/undo/perbackend/pbu_undoaction.c (L3-L4)
Stale vendored header: title "undoaction.c" and IDENTIFICATION "src/backend/access/undo/undoaction.c" do not match this file (pbu_undoaction.c under perbackend/), and the copyright is "(c) 1996-2019". Fix the title, IDENTIFICATION path, and copyright year to this tree's conventions. (My earlier same-note comment was mis-anchored to pbu_recovery.c, whose header is actually correct; this is the file that needs the fix.)
📄 src/backend/access/undo/perbackend/pbu_undoactionxlog.c (L10-L11)
Stale vendored header: title "undoactionxlog.c" and IDENTIFICATION "src/backend/access/undo/undoactionxlog.c" do not match this file (pbu_undoactionxlog.c under perbackend/), and copyright is "(c) 1996-2019". Correct the title/path/year.
📄 src/backend/access/undo/perbackend/pbu_undodiscard.c (L9-L9)
Stale vendored header: title "undodiscard.c" and IDENTIFICATION "src/backend/access/undo/undodiscard.c" do not match this file (pbu_undodiscard.c under perbackend/), and copyright is "(c) 1996-2019". The in-body comment also references the zheap-only symbol lazy_vacuum_zheap_rel, which does not exist in this tree. Fix the header path/year and scrub the zheap-specific reference.
📄 src/backend/access/undo/perbackend/pbu_undoinsert.c (L9-L9)
Stale vendored header: title "undoinsert.c" and IDENTIFICATION "src/backend/access/undo/undoinsert.c" do not match this file (pbu_undoinsert.c under perbackend/), and copyright is "(c) 1996-2019". Correct the title/path/year.
📄 src/include/access/logical_revert_worker.h (L25-L25)
Backend headers must not include postgres.h; the including .c file is responsible for including it first (the only legitimate exceptions in-tree are the precompiled-header and vendored/generated files). Remove this include and let callers include postgres.h before this header. Only block.h/datatype types used by the declarations here should be included if actually needed; the declarations in this header (Size, Datum, Oid) come from c.h/postgres.h already pulled in by the .c file.
📄 src/include/access/logical_revert_worker.h (L40-L41)
These extern GUC backing variables are referenced from other modules (and are candidates for extension use). On Windows/MSVC an extern variable read across a module boundary needs PGDLLIMPORT in its declaration, matching how other GUC externs are declared in headers (e.g. guc-exposed ints). Add PGDLLIMPORT to both.
💡 Suggested change
Before:
extern int logical_revert_naptime;
extern int max_logical_revert_workers;
After:
extern PGDLLIMPORT int logical_revert_naptime;
extern PGDLLIMPORT int max_logical_revert_workers;
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1287-L1290)
Self-deadlock: UndoGetWork() already holds RollbackRequestLock in LW_EXCLUSIVE mode (acquired at the top of the function), but RollbackHTRemoveEntry() unconditionally does LWLockAcquire(RollbackRequestLock, LW_EXCLUSIVE) again. LWLocks are not reentrant, so this path hangs the backend whenever a queued request's database no longer exists. Remove the entry directly here (while the lock is held) via a lock-free hash_search helper instead of calling the self-locking RollbackHTRemoveEntry().
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1277-L1279)
StartTransactionCommand()/CommitTransactionCommand() are invoked while holding RollbackRequestLock in LW_EXCLUSIVE mode. Starting/committing a transaction is heavyweight (it takes other LWLocks, e.g. ProcArrayLock/XidGenLock, allocates memory, and can run deferred triggers/invalidation), so running it under a contended module LWLock both inverts the normal lock ordering (deadlock risk) and holds the queue lock for an unbounded time. The database-existence check should be performed without holding RollbackRequestLock.
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L598-L602)
max_undo_workers is a hardcoded static int = 5, never registered as a GUC (no entry in guc_tables.c references it). Yet this errhint tells the operator to "Raise max_undo_workers and restart" -- there is no knob to raise. Either wire max_undo_workers up as a real GUC (the comment at its declaration calls it a "GUC parameter") or correct this hint so it does not promise a non-existent setting; otherwise recovered rollback requests silently cannot be processed with no actionable remedy.
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L63-L66)
The comment claims these are "GUC parameters", but max_undo_workers and undo_worker_quantum_ms are plain file-scope statics with hardcoded defaults; neither is registered in guc_tables.c, so they cannot be configured. This is dead/misleading configurability (and the comment is inaccurate). Either register them as GUCs or drop the "GUC parameters" claim and the associated errhint that tells users to change max_undo_workers.
📄 src/backend/access/undo/xactundo.c (L787-L789)
Stale comment: it describes behavior that no longer exists. This module's own header states UNDO data lives in the WAL stream (UNDO-in-WAL); there is no "append-only I/O", no "sync UNDO files", and no "segment file". The function body actually relies on WAL (XLogFlush + ApplyUndoChainFromWAL / ATM), as the inline comments below correctly explain. Rewrite this header to describe the WAL-based abort path to avoid misleading future readers.
📄 src/backend/access/undo/xactundo.c (L717-L720)
Comment drifted from the code. UndoRecordSetCreate() deliberately allocates the record set in TopTransactionContext (see its own comment justifying that choice), not CurTransactionContext. The claim here that "The record sets are allocated in CurTransactionContext" is factually wrong and contradicts the allocation site. Fix the reference to TopTransactionContext.
📄 src/backend/access/undo/undoinsert.c (L123-L123)
uset->buffer_size has type Size (64-bit), but XLogRegisterData() takes a uint32 len parameter, so this is a silent narrowing conversion. The adjacent comment is also inaccurate: it conflates xloginsert.c's internal uint64 mainrdata_len accumulator with the function's uint32 len argument -- the main data chain is capped at PG_UINT32_MAX, not unlimited. In normal operation buffer_size is bounded well below 4GB by the deferred-batch limits, so this is defensive, but an explicit bound/cast and a corrected comment would make the uint32 contract clear and portable.
📄 src/backend/access/undo/undoworker.c (L576-L578)
Dead code: the WaitLatch above requests WL_EXIT_ON_PM_DEATH, which causes the wait-set machinery to call proc_exit(1) internally on postmaster death and never return WL_POSTMASTER_DEATH to the caller. This if (rc & WL_POSTMASTER_DEATH) branch can therefore never be taken (and rc is otherwise unused). Drop it to match the WL_EXIT_ON_PM_DEATH convention used elsewhere in the tree.
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L691-L693)
PbuUndoWorkerMain installs only pqsignal(SIGTERM, die) and never a SIGHUP handler (the SIGHUP handler UndoLauncherSighup is installed only in the launcher at UndoLauncherMain). Consequences: (1) a SIGHUP delivered to this worker during pg_ctl reload uses the default disposition and terminates the worker instead of reloading config; (2) got_SIGHUP is never set here, so the if (got_SIGHUP) ProcessConfigFile(...) block below is dead code. Install a SIGHUP handler (e.g. SignalHandlerForConfigReload / PostgresSigHupHandler) in this worker.
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L761-L762)
last_xact_processed_at is a file-scope static left at 0 when the worker's first UndoGetWork() returns no work (or only other-DB work). On the first no-work loop iteration, timeout = TimestampTzPlusMilliseconds(0, UNDO_WORKER_LINGER_MS) is far in the past, so the worker proc_exit(0)s immediately instead of lingering for UNDO_WORKER_LINGER_MS as intended, defeating the restart-avoidance optimization. Initialize last_xact_processed_at to the worker start time (started_at) before the loop.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L256-L261)
SetErrorQueueElem() -- the only setter for error-queue elements -- never writes start_urec_ptr, leaving it at its zeroed value. But UndoErrorQueue has a start_urec_ptr field that is used to build the rollback hash key: GetRollbackHashKeyFromQueue()'s ERROR_QUEUE branch and RemoveOldElemsFromErrorQueue() both read elem->start_urec_ptr into hkey->start_urec_ptr. Because the rollback hash key is {full_xid, start_urec_ptr}, every error-queue element looks up with start_urec_ptr == 0 and therefore never matches its real hash entry. Consequence: error-queue entries are treated as dangling and dropped by cleanup, and are never dispatched by UndoGetWork(), so a rollback that failed once is silently never retried. Add start_urec_ptr to SetErrorQueueElem and pass urinfo->start_urec_ptr from PushErrorQueueElem.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1150-L1155)
Format/argument type mismatch: UndoRecPtrFormat expands to "%016" INT64_MODIFIER "X", which on LP64 platforms (int64 == long) uses the "l" length modifier and thus expects an unsigned long, but the argument is cast to (unsigned long long). This triggers -Wformat warnings and is technically undefined on platforms where unsigned long != unsigned long long. Pass urinfo->start_urec_ptr/end_urec_ptr without the unsigned-long-long cast (UndoRecPtr already matches INT64_MODIFIER's width). Same mismatch recurs in InsertRequestIntoErrorUndoQueue below.
📄 src/backend/access/undo/perbackend/pbu_undofile.c (L432-L433)
Off-by-one in the per-segment clamp: the number of blocks remaining in the current segment from offset off = blocknum % UNDOSEG_SIZE is UNDOSEG_SIZE - off, not 1 + UNDOSEG_SIZE - off. The spurious +1 lets nflush extend one block past the segment boundary, so FileWriteback is asked to flush a byte range beyond the current segment file. Compare md.c's mdwriteback (RELSEG_SIZE - (blocknum % RELSEG_SIZE)) and undofile_prefetch above (UNDOSEG_SIZE - (blocknum % UNDOSEG_SIZE)), both without the +1. Remove the +1.
💡 Suggested change
Before:
nflush = Min(nblocks,
1 + UNDOSEG_SIZE - (blocknum % UNDOSEG_SIZE));
After:
nflush = Min(nblocks,
UNDOSEG_SIZE - (blocknum % UNDOSEG_SIZE));
📄 src/backend/access/undo/logical_revert_worker.c (L334-L337)
Dead/contradictory postmaster-death handling: WaitLatch is given WL_EXIT_ON_PM_DEATH, which makes WaitLatch itself proc_exit() on postmaster death and never return WL_POSTMASTER_DEATH. The subsequent if (rc & WL_POSTMASTER_DEATH) proc_exit(1) is therefore unreachable. Either use WL_POSTMASTER_DEATH + the explicit check (as LogicalRevertLauncherMain does), or drop the dead check. The two should be consistent.
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L2468-L2468)
Spelling error in user-visible stat output: "" should be "". This string is returned by pbu_pg_stat_get_undo_logs for an unrecognized persistence level.
💡 Suggested change
Before:
log->meta.persistence == UNDO_TEMP ? "temporary" : "<uknown>");
After:
log->meta.persistence == UNDO_TEMP ? "temporary" : "<unknown>");
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L2471-L2472)
Format/argument type mismatch: UndoRecPtrFormat expands to "%016" INT64_MODIFIER "X", whose length modifier matches int64 (e.g. "l" on LP64 platforms where int64 is long), but the argument is cast to (unsigned long long). On platforms where INT64_MODIFIER != "ll" this is a -Wformat mismatch. Cast the argument to uint64 (matching INT64_MODIFIER) or drop UndoRecPtrFormat and use a literal "%016llX" with the (unsigned long long) cast. Same issue at the insert/end lines just below.
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L1394-L1396)
Format/argument type mismatch: the path is built with "%016" INT64_MODIFIER "X" (modifier matching int64) but the argument is cast to (unsigned long long). On a platform where int64 is long (INT64_MODIFIER == "l") this is a -Wformat mismatch between unsigned long long and the expected unsigned long. Cast to uint64 to match INT64_MODIFIER, or use a literal "%016llX". The read side in StartupUndoLogs has the identical construct.
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L1262-L1267)
Stale/aspirational comment: it describes a base-backup guard ("If a base backup is in progress, we can't delete any checkpoint snapshot files...") but no such check exists in this function -- it unconditionally proceeds to the retention comparison. Either implement the guard or remove the misleading comment so it describes what the code actually does.
📄 src/include/access/undoworker.h (L49-L50)
undo_worker_naptime is a global int defined in undolog.c and read from undoworker.c (a different translation unit). On Windows/MSVC an extern variable referenced across modules must be marked PGDLLIMPORT, or the backend/extensions fail to link. Add the marker.
(Confidence: high — verified the definition lives in undolog.c and is read in undoworker.c.)
💡 Suggested change
Before:
/* GUC parameters */
+extern int undo_worker_naptime;
After:
/* GUC parameters */
extern PGDLLIMPORT int undo_worker_naptime;
📄 src/include/access/undoworker.h (L62-L62)
UndoWorkerRequestShutdown() is declared here and defined in undoworker.c, but has no callers anywhere in the tree. Dead API — drop the declaration and definition (YAGNI), or wire up the intended caller.
(Confidence: high — searched all .c files; only the definition exists, no call site.)
📄 src/include/access/xactundo.h (L140-L140)
undo_instant_abort_threshold is a global int defined in xactundo.c and read cross-module. Any extern variable referenced from another translation unit (or an extension) needs PGDLLIMPORT to link on Windows/MSVC. Add the marker.
Separately: the comment calls this a "GUC", but it is not registered via guc_tables.c or DefineCustomIntVariable, so it cannot actually be set by the user — either register it as a real GUC or fix the comment.
(Confidence: high for PGDLLIMPORT; high that no GUC registration exists — searched guc_tables.c and all DefineCustomIntVariable sites.)
💡 Suggested change
Before:
+extern int undo_instant_abort_threshold;
After:
extern PGDLLIMPORT int undo_instant_abort_threshold;
📄 src/include/access/xactundo.h (L126-L126)
GetCurrentXactUndoRecPtr() is declared here and defined in xactundo.c but has no callers anywhere. Dead API — remove it (YAGNI).
(Confidence: high — searched all .c/.h files; only declaration + definition, no call site. Its sibling GetCurrentXactLastBatchLSN is used, this one is not.)
📄 src/include/access/undolog.h (L139-L139)
UndoLogShared is a plain extern, but it is read/written from many separate translation units (undo_xlog.c, undostats.c, undoworker.c, etc.). On Windows/MSVC an exported global used cross-module (and by extensions) must be marked PGDLLIMPORT, exactly as the sibling header transam.h does for TransamVariables. Without it the Windows build / extension linkage breaks. (high confidence)
💡 Suggested change
Before:
extern UndoLogSharedData *UndoLogShared;
After:
extern PGDLLIMPORT UndoLogSharedData *UndoLogShared;
📄 src/include/access/undolog.h (L147-L151)
These GUC globals are referenced from other modules (e.g. undo_batch_size_kb in nbtree_undo.c and xactundo.c). Cross-module extern variables need PGDLLIMPORT for Windows/MSVC and extensions. (high confidence)
💡 Suggested change
Before:
extern int undo_worker_naptime;
extern int undo_rollback_overflow_size;
extern int undo_max_wal_retention_size;
extern int undo_batch_size_kb;
extern int undo_batch_record_limit;
After:
extern PGDLLIMPORT int undo_worker_naptime;
extern PGDLLIMPORT int undo_rollback_overflow_size;
extern PGDLLIMPORT int undo_max_wal_retention_size;
extern PGDLLIMPORT int undo_batch_size_kb;
extern PGDLLIMPORT int undo_batch_record_limit;
📄 src/include/access/undolog.h (L186-L194)
These are declared "legacy no-op stubs ... retained for callers not yet fully updated," but a tree-wide search finds no callers for any of them (UndoLogSync, UndoLogCloseFiles, ExtendUndoLogFile, ExtendUndoLogSmgrFile, UndoFlushGetMaxWritePtr, UndoFlushResetMaxWritePtr, UndoLogSealAndRotate, UndoLogDeleteSegmentFile, UndoLogTryPressureDiscard). Per YAGNI/minimal-diff discipline, dead API that exists only "for callers not yet fully updated" should be removed rather than shipped as speculative scaffolding. (high confidence)
📄 src/include/access/undormgr.h (L107-L107)
UndoRmgrs[] is defined in undormgr.c and read via GetUndoRmgr()/dispatch from other modules. Exported globals used cross-module/by extensions need PGDLLIMPORT on Windows/MSVC. (high confidence)
💡 Suggested change
Before:
extern const UndoRmgrData *UndoRmgrs[MAX_UNDO_RMGRS];
After:
extern PGDLLIMPORT const UndoRmgrData *UndoRmgrs[MAX_UNDO_RMGRS];
📄 src/include/access/undormgr.h (L48-L48)
This comment is self-contradictory with undormgrlist.h in the same change. It claims IDs 2/4/5 are "reserved by out-of-core consumers," yet undormgrlist.h registers FileopsUndoRmgrInit (UNDO_RMID_FILEOPS=2, fileops.h) and FluxUndoRmgrInit (UNDO_RMID_FLUX=5, flux_undo.h) as in-core built-ins. Also RECNO uses UNDO_RMID_RECNO=7 (recno_undo.h), which is not documented here at all, and ID 4 has no consumer. Since these IDs are stamped into WAL-durable records, the authoritative map must be accurate: either keep the full in-core allocation here (1 nbtree, 2 fileops, 3 hash, 5 flux, 7 recno) or stop calling them "out-of-core." (high confidence)
📄 src/include/access/undo_xlog.h (L225-L227)
UndoPersistenceLevel is an untyped enum (undodefs.h), so its storage size is implementation-defined. Embedding it directly as the trailing field of the WAL-durable xl_undo_batch with SizeOfUndoBatch = offsetof(persistence) + sizeof(UndoPersistenceLevel) makes the on-disk WAL record width compiler-dependent: undoinsert.c writes the struct via XLogRegisterData((char *) &xlrec, SizeOfUndoBatch) and undo_xlog.c reads it back with memcpy(&result->header, xlrec, SizeOfUndoBatch). A compiler that sizes this enum differently (or a future value forcing a wider type) would silently change the record format. Use a fixed-width field (e.g. uint8 persistence) for the persisted value. (medium confidence)
📄 src/include/access/undo_xlog.h (L141-L151)
xl_undo_apply is a WAL-durable record but mixes RelFileLocator, BlockNumber, OffsetNumber, and uint16 fields that introduce implicit padding, and the AMs serialize/deserialize it via whole-struct writes (XLogRegisterData((char *) &xlrec, SizeOfUndoApply) and corresponding memcpy on redo). Relying on struct layout/padding for an on-disk record is a portability hazard across architectures/compilers. Consider ordering fields to avoid padding (place the two uint16 fields adjacent and before/after the 4-byte fields deliberately) and/or document that the serialization intentionally includes padding bytes that must be identical on all supported platforms. (medium confidence)
📄 src/include/access/undo_bufmgr.h (L195-L196)
This entire buffer read/release/invalidation API (ReadUndoBuffer, ReadUndoBufferExtended, ReleaseUndoBuffer, UnlockReleaseUndoBuffer, MarkUndoBufferDirty, UndoMakeBufferTag, IsUndoBufferTag, InvalidateUndoBuffers, InvalidateUndoBufferRange) has no callers anywhere in the tree outside undo_bufmgr.c itself. The header's own prose says UNDO is written/read via pwrite/pread "bypassing shared_buffers entirely," and the invalidation API it claims is "retained" also has zero callers. This is dead, speculative scaffolding (plus the matching undo_bufmgr.c and the UNDO_DB_OID/UndoLogForkNum machinery). Per YAGNI it should be dropped from the patch. (high confidence)
📄 src/include/access/undo_bufmgr.h (L251-L253)
Guarding these declarations behind #ifdef BUFMGR_INTERNALS_H is fragile: a translation unit that includes undo_bufmgr.h before buf_internals.h silently loses the declarations, risking implicit-declaration/ODR surprises instead of a clean error. Since the only implementation file (undo_bufmgr.c) includes buf_internals.h unconditionally and no other caller exists, this conditional buys nothing; prefer including the needed header or declaring these unconditionally. (medium confidence)
📄 src/include/access/undodefs.h (L20-L20)
The file comment claims these definitions can be "included in both frontend and backend code," but undodefs.h relies on uint64/uint16 without including any header that provides them, and no frontend/common/bin TU includes it. If genuinely intended for frontend use it should include c.h (or be included only after it); otherwise drop the inaccurate "frontend" claim. (low confidence)
📄 src/include/access/undoworker.h (L49-L50)
Duplicate extern int undo_worker_naptime; declaration. This same extern already exists in undolog.h (line 147), which this header includes directly (line 21 #include "access/undolog.h"). Declaring it again here is redundant and risks the two declarations drifting out of sync (e.g. when the PGDLLIMPORT marker is added to only one). Drop this line and rely on the declaration in undolog.h. (Confidence: high -- verified both declarations and the include.)
📄 src/include/access/perbackend/pbu_compat.h (L87-L89)
These #ifndef WAIT_EVENT_* guards never fire. Wait events are generated as typedef enum constants in wait_event_types.h (from wait_event_names.txt), not as preprocessor #defines, so the preprocessor cannot see them and the guards always pass -- these #defines unconditionally take effect and override the real enum constants for the whole TU.
This is an active bug, not a cosmetic alias: WAIT_EVENT_BGWORKER_STARTUP is a real enum value already present upstream (used in parallel.c, bgworker.c, launcher.c and in pbu_undoworker.c:222). Redefining it to WAIT_EVENT_BGWORKER_SHUTDOWN means the pbu undo worker reports "shutting down" in pg_stat_activity.wait_event while it is actually starting up -- the semantically opposite event. The #ifndef is dead; the alias is wrong.
For the events that genuinely do not exist (e.g. WAIT_EVENT_UNDO_CHECKPOINT_*, WAIT_EVENT_UNDO_LAUNCHER_MAIN), the correct fix is to add them to wait_event_names.txt so the enum is generated, not to alias to an unrelated enum via a guard that never triggers. At minimum, drop the WAIT_EVENT_BGWORKER_STARTUP alias entirely since that event already exists.
📄 src/include/access/perbackend/pbu_compat.h (L198-L200)
Unconditionally #define-aliasing widely-used master identifiers (RelFileNode, ShmemVariableCache, GetOldestXmin, RBM_ZERO, ONCOMMIT_TEMP_DISCARD, target_prefetch_pages) is a footgun. These macros remain in effect for the entire translation unit after this header is included. Because this header is included early (e.g. flux_pbu.c line 63, before several access/*/storage/* headers), any master header pulled in after it has its uses of these names silently rewritten -- changing semantics (a legitimate RBM_ZERO user would suddenly take a content lock) or breaking compilation. The vendoring would be far safer if the renamed identifiers were fixed at their (few) use sites in the pbu .c files rather than globally aliased. At minimum, document the hard requirement that pbu_compat.h must be the last include, and verify no master header appears after it in any pbu TU.
📄 src/include/access/perbackend/pbu_compat.h (L149-L151)
These three tranche-ID macros are dead code. The actual implementation (pbu_undolog.c) initializes every pbu LWLock with LWTRANCHE_UNDO_LOG_PBU, which is registered the canonical way via PG_LWLOCKTRANCHE(UNDO_LOG_PBU, UndoLogPBU) in lwlocklist.h. Nothing references LWTRANCHE_UNDOLOG/LWTRANCHE_UNDODISCARD/LWTRANCHE_DISCARD_UPDATE. Remove them -- besides being unused, hardcoded fixed offsets from LWTRANCHE_FIRST_USER_DEFINED can collide with other modules' dynamically-allocated tranche IDs.
📄 src/include/access/perbackend/pbu_compat.h (L43-L45)
Hardcoding INT64_MODIFIER "ll" is non-portable and non-idiomatic. int64 is not long long on every platform/ABI the tree targets, so "ll" can mismatch the actual width (and compilers would warn under -Wformat). Master's portable idiom is INT64_FORMAT/UINT64_FORMAT (and UINT64_HEX_FORMAT for hex). The sole consumer, UndoRecPtrFormat in pbu_undolog.h, should use UINT64_HEX_FORMAT-style formatting against a uint64 argument rather than relying on this shim plus explicit (unsigned long long) casts at every call site.
📄 src/include/access/perbackend/pbu_undolog.h (L190-L191)
The multi-statement macro wrapper has a trailing ; after while (false), which defeats the swallow-the-semicolon idiom: every call site writes UndoRecPtrAssignRelFileNode(...); yielding a stray empty statement (;;). Drop the trailing semicolon so the macro behaves like a function-call statement.
📄 src/include/access/perbackend/pbu_undolog.h (L3-L4)
File-identity header is stale copy-paste from upstream: the module comment says undolog.h and the IDENTIFICATION path says src/include/access/undolog.h, but the real file is src/include/access/perbackend/pbu_undolog.h. Fix the header block to match the actual filename/path.
📄 src/include/access/perbackend/pbu_undoaction_xlog.h (L3-L4)
Stale copy-paste header identity: the comment names the file undoaction_xlog.h and the IDENTIFICATION path is src/include/access/undoaction_xlog.h, but the real file is src/include/access/perbackend/pbu_undoaction_xlog.h. Update the header block to match.
📄 src/include/access/perbackend/pbu_undodiscard.h (L3-L4)
Stale/incorrect header identity: the comment names this file undoinsert.h and the IDENTIFICATION path is src/include/access/undodiscard.h, but the real file is src/include/access/perbackend/pbu_undodiscard.h. Fix both the filename in the comment and the path.
📄 src/include/access/perbackend/pbu_undodiscard.h (L23-L23)
The UndoLogNumber parameter is unnamed in this prototype. PostgreSQL style names parameters in extern declarations for readability; name it (e.g. logno) to match the convention used by the other declarations in this header.
📄 src/include/access/perbackend/pbu_undoinsert.h (L3-L4)
Stale header identity: the comment names the file undoinsert.h and the IDENTIFICATION path is src/include/access/undoinsert.h, but the real file is src/include/access/perbackend/pbu_undoinsert.h. Update to match.
📄 src/include/access/perbackend/pbu_undolog_xlog.h (L3-L4)
Stale header identity: the comment names the file undolog_xlog.h and the IDENTIFICATION path is src/include/access/undolog_xlog.h, but the real file is src/include/access/perbackend/pbu_undolog_xlog.h. Update to match.
📄 src/include/access/perbackend/pbu_undorecord.h (L3-L4)
Stale header identity: the comment names the file undorecord.h and the IDENTIFICATION path is src/include/access/undorecord.h, but the real file is src/include/access/perbackend/pbu_undorecord.h. Update to match.
📄 src/include/access/perbackend/pbu_undorequest.h (L33-L34)
undo_rollback_overflow_size is a GUC (registered in guc_parameters.dat with variable => 'undo_rollback_overflow_size'). The GUC machinery writes it from a different translation unit, so this exported global must be declared PGDLLIMPORT or it will fail to link on Windows/MSVC. Every other exported int GUC in the tree (e.g. wal_segment_size, autovacuum_naptime, and sibling recno_* GUCs in recno.h) uses PGDLLIMPORT.
In addition, this exact declaration already exists in src/include/access/undolog.h:148 (also missing PGDLLIMPORT). The duplicate extern in two headers is redundant and should be consolidated into a single declaration.
💡 Suggested change
Before:
/* GUC: rollback-request size threshold (defined in pbu_undorequest.c) */
+extern int undo_rollback_overflow_size;
After:
/* GUC: rollback-request size threshold (defined in pbu_undorequest.c) */
extern PGDLLIMPORT int undo_rollback_overflow_size;
📄 src/include/access/perbackend/pbu_undorequest.h (L30-L31)
latestRecentGlobalXmin is defined in pbu_undorequest.c:122 but never read or written anywhere in the tree (grep finds only the definition and this declaration). This is dead code; drop both the extern and its backing variable, or wire up the intended logic. (If it is later used cross-module, it will also need PGDLLIMPORT.)
📄 src/include/access/perbackend/pbu_undorequest.h (L3-L8)
Header banner is wrong: the filename reads undorequest.h (should be pbu_undorequest.h), the path line reads src/include/access/undorequest.h (actual path is src/include/access/perbackend/pbu_undorequest.h), and the copyright is the stale 1996-2019. Fix the identity block to match the actual file and current year; a file-name mismatch in the banner misleads readers.
📄 src/include/access/perbackend/pbu_undorequest.h (L114-L121)
SetUndoRequestInfoFromRHEntry has two problems versus ResetUndoRequestInfo:
- It omits
request_size, so afterUndoGetWork(which callsResetUndoRequestInfothen this macro),request_sizestays 0. A large request re-inserted viaInsertRequestIntoUndoQueues-> size queue (pbu_undorequest.c:466 usesurinfo->request_size) loses its size ordering. If 0 is intentional, say so; the asymmetry is a footgun. - The macro dereferences
urinfo/rhwithout parenthesizing the parameters (urinfo->full_xid = rh->full_xid), unlikeResetUndoRequestInfowhich uses(urinfo)->.... Callers currently pass a plain pointer so it works, but a caller passing an expression (e.g.&array[i]) would hit operator-precedence surprises. Parenthesize the parameters for safety and consistency.
💡 Suggested change
Before:
+#define SetUndoRequestInfoFromRHEntry(urinfo, rh, cur_queue) \
+( \
+ urinfo->full_xid = rh->full_xid, \
+ urinfo->start_urec_ptr = rh->start_urec_ptr, \
+ urinfo->end_urec_ptr = rh->end_urec_ptr, \
+ urinfo->dbid = rh->dbid, \
+ urinfo->undo_worker_queue = cur_queue \
+)
After:
#define SetUndoRequestInfoFromRHEntry(urinfo, rh, cur_queue) \
( \
(urinfo)->full_xid = (rh)->full_xid, \
(urinfo)->start_urec_ptr = (rh)->start_urec_ptr, \
(urinfo)->end_urec_ptr = (rh)->end_urec_ptr, \
(urinfo)->dbid = (rh)->dbid, \
(urinfo)->undo_worker_queue = (cur_queue) \
)
📄 src/include/access/perbackend/pbu_undorequest.h (L28-L28)
Unparenthesized negative sentinel. -1 can bite when substituted into a larger expression. Parenthesize per the ALL_CAPS-macro convention: #define InvalidUndoWorkerQueue (-1). (It is assigned to the enum field undo_worker_queue at pbu_undoworker.c:435; the enum must therefore be a signed type, which it is on the compilers the tree targets, but the parenthesization is still the correct hygiene.)
💡 Suggested change
Before:
+#define InvalidUndoWorkerQueue -1
After:
#define InvalidUndoWorkerQueue (-1)
📄 src/include/access/perbackend/pbu_undoworker.h (L16-L18)
UndoWorkerDelay is declared extern here but has no definition anywhere in the tree and no users (grep finds only this line). It is also not registered as a GUC despite the "GUC options" comment; the actual sleep-time GUC is undo_worker_naptime. Remove this dead declaration (and its misleading comment). If it is meant to be exported later it will also need PGDLLIMPORT.
📄 src/include/access/perbackend/pbu_undoworker.h (L3-L9)
Header banner is wrong: filename reads undoworker.h (should be pbu_undoworker.h), path line reads src/include/access/undoworker.h (actual path is src/include/access/perbackend/pbu_undoworker.h), and the copyright is the stale 1996-2019. Correct the identity block to match the actual file and current year.
📄 src/include/access/undodefs.h (L19-L29)
These typedefs (UndoRecPtr, UndoRecordSize, UndoLogNumber) duplicate identical typedefs already present in access/perbackend/pbu_undolog.h (lines 30, 33, 114 there). The two header families now coexist in the same translation unit: flux_pbu.c and recno_pbu.c include both access/perbackend/pbu_undolog.h and (via recno_pbu.h/flux_pbu.h/undormgr.h) access/undodefs.h, so each gets typedef uint64 UndoRecPtr; twice in one TU. Redefining a typedef is only permitted since C11; under strict C99 and MSVC this is a constraint violation that will fail the portability gate. Make pbu_undolog.h include undodefs.h for these shared types instead of re-declaring them, so there is a single canonical definition.
confidence: high
📄 src/include/access/undo_bufmgr.h (L154-L158)
UndoRecPtrGetBlockNum and UndoRecPtrGetPageOffset are also defined in access/perbackend/pbu_undolog.h (lines 155-160) with DIFFERENT semantics: there they take a full UndoRecPtr and internally apply UndoRecPtrGetOffset(), whereas here they take an already-extracted raw byte offset. Likewise UndoLogForkNum is defined in both headers. If a TU ever includes both headers it hits a preprocessor redefinition (the macro bodies differ textually, so a warning/error fires); worse, if the error is suppressed, the two forms silently compute different results depending on whether the argument is a pointer or an offset. This is a footgun given confirmed finding #1 already notes this whole buffer API has no callers. Rename these to avoid colliding with the pre-existing pbu_undolog.h macros, or drop the API entirely.
confidence: high
📄 src/include/access/undo_xlog.h (L237-L241)
xl_undo_chain_state is dead: it is referenced nowhere in the tree outside this header (and typedefs.list). The 2PC undo chain is actually persisted via last_batch_lsn chaining, not this struct. The three callbacks declared just below it -- undo_twophase_recover, undo_twophase_postcommit, undo_twophase_postabort -- are likewise never implemented and never called (twophase_rmgr.c does not reference them). This is unimplemented 2PC-via-undo scaffolding; remove the struct and the three unimplemented declarations (YAGNI), or wire them up.
confidence: high
📄 src/include/access/undo_xlog.h (L249-L254)
These three two-phase-commit callbacks are declared but have no definition and no caller anywhere in the tree (twophase_rmgr.c does not reference them). Dead declarations for an unimplemented path; drop them until the functionality actually exists.
confidence: high
📄 src/include/access/perbackend/pbu_undoinsert.h (L62-L64)
This header is not self-contained: RelFileNode is not a real master type -- it only resolves because pbu_compat.h #define-aliases it to RelFileLocator. But this header does not include pbu_compat.h, so it compiles only if the including TU happens to pull in pbu_compat.h first. Any new consumer that includes pbu_undoinsert.h without pbu_compat.h ahead of it will fail to compile with an obscure "unknown type RelFileNode" error. Either declare this prototype with the real master type RelFileLocator (preferred), or make the header include its dependency explicitly. The same hidden dependency applies to the other declarations here that rely on pbu_compat.h-only names.
📄 src/include/access/perbackend/pbu_undolog.h (L335-L335)
This prototype is missing the extern keyword that every other function declaration in this header uses. While extern is implicit for function declarations, PostgreSQL style (and consistency with the surrounding declarations) requires it here.
💡 Suggested change
Before:
void UndoLogNewSegment(UndoLogNumber logno, Oid tablespace, int segno);
After:
extern void UndoLogNewSegment(UndoLogNumber logno, Oid tablespace, int segno);
📄 src/include/access/slog.h (L238-L239)
Missing PGDLLIMPORT on an exported global variable. PostgreSQL convention (and the Windows/MSVC hard portability gate) requires every backend global declared in a header to be marked PGDLLIMPORT so cross-module/extension references resolve on Windows. This header is the public, explicitly "engine-agnostic" sLog API included by consumer AMs (e.g. src/backend/access/flux/*.c). All comparable GUC externs in the tree (see access/xlog.h, miscadmin.h) carry PGDLLIMPORT. The same applies to slog_num_partitions below.
💡 Suggested change
Before:
/* GUC: maximum DSA size (in MB) */
extern int slog_dsa_max_size_mb;
After:
/* GUC: maximum DSA size (in MB) */
extern PGDLLIMPORT int slog_dsa_max_size_mb;
📄 src/include/access/slog.h (L399-L400)
Missing PGDLLIMPORT. Same issue as slog_dsa_max_size_mb: this is a backend global exposed through a public API header and must be marked PGDLLIMPORT for Windows/MSVC cross-module linkage.
💡 Suggested change
Before:
/* GUC: number of sLog flat hash partitions (0 = auto based on CPU count) */
extern int slog_num_partitions;
After:
/* GUC: number of sLog flat hash partitions (0 = auto based on CPU count) */
extern PGDLLIMPORT int slog_num_partitions;
📄 src/include/access/slog.h (L64-L64)
These constants drive an unconditionally-allocated shared-memory footprint that is enormous. SLogFlatBucket embeds SLogTupleEntry which embeds ops[SLOG_MAX_TUPLE_OPS] (128 * ~40B ~= 5KB per bucket). With SLogTupleNumEntries = MaxBackends * SLOG_TUPLE_PER_BACKEND_SLOTS (1024), clamped to SLOG_TUPLE_MAX_ENTRIES (4,194,304), the flat-hash capacity (SLogFlatHashCapacity divides by 0.7 and rounds to a power of 2) reaches ~8.4M buckets * ~5KB ~= 43 GB worst case. Even at a modest MaxBackends ~= 110 the reservation is ~676 MB. slog_tuple.c allocates this unconditionally at startup (SLogTupleShmemInit comment: "Runs unconditionally ... whether or not an AM has opted in"), so a cluster whose AMs never use the tuple sLog still pays this. This directly contradicts the header's claim that an idle flat hash "costs only its shared-memory footprint" -- that footprint is the problem. Either gate allocation on an AM having opted in, or sharply reduce the per-TID cap / max entry count / per-backend slots. The 4,194,304 max entry clamp is the single largest multiplier and should be revisited.
📄 src/include/access/slog_internal.h (L60-L61)
Missing PGDLLIMPORT. Although slog_internal.h is currently included only by the two in-tree sLog .c files, SLogState is read by the inline function SLogFlatHashPartitionIndex() in slog_flathash.h (via SLogNumPartitions) and the subsystem is documented as engine-agnostic for any AM/extension. If any out-of-tree consumer ever includes these internal headers, the unqualified extern will fail to link on Windows/MSVC. Mark it PGDLLIMPORT to follow the tree-wide convention for exported globals.
💡 Suggested change
Before:
/* The single shared-state instance (defined in slog.c). */
extern SLogSharedState *SLogState;
After:
/* The single shared-state instance (defined in slog.c). */
extern PGDLLIMPORT SLogSharedState *SLogState;
📄 src/include/access/slog_flathash.h (L237-L237)
Missing PGDLLIMPORT, and this one is reachable cross-module. SLogNumPartitions is read inside the inline function SLogFlatHashPartitionIndex() defined in this header; any translation unit that includes slog_flathash.h and calls that inline references the extern int directly. For a subsystem documented as usable by any in-place-MVCC AM/extension, a reference to this unqualified extern from another module/DLL will fail to link on Windows/MSVC. Mark it PGDLLIMPORT.
💡 Suggested change
Before:
extern int SLogNumPartitions;
After:
extern PGDLLIMPORT int SLogNumPartitions;
📄 src/include/access/slog_flathash.h (L148-L156)
Stale naming/abstraction. The comments and identifiers throughout this header refer to a "Seqlock" (SLogSeqWriteBegin/End, SLOG_SEQ_READ_BEGIN/END, the seqvar/slog_seq_ parameter), but the header itself states the seqlock scheme was replaced by a plain LWLock and these helpers are now no-ops kept "only so existing writer call sites need not change." Carrying a dead seqlock vocabulary and an unused seqvar parameter is misleading scaffolding (per PostgreSQL review discipline: comments must describe what the code does now, and no-op shims kept for churn-avoidance are flagged). Consider removing the no-op write helpers and renaming the read macros to reflect that they are LW_SHARED critical sections, so the code reads as if always written this way.
📄 src/include/access/slog_flathash.h (L188-L191)
Footgun macro: SLOG_SEQ_READ_BEGIN acquires writer_lock LW_SHARED and the matching END releases it, with the body supplied by the caller. As the comment admits, any return/goto/elog/ereport or a CHECK_FOR_INTERRUPTS longjmp inside the block leaks the LWLock. The in-tree call sites in slog_tuple.c are disciplined (locals-only, side effects after END), but this is an easy-to-misuse API with no compiler enforcement. Prefer an explicit LWLockAcquire/LWLockRelease pair at each call site (clearer and lets the error-cleanup machinery see the lock), or at minimum document the hazard at every call site rather than relying on a convention.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L54-L60)
These frontend struct copies do not match the authoritative WAL record layout in flux_xlog.h, so pg_waldump misparses FLUX records.
- xl_flux_insert is actually
{offnum(u16), flags(u16), tuple_len(u32), commit_ts(u64)}(16 bytes). The copy here declares{offnum, flags, commit_ts(u64), xact_ts(u64)}(24 bytes). There is noxact_tsfield in the real record andtuple_lenis dropped. Because tuple bytes follow the header,datalen >= sizeof(..._fe)passes andxact_tsis read from tuple payload -- garbage output. - xl_flux_delete has the same spurious
xact_ts/missingtuple_lenproblem. - xl_flux_update is
{offnum, flags, old_commit_ts, new_commit_ts, old_tuple_len(u16), new_tuple_len(u16), dst_block_id(u8), pad[3]}; the copy ends with a bogusxact_ts(u64)read over those trailing fields.
The FRONTEND block already present in flux_xlog.h (guarded by #ifdef FRONTEND) defines matching frontend structs -- include that header and delete these local copies instead of maintaining a hand-synced duplicate that has already drifted.
📄 src/backend/access/rmgrdesc/undodesc.c (L76-L84)
The operation_type labels are wrong for the records that are actually emitted. XLOG_UNDO_APPLY_RECORD is produced by flux_undo.c/recno_undo.c with xlrec.operation_type = info, where info is a FLUX_UNDO_/RECNO_UNDO_ subtype: UPDATE=0x0002 and DELETE=0x0003 (see flux_undo.h / recno_undo.h). This mapping has them swapped (0x0002 -> DELETE, 0x0003 -> UPDATE), and 0x0004 is UPDATE_DELTA for FLUX, not PRUNE. pg_waldump will mislabel every undo-apply CLR. Reference the shared named constants instead of hardcoded hex, and ensure the labels match the subtype definitions.
📄 src/backend/access/rmgrdesc/atmdesc.c (L38-L46)
atm_desc has no default branch, so an unrecognized info byte leaves buf empty and pg_waldump prints a blank description. Sibling desc files added in the same change (fileopsdesc.c, fluxdesc.c, recnodesc.c) emit an "unknown"/"UNKNOWN" string. Add a default case for consistency and diagnosability.
📄 src/backend/access/rmgrdesc/pbu_undoactiondesc.c (L32-L41)
undoaction_desc has no default branch; an unexpected info byte produces an empty description. Match the behavior of the other desc files in this change by emitting an "unknown" fallback.
📄 src/backend/access/transam/rmgr.c (L8-L9)
Include ordering churns the file. #include "storage/fileops.h" is wedged directly after postgres.h and ahead of the long alphabetically-sorted access/* block, while the other new headers (access/undo_xlog.h, access/atm.h, pbu_* headers) are appended out of order after utils/relmapper.h. Fold these into the existing sorted IWYU-keep group so the diff is minimal and the include list stays ordered, per project convention.
📄 src/bin/pg_waldump/t/001_basic.pl (L100-L106)
This test unconditionally expects FLUX, FileOps, and Recno in the --rmgr=list output, but those three entries in rmgrlist.h are compiled only under #ifdef USE_FLUX, #ifdef USE_FILEOPS, and #ifdef USE_RECNO. If any of those macros is unset at build time the corresponding rmgr disappears from the backend table and this test fails. It happens to pass today only because the build system hard-codes all three macros on; the test therefore encodes an assumption that contradicts the conditional-compilation in rmgrlist.h. Either the guards should be removed from rmgrlist.h (so the list is genuinely unconditional) or this expected output must be made conditional to match. See the related finding on rmgrlist.h.
📄 src/include/access/rmgrlist.h (L53-L62)
WAL-format footgun. RmgrId values are assigned by position in this list and are stored in WAL (see the header comment at the top: "order of entries defines the numerical values of each rmgr's ID"). These #ifdef USE_FLUX/USE_FILEOPS/USE_RECNO guards sit in the middle of the list, so disabling any one of them shifts the numeric IDs of every entry below it (RM_UNDOLOG_ID, RM_UNDOACTION_ID). A build with, say, USE_RECNO unset would produce WAL whose rmgr IDs are incompatible with the standard build, breaking crash/replica recovery and pg_upgrade between differently-configured binaries.
The build system (src/Makefile.global.in, meson.build) already forces all three macros on precisely "so [the] WAL rmgr ID ... is stable across every translation unit". That makes the guards simultaneously (a) dead #ifdef-fencing of a feature that is always compiled, and (b) a latent corruption hazard the moment someone flips a macro. Either remove the guards entirely (if always-on is the real contract), or, if these AMs are meant to be truly optional, move the conditional PG_RMGR entries to the end of the list after RM_UNDOACTION_ID so earlier IDs never shift.
📄 src/bin/pg_waldump/rmgrdesc.c (L42-L46)
The externs for recno_desc/recno_identify are guarded by #ifdef USE_RECNO, but RmgrDescTable is built from rmgrlist.h which references recno_desc/recno_identify under the same guard, while undolog_/undoaction_ and the flux/fileops/atm/undo desc routines are declared unconditionally (via headers). This is inconsistent: the desc-table population and these forward declarations should be gated by the same conditions. If USE_RECNO were unset, rmgrlist.h would not emit the RM_RECNO_ID row, so the guarded extern is consistent only by coincidence of the build always defining USE_RECNO. Mirror the guard policy used in rmgrlist.h (and ideally obtain these prototypes from their proper headers, e.g. access/recno_xlog.h and the pbu *_xlog.h headers, rather than hand-rolled externs here) so the two stay in lockstep.
📄 src/bin/pg_waldump/rmgrdesc.c (L20-L22)
The new #include lines break the alphabetical ordering of this include block (flux_xlog.h/fileops_xlog.h inserted between multixact.h and nbtxlog.h; atm_xlog.h/undo_xlog.h between spgxlog.h and xact.h). pgsql convention keeps these sorted. Reorder so the block stays alphabetical (e.g. atm_xlog.h, fileops_xlog.h, flux_xlog.h grouped in order and undo_xlog.h after spgxlog.h/before xact.h as appropriate).
📄 src/bin/pg_waldump/rmgrdesc.c (L47-L48)
#include "storage/standbydefs.h" and #include "utils/relmapper.h" now sit after the block of extern declarations added above, so includes are interleaved with code. Move all #include directives above the extern declarations to keep the file's include section contiguous and conventional.
📄 src/bin/pg_waldump/rmgrdesc.c (L37-L41)
These four prototypes duplicate declarations that already exist in headers: undolog_desc/undolog_identify in access/perbackend/pbu_undolog_xlog.h and undoaction_desc/undoaction_identify in access/perbackend/pbu_undoaction_xlog.h. Hand-maintained duplicate externs can silently drift from the real signatures (the compiler won't catch a mismatch across TUs), and it is inconsistent with how flux/fileops/atm/undo are wired here (they get their prototypes by including the corresponding *_xlog.h). Prefer including the two headers in the include block above instead of re-declaring. Confidence: high.
📄 src/bin/pg_waldump/rmgrdesc.c (L42-L46)
recno_desc/recno_identify are already declared in access/recno_xlog.h (which src/backend/access/recno/recno_handler.c et al. include), so this local re-declaration duplicates the authoritative prototype and the comment misdirects the reader to access/rmgrdesc/recnodesc.c (the definition site, not the declaration). Include access/recno_xlog.h under the same #ifdef USE_RECNO guard instead of redeclaring. Confidence: high.
📄 src/include/access/slog_flathash.h (L36-L39)
Dead includes. Neither port/atomics.h (no pg_atomic_* type/function is referenced) nor storage/s_lock.h (no slock_t/spinlock is referenced) is used in this header. Both are leftovers from the removed seqlock reader/writer scheme that the file header itself documents as replaced by a plain LWLock. Per the minimalism discipline, drop the unused includes; this header only needs access/slog.h and storage/lwlock.h.
💡 Suggested change
Before:
#include "access/slog.h"
#include "port/atomics.h"
#include "storage/lwlock.h"
#include "storage/s_lock.h"
After:
#include "access/slog.h"
#include "storage/lwlock.h"
📄 src/include/access/slog_flathash.h (L208-L208)
Dead parameter. The max_backends argument on both SLogFlatHashShmemSize() and SLogFlatHashPartitionedShmemSize() is unused in the implementation (slog_flathash.c does (void) max_backends; /* retained for call-site compatibility */). These are brand-new functions in a brand-new header with only in-tree callers, so there is no external ABI/call-site compatibility to preserve. Drop the parameter from the declarations, definitions, and the two call sites in slog_tuple.c. (YAGNI.)
💡 Suggested change
Before:
extern Size SLogFlatHashShmemSize(int capacity, int max_backends);
After:
extern Size SLogFlatHashShmemSize(int capacity);
📄 src/include/access/slog_flathash.h (L226-L226)
Type-unsafe opaque interface. SLogFlatHashApply(void *data, const void *operation, Size op_size) always receives a SLogFlatHash * and a SLogFlatOp *, and op_size is used only for Assert(op_size == sizeof(SLogFlatOp)). The void * + size shape is a footgun left over from the old double-buffered/copied-hash design; it defeats compile-time type checking at every call site. Prefer concrete types: extern void SLogFlatHashApply(SLogFlatHash *ht, const SLogFlatOp *op); and drop op_size.
💡 Suggested change
Before:
extern void SLogFlatHashApply(void *data, const void *operation, Size op_size);
After:
extern void SLogFlatHashApply(SLogFlatHash *ht, const SLogFlatOp *op);
📄 src/include/access/slog_flathash.h (L223-L224)
Stale comment. This references SLogSeqWriteBegin/End, which the header itself documents as no-op leftovers of the removed seqlock scheme; writers now simply hold writer_lock LW_EXCLUSIVE. Update to describe current behavior to avoid misleading future callers (tied to the broader stale-seqlock naming cleanup).
💡 Suggested change
Before:
* Apply one SLogFlatOp to the (single) flat-hash copy. Called by writers
* holding the partition writer_lock, between SLogSeqWriteBegin/End.
After:
* Apply one SLogFlatOp to the (single) flat-hash copy. Called by writers
* while holding the partition writer_lock LW_EXCLUSIVE.
📄 src/backend/access/rmgrdesc/Makefile (L17-L20)
The rmgrdesc OBJS list is kept alphabetically sorted (brindesc, clogdesc, committsdesc, dbasedesc, genericdesc, ...). The new entries break that order: recnodesc.o is placed before genericdesc.o (badly out of place), and pbu_undoactiondesc.o/pbu_undologdesc.o/undodesc.o are wedged between tblspcdesc.o and xactdesc.o. Insert each entry at its correct alphabetical position (and mirror the same ordering in meson.build) to keep the list maintainable and consistent with the rest of the tree.
📄 src/backend/access/rmgrdesc/undodesc.c (L137-L139)
undo_desc has no default branch. For an unrecognized info byte the switch falls through and buf is left empty, so pg_waldump prints a blank description. The sibling desc files added in this change (fileopsdesc.c, fluxdesc.c, recnodesc.c) emit an "unknown"/"UNKNOWN" fallback. Add a default case here for consistency and diagnosability.
📄 src/backend/access/rmgrdesc/nbtdesc.c (L139-L141)
The description omits the deleted field of xl_btree_delete_mark. That flag changes record semantics (it indicates the full-page fallback where the item is deleted and the new tuple image is absent), so including it makes pg_waldump output materially more useful. Consider also printing deleted.
💡 Suggested change
Before:
appendStringInfo(buf, "offnum: %u, setmark: %c",
xlrec->offnum,
xlrec->setmark ? 'T' : 'F');
After:
appendStringInfo(buf, "offnum: %u, setmark: %c, deleted: %c",
xlrec->offnum,
xlrec->setmark ? 'T' : 'F',
xlrec->deleted ? 'T' : 'F');
📄 src/include/access/fileops_xlog.h (L21-L33)
These 13 XLOG_FILEOPS_* opcodes are defined here and identically in src/include/storage/fileops.h (lines 41-53). fileops.c includes both headers (lines 76 and 90), so the macros are redefined in the same translation unit. The redefinitions are token-identical, so this is technically benign under C99 (no warning), but it is a DRY violation and a WAL-format footgun: two independent sources of truth for on-disk record type codes that must stay byte-for-byte identical forever. If one copy is edited and the other is not, redo silently mismatches the writer. Make this frontend-safe header the single source of truth and have storage/fileops.h #include "access/fileops_xlog.h" instead of re-declaring the opcodes (mirroring how other subsystems separate the frontend-safe xlog header from the backend header).
📄 src/include/access/atm_xlog.h (L27-L33)
Field ordering puts TransactionId xid (4 bytes) before XLogRecPtr last_batch_lsn (8-byte aligned), so the compiler inserts 4 bytes of padding at offset 4. SizeOfXlAtmAbort resolves to the full 24-byte struct size, and ATMAddAborted() registers the whole struct (XLogRegisterData((char *) &xlrec, SizeOfXlAtmAbort)) without zeroing it, so those uninitialized padding bytes are written into the WAL stream. This is non-deterministic WAL content (an info leak and a nuisance for byte-level WAL comparisons / standby divergence testing). Round-trip correctness is unaffected because the reader casts back to the same struct, but it is cleaner to reorder fields largest-first (lsn, then xid/dboid/reloid) so there is no interior padding, or memset the record before populating it.
📄 src/include/access/atm_xlog.h (L32-L32)
The reloid field is documented as always InvalidOid and "kept for struct layout" -- i.e. a dead field carried only to preserve on-disk size. The producer always sets it to InvalidOid and ATMAddAborted() passes InvalidOid downstream. This is speculative scaffolding (YAGNI): a brand-new WAL record format has no legacy layout to preserve, so there is no reason to ship a permanently-unused field in the record. Drop reloid and define SizeOfXlAtmAbort from the remaining fields, unless a concrete near-term use exists (in which case the comment should state it rather than "kept for struct layout").
| flux_pbu.o \ | ||
| flux_pvs.o | ||
|
|
||
| include $(top_srcdir)/src/backend/common.mk No newline at end of file |
There was a problem hiding this comment.
Missing trailing newline at end of file (\ No newline at end of file in the diff). This fails git diff --check and deviates from the convention in sibling makefiles (e.g. src/backend/access/recno/Makefile ends with a newline). Add a final newline. (high confidence)
| include $(top_srcdir)/src/backend/common.mk | |
| include $(top_srcdir)/src/backend/common.mk |
| @@ -1,4 +1,4 @@ | |||
| #!/usr/bin/perl | |||
| #!/usr/bin/env perl | |||
There was a problem hiding this comment.
Unrelated change to a committer-maintained tool. Changing the shebang from #!/usr/bin/perl to #!/usr/bin/env perl is churn outside the stated purpose of the access-method/undo work, and it is inconsistent with the rest of the tree: every other Perl script under src/tools, src/backend, src/common, contrib, etc. still uses #!/usr/bin/perl (or #! /usr/bin/perl). /usr/bin/env perl picks up whatever perl is first on PATH (e.g. the Nix dev shell's Perl), which can differ from the documented invocation and potentially alter formatting results. Revert this hunk.
| #!/usr/bin/env perl | |
| #!/usr/bin/perl |
| break src/backend/access/transam/xact.c:447 # IsAbortedTransactionBlockState() | ||
| break src/backend/access/transam/xact.c:1183 # IsInParallelMode() | ||
| break src/backend/access/transam/xact.c:2284 # StartTransaction() | ||
| break src/backend/access/transam/xact.c:2593 # CommitTransaction() |
There was a problem hiding this comment.
This .gdbinit is personal/environment-specific developer tooling committed at the repository root. It is auto-generated against a single snapshot and will never be part of a pgsql-hackers patch. More concretely, the undo-bp-embedded block hardcodes absolute source line numbers that drift the instant any of these heavily-modified files change, silently setting breakpoints on the wrong lines. For example this entry claims line 2593 of xact.c is in CommitTransaction(), but after the accompanying changes line 2593 is the comment /* Clean up transaction undo state ... */ and the actual call (AtCommit_XactUndo) is on 2594 -- the references are already stale. This file should live outside the tree or in a local-ignore list rather than be committed.
| - -g | ||
| - -std=c11 | ||
| - -I. | ||
| - -I../../../../src/include |
There was a problem hiding this comment.
Personal/environment-specific developer scaffolding committed into the repository root. .clangd, .envrc, flake.nix, shell.nix, pg-aliases.sh, and .gdbinit are not part of a pgsql-hackers patch; they bundle unrelated concerns and reference external personal inputs (e.g. .envrc's source_up, use project_steering, use project_mcp directives tie to a private direnv setup). These will never be accepted upstream and should live outside the tree (or in a local-ignore list). Note .clangd also hardcodes -I../../../../src/include, which is relative to a specific nested build layout and not generally valid.
| ulimit -c unlimited | ||
|
|
||
| # Local git excludes | ||
| git config core.excludesFile .local-gitignore 2>/dev/null || true |
There was a problem hiding this comment.
shellHook sets git config core.excludesFile .local-gitignore, but no .local-gitignore is provided anywhere in the change set (confirmed by search: the only reference is here). Pointing core.excludesFile at a missing file silently overrides the user's normal global excludes with nothing, so the entry falls back to default behavior and the dev-only files (.clangd, flake.nix, build/, etc.) that this hook presumably intends to ignore are not actually excluded. Either commit the .local-gitignore it expects or drop this line.
| reports TU_None so the executor does no index work. The delete-marked | ||
| entry is kept, not removed, so a snapshot that predates the key change still | ||
| reaches the row under k_old; a scan that arrives through a delete-marked | ||
| entry sets xs_recheck so the executor re-evaluates the qual. FLUX's own |
There was a problem hiding this comment.
This mischaracterizes FLUX's actual scan behavior. For a FLUX table, the per-TID heap-fetch loop lives in flux_index_getnext_slot (flux_handler.c), which calls flux_index_entry_key_mismatch and skips a delete-marked/stale entry via continue -- it does NOT return the tuple with xs_recheck set for the executor to re-evaluate the qual. xs_recheck is grep-absent from the entire flux/ tree; it is only set at the nbtree level (_bt_returnitem), but FLUX overrides the whole index-scan heap-fetch loop (per ddce1da) and does its own recheck-and-skip. The sentence describes heap/nbtree executor-level qual recheck, not FLUX's design, and will mislead a reader cross-referencing flux_index_getnext_slot. Reword to state that the FLUX scan loop rechecks the entry key against the visible version and skips on mismatch (Phase 8c invariant I2), matching the function and flux_index_scan_begin comments.
| entry sets xs_recheck so the executor re-evaluates the qual. FLUX's own | |
| entry is rechecked by FLUX's own scan loop (flux_index_getnext_slot), which | |
| compares the entry key against the visible version and skips the entry on | |
| mismatch (Phase 8c invariant I2). FLUX's own |
| bool | ||
| FluxDirtyMapCheck(Oid relid, BlockNumber blkno) | ||
| { |
There was a problem hiding this comment.
Critical correctness gap: this map lives only in shared memory and is reset to all-empty on every postmaster start (FluxDirtyMapShmemInit_cb). There is no redo-path reconstruction -- FluxDirtyMapMark is never called from any WAL replay/recovery code (only from the live INSERT/UPDATE/DELETE/lock paths in flux_handler.c and flux_operations.c).
But FluxDirtyMapCheck gates before-image reconstruction on the read path (flux_handler.c:2232 and :2423): a false result causes the scanner to serve the on-page value and SKIP the sLog/UNDO before-image probe. After a crash+recovery (or any restart), the map is empty, so Check returns false ("provably clean") for pages that still carry retained before-images that an old snapshot needs. That is precisely the wrongly-cleared-bit false negative the header's own correctness invariant warns leads to WRONG RESULTS.
The grow-only / mark-before-unlock argument only holds within a single postmaster lifetime; it does not survive a restart. Either the map must be reconstructed during recovery/startup (e.g. replay FluxDirtyMapMark in the FLUX rmgr redo path, or rebuild it from the sLog's retained-entry set before accepting queries), or Check must not be relied upon as a gate when it may be stale across restarts.
| void | ||
| FluxDirtyMapShmemInit(void) | ||
| { | ||
| /* Initialization is handled by FluxDirtyMapShmemCallbacks */ | ||
| } |
There was a problem hiding this comment.
FluxDirtyMapShmemInit is dead code: it is a no-op whose body only initializes via the callback, and it has no callers anywhere in the tree (initialization is driven by the PG_SHMEM_SUBSYSTEM(FluxDirtyMapShmemCallbacks) registration in subsystemlist.h, which invokes FluxDirtyMapShmemInit_cb). The empty function plus its two redundant extern declarations (flux_dirtymap.h and flux.h) are speculative scaffolding; remove them.
| nearlyEmptyFreeSpace = FLUX_MAX_TUPLE_SIZE - | ||
| (FLUX_MAX_TUPLE_SIZE / BLCKSZ + 32 * sizeof(ItemIdData)); |
There was a problem hiding this comment.
The port of heap's nearlyEmptyFreeSpace slack term is broken. Heap (hio.c) computes MaxHeapTupleSize - (MaxHeapTuplesPerPage / 8 * sizeof(ItemIdData)), where the subtracted term is a count of line pointers (MaxHeapTuplesPerPage/8) times the line-pointer size. The FLUX version computes FLUX_MAX_TUPLE_SIZE / BLCKSZ, which is dimensionless and evaluates to 0 (FLUX_MAX_TUPLE_SIZE < BLCKSZ), plus a magic 32 * sizeof(ItemIdData). So the slack is just a hardcoded 128 bytes with no relation to FLUX's page geometry, and the comment's claim of "heap's reasoning, in FLUX's geometry" is false. FLUX already defines MaxFluxItemsPerPage for exactly this purpose; use it to mirror heap faithfully.
| nearlyEmptyFreeSpace = FLUX_MAX_TUPLE_SIZE - | |
| (FLUX_MAX_TUPLE_SIZE / BLCKSZ + 32 * sizeof(ItemIdData)); | |
| nearlyEmptyFreeSpace = FLUX_MAX_TUPLE_SIZE - | |
| (MaxFluxItemsPerPage / 8 * sizeof(ItemIdData)); |
| START_CRIT_SECTION(); | ||
|
|
||
| MarkBufferDirty(buffer); | ||
|
|
||
| /* Log page initialization */ | ||
| if (RelationNeedsWAL(rel)) | ||
| { | ||
| uint64 init_commit_ts = FluxGetCommitTimestamp(); |
There was a problem hiding this comment.
FluxGetCommitTimestamp() is called inside the critical section (START_CRIT_SECTION() is at the line above). That function can elog(ERROR, "FLUX MVCC not initialized") (flux_mvcc.c), and any ERROR raised inside a critical section is promoted to a PANIC. The rest of the codebase deliberately avoids this: flux_handler.c computes the timestamp before entering the critical section with the comment "Get timestamp BEFORE entering critical section". Hoist this call above START_CRIT_SECTION() to match that convention and keep the critical section ERROR-free.
Squashes the four CI commits this fork carried on top of upstream into
one, and cuts the carry from 3776 lines to ~1000.
Removed:
- .github/{README,QUICKSTART}.md and .github/docs/* (2273 lines). Beyond
being bulk we rebase hourly, they had drifted into being wrong: they
described a daily (not hourly) sync, Claude 3.5 Sonnet via the direct
Anthropic API (we run Opus on Bedrock), a static IAM user (we use
OIDC), and a Windows dependency-builder workflow that the same commit
which documented it had already deleted. 16 referenced paths did not
exist. An AI reviewer reading this repo ingests that as truth.
- sync-upstream-manual.yml (252 lines). sync-upstream.yml already has
workflow_dispatch; the sole behavioral difference was a force_push
toggle whose "false" setting cannot work after a rebase anyway.
- .github/.gitignore, which only ignored scripts/ai-review/, a directory
that does not exist here.
Rewritten:
- sync-upstream.yml -> fork-sync-upstream.yml, 260 lines to 47. git
rebase already handles the fast-forward, no-op and diverged cases that
the ahead/behind arithmetic hand-rolled, so all that goes away, as do
the issue open/comment/close machinery (a scheduled-run failure is
already emailed to the one consumer, and the run log carries what the
issue body would) and the "dev setup|dev v[0-9]" commit allowlist,
which matched zero commits on master and had since it was written.
Sync drops to every three hours: hourly meant 24 full-history clones
of an 840MB repo per day, each force-push also re-triggering pg-ci.yml
on master, to collect a handful of upstream commits.
Renamed:
- ocr-review.yml, ocr-model-check.yml -> fork-*.yml. Upstream owns
.github/ too (it has touched pg-ci.yml three times this year), so
fork-owned workflows now live under a name prefix upstream will not
collide with, making the rebase structurally conflict-free and letting
the sync guard match fork-owned paths exactly rather than allowing all
of .github/.
Unchanged: the OCR review workflows' logic and .github/ocr/* (rule.json,
context.md, litellm.yaml, pg-history.py) - the part that carries value.
The OCR review workflows and their config were the last bulk this fork carried on top of upstream: ~970 lines rebased onto postgres/postgres every few hours, editable only by amending a commit that gets force- pushed. They are now reusable (workflow_call) workflows in the sidecar repo gburd/ci-workflows, and master carries two caller stubs instead. Carry after this: 3 files, ~110 lines, one commit. The sidecar is also where the value is: ocr/rule.json and ocr/context.md (the PostgreSQL committer-grade review corpus) become normal reviewable files with normal PRs and history, rather than a blob inside a commit that is rewritten on every sync. Mechanics worth knowing: - The review jobs check ci-workflows out at github.job_workflow_sha, so the rules always come from the same commit as the logic using them, and pinning @v1 (or a sha) in the stub pins both. This replaces the old "git show origin/<default_branch>:.github/ocr/..." dance, which existed only because the config had to be read off a branch other than the PR's. - AWS settings are passed as explicit inputs rather than read from vars inside the reusable workflow. vars does resolve across the boundary, but the contract stays visible at the call site. - A reusable workflow does not change the OIDC subject: it stays repo:gburd/postgres:*, so the ocr-bedrock-ci trust policy is unchanged. - ci-workflows must stay public; callers read it with GITHUB_TOKEN. The sync guard narrows to .github/workflows/fork-* accordingly, and sync-upstream keeps needing SYNC_TOKEN: the stubs are still workflow files, which GITHUB_TOKEN may not push.
Add a cluster-wide UNDO engine whose records live in the common WAL
stream rather than in per-log segment files. A transaction that must be
rolled back has its UNDO applied by walking its UNDO-batch chain backward
from the last batch LSN and restoring before-images.
Core pieces:
* A new "Undo" resource manager (rmgrlist.h) and its WAL records:
XLOG_UNDO_BATCH and friends, described by undodesc.c and applied by
undo_xlog.c / undoapply.c. ApplyUndoChainFromWAL() walks a chain
from a given last-batch LSN.
* An UNDO resource-manager dispatch layer (undormgr.c/.h, driven by the
initially empty access/undormgrlist.h / undormgrs.h) so that later
UNDO-writing consumers register themselves without the UNDO core
needing to know their names at compile time.
* Per-transaction UNDO record assembly and insertion into WAL
(undorecord.c, undoinsert.c, xactundo.c), a dedicated UNDO buffer
manager (undo_bufmgr.c), log/discard bookkeeping (undolog.c), the
cluster-wide UNDO discard worker (undoworker.c), and statistics
(undostats.c).
* Transaction integration: AbortTransaction() and ROLLBACK PREPARED
apply the transaction's cluster-wide UNDO chain synchronously
(inline ApplyUndoChainFromWAL); the end-of-recovery phase applies any
chains deferred during redo. The chain-head LSN is durably carried
across 2PC in the prepared-transaction state file, and WAL holding a
still-needed UNDO chain is protected from recycling
(UndoGetOldestBatchLSN, TwoPhaseGetOldestUndoBatchLSN).
* A tableam.h am_supports_undo flag marking access methods that record
UNDO for their own changes, buffer-manager helpers used by inline
apply (BufferLockReleaseAll and friends), and the UNDO GUCs, wait
events, LWLock tranches, and documentation for the above.
Later patches add UNDO-writing consumers (index and table access
methods) and the additional UNDO layers built on top of this engine.
This cluster-wide UNDO engine draws on two lines of prior work proposed on
pgsql-hackers: the zheap storage engine and its undo-log infrastructure, and
the zedstore columnar engine whose design discussion shaped the record and
apply model used here.
Author: Greg Burd <greg@burd.me>
Co-authored-by: Amit Kapila <amit.kapila16@gmail.com>
Co-authored-by: Dilip Kumar <dilipbalaut@gmail.com>
Co-authored-by: Thomas Munro <thomas.munro@gmail.com>
Co-authored-by: Kuntal Ghosh <kuntalghosh.2007@gmail.com>
Co-authored-by: Rafia Sabih <rafia.sabih@enterprisedb.com>
Co-authored-by: Mithun Cy <mithuncy@gmail.com>
Co-authored-by: Ashutosh Sharma <ashu.coek88@gmail.com>
Co-authored-by: Andres Freund <andres@anarazel.de>
Co-authored-by: Antonin Houska <ah@cybertec.at>
Co-authored-by: Ashwin Agrawal <ashwinstar@gmail.com>
Co-authored-by: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Co-authored-by: Taylor Vesely <tvesely@pivotal.io>
Co-authored-by: Melanie Plageman <melanieplageman@gmail.com>
Co-authored-by: Alexandra Wang <alexandra.wang.oss@gmail.com>
Changes since v2:
- Remove the unused table AM prepare hook [a1ef4339f9]
- Headerscheck: skip undormgrlist.h [3e9124969b]
- Describe the cluster-wide UNDO recovery model as implemented [3a1784e689]
- Release buffer content locks before applying UNDO inline at abort [1fdfe0a8a9]
- Do not claim a failed abort keeps its WAL hold until backend exit [4825775611]
- Doc: make the UNDO page a chapter, as postgres.sgml includes it [479155221f]
Make the structural filesystem mutations the server performs outside the buffer manager crash-safe and rollback-safe via a WAL resource manager (RM_FILEOPS_ID). Forward operations are WAL-logged and, inside a transaction, register a pending-op / UNDO record so an abort or crash recovery recursively reverts them. FILEOPS records its UNDO in the common WAL stream, so crash recovery reverts a loser transaction's structural changes through the cluster-wide UNDO engine's recovery pass; the AtPrepare path is handled so a prepared transaction's pending file operations survive 2PC. This commit adds the FILEOPS access method and its UNDO plumbing but does not yet route any in-tree DDL/storage caller through it (that is the next commit): the FileOps* API, fileops_redo, the fileops UNDO resource manager (FileopsUndoRmgrInit, registered via undormgrlist.h), the pg_xattr portability wrapper, rmgrdesc + pg_waldump support, the test_fileops module, SQL regression coverage, and recovery TAP tests for crash and 2PC. Uncouple FILEOPS from the FLUX build guard so it builds and registers on its own, ahead of the FLUX table AM. RM_FILEOPS_ID was registered inside the USE_FLUX rmgr block; introduce a dedicated USE_FILEOPS build flag (mirroring USE_FLUX) and move the FILEOPS PG_RMGR line into its own guarded block. The resource-manager ID order (and therefore every WAL rmgr ID) is unchanged in the final build; the only effect is that FILEOPS no longer depends on FLUX being compiled. Author: Greg Burd <greg@burd.me> Changes since v2: - Report FILEOPS UNDO apply failures as failures [f3bd4a3cc7] - Remove obsolete UNDO file cleanup calls [fdf012acad] Tests: - Do not set logical_revert_naptime in 059 and 061 [3072609035] - Show that a crash right after commit loses deferred ops [b34b2463bb]
Route the server's transactional DDL/storage callers that mutate the filesystem
outside the buffer manager through the FILEOPS resource manager added in the
previous commit, so an abort or crash recovery recursively reverts a partial
filesystem change instead of leaving orphan trees behind.
- copydir() gains a register_for_abort_cleanup argument: forward-path callers
register the destination tree with FILEOPS (FileOpsRmtree at abort), while
WAL-replay callers pass false because the redo handler is itself the
recovery action.
- CREATE DATABASE (STRATEGY=FILE_COPY) and ALTER DATABASE SET TABLESPACE copy
their directory trees with abort cleanup enabled, closing the long-standing
crash-asymmetry where a rolled-back copy left an orphan tree.
- CREATE/DROP TABLESPACE route their directory create/remove through FileOps*
so tablespace symlink and directory changes are transactional.
Author: Greg Burd <greg@burd.me>
Add the per-backend UNDO log engine: UNDO records for a backend's own
changes are written to dedicated undo-log segment files (base/undo,
routed through a new SMGR_UNDO storage manager and SYNC_HANDLER_UNDO
sync handler) rather than into the common WAL stream. This is the
zheap-derived UNDO-log infrastructure, adapted to sit alongside the
cluster-wide UNDO engine.
* The engine itself lives under access/undo/perbackend/: log allocation
and metadata (pbu_undolog.c), record assembly and insertion
(pbu_undorecord.c, pbu_undoinsert.c), the undo-action apply machinery
(pbu_undoaction.c, pbu_undoactionxlog.c), rollback-request queues
(pbu_undorequest.c), discard (pbu_undodiscard.c), the discard worker
and undo launcher (pbu_discardworker.c, pbu_undoworker.c), crash
recovery (pbu_recovery.c), the segment-file smgr (pbu_undofile.c),
and helpers/compat shims (pbu_helpers.c, pbu_compat.h).
* Two new resource managers, UndoLog and UndoAction, with their WAL
records described by pbu_undologdesc.c and pbu_undoactiondesc.c, and a
two-phase resource manager (TWOPHASE_RM_PERBACKEND_ID) so a prepared
transaction's per-backend undo chain-head is persisted at PREPARE and
reverse-applied on ROLLBACK PREPARED / discarded on COMMIT PREPARED.
* Integration: the per-backend engine's shmem, LWLock tranche
(UNDO_LOG_PBU), workers, checkpoint snapshot (pg_undo/<redo> via
CheckPointUndoLogs / StartupUndoLogs), and crash-recovery apply
(PbuPerformUndoRecovery) are wired into undo.c, xlog.c,
xlogrecovery.c, xact.c, twophase.c, smgr.c, and sync.c. The default
max_worker_processes is raised from 8 to 12 to leave parallel query
its budget alongside the always-on per-backend UNDO workers.
Based on the zheap undo work by Thomas Munro, Amit Kapila, Dilip Kumar,
Kuntal Ghosh and others.
Author: Greg Burd <greg@burd.me>
Co-authored-by: Amit Kapila <amit.kapila16@gmail.com>
Co-authored-by: Dilip Kumar <dilipbalaut@gmail.com>
Co-authored-by: Thomas Munro <thomas.munro@gmail.com>
Co-authored-by: Kuntal Ghosh <kuntalghosh.2007@gmail.com>
Co-authored-by: Rafia Sabih <rafia.sabih@enterprisedb.com>
Co-authored-by: Mithun Cy <mithuncy@gmail.com>
Co-authored-by: Ashutosh Sharma <ashu.coek88@gmail.com>
Co-authored-by: Andres Freund <andres@anarazel.de>
Co-authored-by: Antonin Houska <ah@cybertec.at>
Changes since v2:
- Apply the whole per-backend chain on ROLLBACK PREPARED [5fa0818211]
- Pass a committed subtransaction's per-backend undo to its parent [f7c9badf81]
- Key the per-backend rollback hash table on the full RollbackHashKey [313e36bf69]
- Size per-backend undo log banks by logs per bank [62b66796e5]
- Fix stale per-backend UNDO comments [15ab46b204]
- Do not WAL-log undo apply progress for unlogged undo logs [c4ddfa2eaa]
- Report a per-backend undo record with no handler as not applied [840d38aae8]
- Tighten the recovery/075 header comment [2801df9130]
- Never re-apply undo already applied by ROLLBACK TO SAVEPOINT [9f8e0fea0a]
- Retry per-backend rollback requests an undo apply worker could not finish [ef5d7c2c13]
- Always apply temporary undo in the aborting backend [e90258a472]
- Discard temporary per-backend undo once its transaction ends [294017447a]
- Do not buffer unlogged and temp undo pages as permanent [f6d492c687]
- Empty unlogged and temp undo logs at the end of crash recovery [43b941aeb0]
- Roll back every persistence level's per-backend undo chain [42c24e555f]
- Retry savepoint undo once the subtransaction's catalog changes are undone [933792cf34]
- Nothing-to-do worker requests are no longer retried (drop the TODO in recovery/085) [73784d8c6b]
- Do not WAL-log discards and extensions of non-permanent undo logs [6aa1d5640c]
- Per-backend UNDO redo: restore undo-page full-page images [0048aa395a]
- Allow per-backend undo records to span three pages [c69a531178]
- Allow WAL-logged amendments to owned undo payloads [1dbd03bbc8]
- Remove the unused worker error-output helper [f4a909bbd1]
- Preserve checksums across undo segment lifecycles [dc5d914e36]
- Add a per-backend subtransaction undo dispatcher [a0185f0eb9]
- Skip already-applied records in subtransaction undo dispatch [e423be2e75]
- Skip completed undo records when retrying an outer abort [79f2f00a5a]
- Start undo log recovery from a newer snapshot when pg_undo lacks one [ed83186497]
- Make pbu_undoinsert.h self-contained [97a54337b0]
- Make the undo discard worker unable to spin, and stop it on shutdown [07a7326b62]
- Describe per-backend UNDO rollback, recovery, and a snapshot limit [694472a2a8]
- Never WAL-log the meta of a non-permanent undo log [05c3499672]
- Undo redo: do not rewrite an undo page that already holds the record [f6881ed0ba]
- Recovery/075: wait for the worker to fail instead of sleeping [66863e4b21]
- Per-backend UNDO: drop comments about zheap code that is not here [66414f6b47]
- Per-backend UNDO: apply project formatting (6 files) [e93e8088de]
- Per-backend UNDO: apply pgperltidy to its tests (11 files) [ea694d86a3]
- Per-backend undo: finish an abort's rollback once its catalog changes are undone [fc5034ceb4]
- Per-backend UNDO: add PbuPendingUndo to typedefs.list [c5a2e6e616]
- Document the fixed startup cost of the UNDO background workers [627174dacb]
Tests:
- Roll back a row updated two and three times [4fecd09e0d]
- One aborted transaction, every applier (recovery/084) [7fe8079db9]
- ROLLBACK TO an empty savepoint reverts nothing (recovery/084) [2af8450b41]
- Roll back changes to a table the transaction then rewrote [8b89a28a03]
- Poll with poll_query_until instead of usleep loops [2d9d668c52]
- Compare 089's marker row with only its CLOG hint bits masked [6413e9272b]
- Reproduce subtransaction abort undo after storage removal [f42a116719]
- Amcheck the index after UPDATE, REINDEX and rollback [31b45dc989]
- Guard recovery against a missing undo-log xid mapping [da416a8db4]
Add asynchronous UNDO application on top of the cluster-wide and
per-backend UNDO engines. Instead of applying an aborted or in-doubt
transaction's UNDO chain synchronously at abort, ROLLBACK PREPARED, or
end of recovery, the transaction is recorded in an in-memory Aborted
Transaction Map (ATM) and its chain is reverse-applied later by a
background logical revert worker.
* The ATM (atm.c, atm.h, atm_xlog.h) is a crash-safe map of aborted
transactions and their UNDO chain-head LSNs, backed by the sLog
(slog.c, slog_flathash.c, slog_tuple.c, slog.h and friends): a
shared-memory, DSA-backed store with an optional per-tuple tracking
hash a consumer AM may extend. A new ATM resource manager
(rmgrdesc/atmdesc.c) makes ATM inserts/forgets crash-safe, and
CheckPointATM / ATMReloadFromCheckpoint carry the map across a
checkpoint that advances the redo pointer past an un-forgotten entry.
* The logical revert worker (logical_revert_worker.c) is a per-database
background worker, spawned by a launcher registered in postmaster.c,
that drains the ATM by walking each recorded UNDO chain via
ApplyUndoChainFromWAL().
* Abort, ROLLBACK PREPARED, and the end-of-recovery deferral queue now
route to ATMAddAborted() instead of applying inline; abort keeps a
synchronous fast path for small transactions governed by
undo_instant_abort_threshold.
* Adds the SLOG/ATM LWLock tranches and wait events, the sLog shmem
subsystem, and the ATM/sLog/logical-revert GUCs.
Author: Greg Burd <greg@burd.me>
Changes since v2:
- Replace the dead UNDO log stats with a retention report [ebdd152b37]
- Fix comments that contradict the code [112a0c74ac]
- Always roll back non-index UNDO inline, whatever its size [7cb511bb5d]
- Keep interrupts held when an inline UNDO apply throws [37360eaa7a]
- Revert only objects that still show the operation being undone [514f51c381]
- Register pg_stat_get_undo_logs() [cda0d6ea92]
- Make an ordinary filesystem error an ERROR, not a PANIC [41b74acee7]
- Say plainly that syscall-first leaves an unlogged crash window [9aaad487fc]
- Remove the whole new database directory on abort or crash [030472f3c9]
- Roll back in-flight transactions whose UNDO precedes the checkpoint [8bb6b33b89]
- Do not chain new batches onto a rolled-back subtransaction's [da620b2a3d]
- Do not lose a ROLLBACK PREPARED that crashes before the ATM entry [84429a4c0e]
- Pgindent: typedefs for the in-flight UNDO record [4eb0c3db7e]
- Make FILEOPS abort effects survive WAL replay [e6e8b9732f]
- Restore prepared row locks without clearing other owners [8d76c9354e]
- Remove the unused sLog before-image API [0d0e167bea]
- Complete early error cleanup during inline undo abort [d77c63c85d]
- Remove dead cluster-wide UNDO compatibility code [e329c8fc0e]
- Remove no-op sLog writer brackets [08757e6f74]
- Drop the O(1) claim from the ATM instant-abort description [381cb66f4a]
- ATM tests: aborting into a full ATM must not fail [cff6ca71b1]
- Recovery/126: create the test table USING heap [60c0b7adbf]
- Apply pgperltidy to its tests (2 files) [f1a4d5a0f5]
- FILEOPS routing: apply pgperltidy to its tests (1 file) [818a5dc556]
Tests:
- Wait for the log, not a sleep loop, in test_idxundo/004 [456281b4e2]
- Show that a failed inverse fsync during abort re-enters abort [426f98c779]
- A cluster that never uses UNDO must cost what master does [90fb63c170]
When a transaction aborts, mark the B-tree leaf entries it inserted LP_DEAD
during rollback, so an aborted transaction leaves no index work for VACUUM.
Controlled per table by a new heap reloption, index_undo, which defaults to
on:
CREATE TABLE t (id int);
CREATE INDEX ON t (id);
BEGIN; INSERT INTO t SELECT g FROM generate_series(1,100) g; ROLLBACK;
-- the 100 index entries are LP_DEAD now, with no VACUUM
The heap tuples are unchanged: they are dead by xmin as always, and heap
writes no UNDO. Only the index entries are cleaned up early.
Each leaf insert writes one small INSERT_LEAF record into the cluster-wide
UNDO-in-WAL stream: the index OID, the (block, offset) of the insert, and a
discriminator -- the entry's heap TID, its length, and a 4-byte digest of its
key bytes. The record does not carry the IndexTuple, so its size does not
grow with the key width. Records are deferred (DeferXactUndoData()) and
flushed as one XLOG_UNDO_BATCH per statement, and at every point that can
consume the chain: commit, abort, subtransaction commit and abort, and
PREPARE. On abort, AtAbort_XactUndo() walks the chain and dispatches each
record to nbtree_undo_apply(), which marks the entry LP_DEAD and writes a
CLR so the rollback is idempotent under crash recovery.
The recorded offset is where the entry was, not where it is: a later
lower-key insert shifts it right on the page, and a split can move it to a
sibling. The apply path therefore searches -- recorded offset, then the
page, then up to eight leaf right-links -- and kills a slot only if its heap
TID, length and key digest all match. Any mismatch leaves the entry for
VACUUM, which is safe; killing the wrong slot would make a committed row
unreachable through the index.
Structural changes -- splits, new roots, internal downlinks, posting-list
splits, deduplication -- are not recorded or reversed, because concurrent
readers may already have seen them. The aborted leaf entries are killed
wherever the split moved them, and pages left empty are recycled by VACUUM
as before.
RelationUsesIndexUndo() is the single gate for the write path, and returns
false (feature off for that relation, never an error) for: catalogs and
TOAST, bootstrap, relations that are not WAL-logged (TEMP, UNLOGGED), and
indexes created or reindexed in the current transaction, whose abort drops
the whole index anyway -- the last is what keeps CREATE INDEX from writing a
record per row. Index UNDO is a property of the relation, not of the table
AM: it does not depend on whether the table AM writes its own UNDO, and
plain heap uses it.
Crash recovery replays the UNDO batches like any other WAL; a transaction
with no commit record has its chain applied, deferred to the ATM and the
logical revert worker where the syscache is not yet available.
src/test/modules/test_idxundo covers rollback, page splits during an
aborted transaction, subtransactions, statements larger than one batch, 2PC,
and a kill -9 mid-transaction recovery test that checks index/heap agreement
and amcheck. The hash and FLUX cases are added by the commits that add
those features.
Known limitation: ROLLBACK PREPARED does not drive the apply path, so the
entries of a rolled-back prepared transaction are left for VACUUM as
before. test_idxundo records this.
Cost. Measured on a 32-vCPU EC2 instance, release build, 8 clients, each
transaction inserting 10 random int keys into a heap table with one btree
and one hash index (hash index UNDO is added by the next commit) and then
committing or rolling back at the given rate;
60 s runs, index_undo on and off interleaved, median of 5:
abort TPS on/off WAL/txn on/off index bytes per live row on/off
0% 27583 / 28692 -3.9% 3548 / 2364 67.5 / 65.7
40% 26509 / 31895 -16.9% 4142 / 2371 82.5 / 109.6
80% 25901 / 34153 -24.2% 4746 / 2370 179.6 / 344.9
Every one of the five paired runs was within 1.2 points of the median at
40% and 80%, and within 3.7 at 0%. The index-size effect grows with the
abort rate (1.3x smaller at 40%, 1.9x at 80%); the throughput cost grows
with it too, because an aborting transaction now does the rollback work
VACUUM would otherwise have done later. At 0% aborts the cost is the WAL
for records that are never needed. Workloads that commit nearly
everything and can afford VACUUM can set index_undo = off per table.
Under the same concurrent 80%-abort load (30 s), 3695 of 6.37M aborted
btree entries (0.06%) were left live for VACUUM, the ones a split moved
beyond the right-link search bound; with index_undo = off all of them are.
After that run an index scan and a sequential scan returned the same
1571680 rows, so no committed entry was killed.
Author: Greg Burd <greg@burd.me>
Changes since v2:
- Flush pending index UNDO before the commit record [258728b1f4]
- Make undo_instant_abort_threshold measure the real chain size [3f6a91bf0b]
- Hand crash-recovery deferrals to the right database, keep their WAL [e4080312f5]
- Stamp UNDO batches with the top-level xid [dd4bd04cc6]
- Do not count failed UNDO records as applied [2f76b4dc36]
- Apply UNDO newest first in crash recovery too [48a741faf2]
- Retain a deferred rollback's WAL from its oldest batch [a6323be6b5]
- Keep ATM entries whose UNDO chain could not be read [f8d3e4124e]
- Hand off only the permanent chain of a failed subtransaction rollback [1bbdb8f2bd]
- Keep one unfinished ATM entry from blocking the revert worker [09db3aa00d]
- Retain a recovery-deferred chain from its real first batch [67730b0bc7]
- Abort failed logical revert worker transactions before retrying [9151cde4a8]
- Fail startup on errors applying per-backend undo [de2b3815ab]
- Apply index UNDO only inline, while the aborting transaction runs [1a267a68f4]
- Test_idxundo: count the dead entries after a rollback that splits leaves [41db174eee]
- Nbtree: write index UNDO for an insert that splits its leaf [05e348c9f2]
- Test_idxundo: wal_consistency_checking must cover index-UNDO CLRs [d42d5f87be]
- Give the Undo rmgr a mask so CLR redo is consistency-checked [e367a010a3]
- Recovery/124: create the test tables USING heap [7dce254c15]
- Nbtree: describe index UNDO's exclusions without naming later features [c059b137a5]
- ATM and revert worker: apply project formatting (2 files) [c00f78fc82]
- Nbtree index UNDO: apply pgperltidy to its tests (2 files) [bf88bbd276]
Tests:
- Test that ROLLBACK PREPARED queues UNDO for the preparing database [92ca01e8ad]
- Test that deferred index UNDO leaves a committed entry on a reused TID alone [5245c737fd]
Extend the index UNDO added for nbtree in the previous commit to hash indexes: on a table with index_undo on (the default), the entries a transaction inserted into a hash index are marked LP_DEAD during rollback rather than left for VACUUM. _hash_doinsert() writes one INSERT record per entry into the cluster-wide UNDO-in-WAL stream, tagged UNDO_RMID_HASH: the index OID, the (block, offset) of the insert, and the same discriminator nbtree uses -- heap TID, length, and a 4-byte digest of the stored hashkey. Records are deferred and batched exactly as for nbtree. On abort the chain walk dispatches each one to hash_undo_apply(), which marks the entry LP_DEAD and writes a CLR for crash-recovery idempotency. The recorded offset is not reliable by rollback time: _hash_pgaddtup() keeps a page in hashkey order, so a later insert with a smaller hashkey shifts the aborted entry up a slot, and a bucket split or overflow allocation can move it within the bucket. The apply path searches the recorded offset, then the page, then up to eight pages along the bucket's overflow chain, and kills a slot only when heap TID, length and digest all match. Killing a relocated slot would mark a committed entry dead, making its row unreachable by any hash scan -- so any mismatch leaves the entry for VACUUM instead. Gating is shared with nbtree through RelationUsesIndexUndo(), including the "index created in this transaction" exclusion. That one matters more for hash than for nbtree: hashbuildCallback() passes a live heapRel into _hash_doinsert() during a build, where nbtree passes NULL, so without it CREATE INDEX would write one UNDO record per indexed row. test_idxundo gains hash cases for plain rollback and for the deferred-batch path. Measured under a concurrent 80%-abort insert load, 266 of 6.37M aborted hash entries (0.004%) were left for VACUUM; the cost figures in the previous commit's message were measured with both a btree and a hash index on the table. Author: Greg Burd <greg@burd.me> Changes since v2: - Restore UNDO abort ordering and persist FILEOPS ownership [c0202ed6c8] - Preserve FILEOPS ownership for deferred crash losers [e39358b589] - Checkpoint at promotion when startup undid FILEOPS losers [bfe3f394ca] - Apply subtransaction undo before logging storage removal [8d784c8bc0] - Nbtree: skip index UNDO outside the aborting backend [b17de80480] - Hash: skip index UNDO outside the aborting backend [50d7399bfe] - Test_idxundo: a hash rollback must set the page's dead-tuples hint [dd25eb77e3] - Hash: set the dead-tuples hint when UNDO kills an aborted entry [cbcd97c139] - Test_idxundo: make the 2PC case deterministic [422215b692] - ATM: never fail to record an aborted transaction while aborting [133259d44b] - PANIC when an inverse cannot be made durable during abort [9f1f9e60c6] - Hash: describe index UNDO's exclusions without naming later features [67dc0c6dcc] - Apply project formatting (1 file) [9e994d0e34] - Apply pgperltidy to its tests (12 files) [88d387b23a] Tests: - Drop the blank line left at the end of 010 [b287419f0a]
Add the nbtree delete-marking machinery an in-place-UPDATE table AM requires, so
an indexed-column UPDATE can keep the row's TID instead of moving it.
- nbtree delete-marking: an index entry can be marked deleted in place and
later reactivated, so an in-place UPDATE of an indexed column inserts the
new key and delete-marks the old one at the same TID instead of moving the
row. amcheck and index scans agree with a sequential scan across such
UPDATEs, including the A->B->A case. The in-place UPDATE index reversal is
idempotent and durable so a rollback or crash-recovery re-drive is safe.
- Table-AM plumbing the feature keys off: the am_index_delete_marking routine
field and the RelationSupportsDeleteMarking() accessor. Index-only scans
are suppressed for indexes on a delete-marking table (a delete-marked
entry's key may be stale -- Phase 5 invariant I4), enforced in the planner.
- Includes the test_deletemark module and its recovery TAP test.
The nbtree/hash index UNDO apply handlers that previously shared this commit are
now separate commits earlier in the series: they are a different feature (revert
provisionally-inserted index entries when a transaction aborts) from
delete-marking (mark a stale index entry in place for a stable-TID UPDATE), and
separating them keeps their defects reviewable on their own.
This commit narrows the index UNDO write hooks to exclude a delete-marking
parent table: such an AM drives its own index cleanup on rollback through its
table UNDO (remove the new entry, clear the old delete-mark), so the index AM
must not also write index UNDO or the two would fight. Because every in-tree
AM that supports UNDO also sets am_index_delete_marking, this is what makes the
index UNDO write hooks unreachable in-tree; see those commits' messages.
No table AM sets am_index_delete_marking yet; the first consumer (FLUX) arrives
in a later commit.
Author: Greg Burd <greg@burd.me>
Changes since v2:
- Nbtree: a uniqueness check asks the table AM about a delete-marked entry [094bcd255b]
- Nbtree: delete-mark a full leaf's entry atomically, with a replace split [9049cd3056]
- Nbtree: keep LP_DEAD off delete-marked and demoted entries [d0e28e1310]
- Amcheck: handle nbtree delete-marked entries [215a5ea9b0]
- Nbtree: leave room for the tombstone trailer in delete-marking indexes [14ad7d28e4]
- Nbtree: describe delete-marking without development phase labels [1bade0b4df]
- Nbtree: document delete-marking in the README [873d5a9fd5]
- Nbtree: describe XLOG_BTREE_DELETE_MARK's deleted flag as it is used [d1c043661e]
- Nbtree: read a tombstone's heap TID from its trailer when splitting [8fb67e167a]
- Nbtree: keep the index UNDO comment in _bt_insertonpg as first written [19dfab47b2]
- Nbtree: mark btundomark's search stack as used only by assertions [96a9fc1b30]
Tests:
- Old snapshot loses a row while nbtree delete-marks it [6de97aaa51]
Heap writes a new tuple version at a new TID on every UPDATE. The executor asserts as much in ExecUpdate and ExecDelete: when table_tuple_lock with TUPLE_LOCK_FLAG_FIND_LAST_VERSION returns TM_Ok for a row that the first attempt found concurrently updated, it must have followed the update chain to a different TID, so TM_FailureData.traversed must be set. An AM that updates rows in place reaches the latest version without moving, correctly reports traversed = false, and trips the assertion although nothing is wrong: EvalPlanQual is then run on the latest version exactly as for heap. Add a TableAmRoutine flag, am_inplace_update_keeps_tid, for an AM whose rows keep their TID for their whole lifetime (so the TID names only the newest version), and RelationUpdatesInPlace() to test it, false for relations without a table AM. Accept traversed = false in the two assertions for such an AM. Later commits use the same test where the core relies on the TID naming a particular version. Heap leaves the flag false; the change only weakens two assertions for AMs that set it. No in-core AM sets it yet. The FLUX table AM does, and its isolation test flux_lock_recheck fails this assertion without the change.
ExecUpdate reads the old row into oldSlot, calls table_tuple_update, and afterwards still uses the old row: RETURNING OLD projects from oldSlot, and ExecARUpdateTriggers re-fetches the old row by TID for the AFTER ROW UPDATE triggers. Both rely on heap writing the new version elsewhere. For an AM whose UPDATE overwrites the row in place (RelationUpdatesInPlace), oldSlot may point into the storage that was just overwritten, and the TID yields the new version, so an AFTER trigger would be handed NEW as OLD. Before updating such a row, materialise oldSlot, in ExecUpdate, in MERGE's UPDATE action and in logical replication apply (ExecSimpleRelationUpdate's searchslot). Pass the copy down: ExecUpdateEpilogue takes oldSlot, and ExecARUpdateTriggers takes a new old_image argument, which it uses instead of the TID re-fetch when the relation updates in place (setting tableoid and the TID on the trigger slot as the fetch would). Every other caller passes NULL. Heap is unaffected: both the materialisation and the use of old_image are gated by RelationUpdatesInPlace(), and heap keeps the TID re-fetch. This changes the signature of ExecARUpdateTriggers (one new argument); out-of-tree callers pass NULL. Exercised by FLUX's regress tests flux_deferred_trigger, flux_dml_semantics and flux_fk.
…apshot EvalPlanQualFetchRowMark refetches the row of a non-locked relation (ROW_MARK_REFERENCE) by TID with SnapshotAny. That is right for heap, whose TID names the version the query saw. An AM that updates rows in place keeps the row's newest version at that TID, so the recheck could join the locked row with values written by a transaction that committed after the statement's snapshot: SELECT ... FOR UPDATE OF a1 over a self-join returned a2 with the concurrent update's values where heap returns the ones the query saw. For such an AM fetch with the recheck estate's snapshot, the statement's, which yields the version the scan returned. If it sees none, fail as for heap. Heap's path is unchanged. Heap-neutral: gated by RelationUpdatesInPlace(). Tested by the isolation specs flux_tid_fetch_snapshot and its heap control heap_tid_fetch_snapshot (identical expected output), added with FLUX.
An AFTER row trigger event records the row's TIDs, and the trigger re-fetches the OLD and NEW rows by TID when it fires, possibly at commit for a deferred trigger. That relies on the TID naming the version the event was queued for. For an AM that updates rows in place (RelationUpdatesInPlace) the TID yields the newest version by then, so a trigger would see a later version, and the RI triggers' "is this row version still live?" test (RI_FKey_check's table_tuple_satisfies_snapshot with SnapshotSelf) would be asked about the wrong version. For such a relation AfterTriggerSaveEvent stores the event's rows with the event, as heap-format copies made by the AM slot's copy_heap_tuple callback. When the event fires, the images are loaded with ExecCopySlot into the relation's trigger slots, which are of the AM's own slot type, as on the TID path; the AM's copyslot callback decides what of the version it keeps. The table AM contract is unchanged: tuple_satisfies_snapshot is still handed a slot of the AM's type. An AM that sets the flag must make copy_heap_tuple and copyslot carry what identifies the row version, so that tuple_satisfies_snapshot can judge that version rather than the row's newest; am_inplace_update_keeps_tid documents this. Heap never takes this path (the flag is false), so its events and memory use are unchanged. Tested by FLUX's regress tests flux_deferred_trigger and flux_fk (deferred FK checks across in-place updates) and flux_dml_semantics (tableoid on OLD/NEW).
The apply worker detects that a replicated UPDATE conflicts with a local row after the fact: table_tuple_update stores the new row, ExecInsertIndexTuples inserts its index entries without raising duplicate errors, and CheckAndReportConflict reports update_exists or multiple_unique_conflicts. An AM that updates rows in place may maintain the indexes of a key change inside table_tuple_update and report TU_None; a duplicate then surfaces as a plain unique violation, with no conflict statistics, no conflict log entry and no DETAIL naming the local row and its origin for ALTER SUBSCRIPTION ... SKIP. For such an AM, look for the conflict before the update, and only in the unique (arbiter) indexes whose key the update changes: an unchanged key still belongs to the row alone. index_key_may_change() compares the columns the index uses, including those of its expressions and predicate. The row keeps its TID, so passing it as the new row's TID makes the check ignore the row's own entries. A conflicting row committed between the check and the update still makes the update fail, with a unique violation; the retried transaction then reports the conflict. Heap-neutral: gated by RelationUpdatesInPlace(). Tested by subscription test 104_inplace_am_update_conflict, added with FLUX.
With retain_dead_tuples, the apply worker tells a replicated UPDATE of a row deleted locally (update_deleted) from one of a row that never existed (update_missing) by searching for the deleted version with a SnapshotAny scan, through the replica identity index or sequentially, and judging each version with HeapTupleSatisfiesVacuum. Both halves assume heap: SnapshotAny is not required to return deleted versions, and update_most_recent_deletion_info casts the scan slot to a BufferHeapTupleTableSlot. Add a scan option, SO_RETURN_DELETED: with SnapshotAny, also return the versions a committed or in-progress DELETE removed. Heap's SnapshotAny always returns them, so heap ignores the flag. RelationFindDeletedTupleInfoSeq and RelationFindDeletedTupleInfoByIndex pass it. update_most_recent_deletion_info keeps the heap test for a buffer heap tuple slot; for any other slot it reads the deleter from the xmax system column and accepts the version as heap accepts HEAPTUPLE_RECENTLY_DEAD: the deleter committed, and not before oldestxmin. Heap behaviour is unchanged (same slot type, same test, flag ignored). Tested by subscription test 105_inplace_am_update_deleted and 035_conflicts with FLUX as the subscriber's table AM.
table_block_relation_estimate_size() estimates the tuple density of a never-vacuumed relation from the column widths, the AM's per-tuple and per-page overheads, and the relation's fillfactor. When the relation has no fillfactor reloption it assumes HEAP_DEFAULT_FILLFACTOR (100). An AM that fills its pages to a different default, as FLUX does to leave room for in-place updates that grow a row, would therefore overestimate the rows per page. Its only alternative is to copy the helper. Add table_block_relation_estimate_size_extended(), which takes the AM's default fillfactor. table_block_relation_estimate_size() becomes a wrapper that passes HEAP_DEFAULT_FILLFACTOR, so heap and every existing caller behave exactly as before. Heap-neutral. Exercised by FLUX's planner size-estimate regress tests.
…coded REPACK (CONCURRENTLY) replays the changes made during the rewrite from logical decoding, so a table AM whose changes are not decoded would lose them silently. REPACK therefore refused every AM but heap, by comparing the routine with heap's, because nothing in TableAmRoutine said whether an AM's changes are decoded. Add that: a bool am_logical_decoding, which promises that every committed row change of a logically logged relation is decoded as an INSERT, UPDATE or DELETE with the usual tuple images and that an aborted change is not. Heap sets it, and the REPACK check tests it instead of naming heap. No behaviour change for heap or for any AM that leaves it false; only the DETAIL of the refusal changes, so the one test that matched it (recovery/106) is updated.
… point Two heap-neutral helpers a table AM that is not heap needs from the core VACUUM machinery. vacuumparallel.c assumes a table AM keeps one parallel vacuum context for its whole VACUUM: parallel_vacuum_end() hands back the index statistics only once, at the end, and the next pass of a context reads the statistics the previous pass left in shared memory. A table AM that must leave parallel mode between its index passes (because its table phases do things parallel mode forbids) creates a context per pass and loses the statistics of the earlier passes. parallel_vacuum_set_index_stats() loads an array of per-index statistics, as parallel_vacuum_end() returned them, into a new context, so each index AM gets its earlier statistics back as input, as in a serial VACUUM. Heap does not call it. vac_bulkdel_one_index() is where every table AM's VACUUM hands its dead TIDs to an index, so a test that stops at the vacuum-bulkdel-one-index injection point sees a VACUUM whose table scan is complete and whose index vacuuming has not started; a later test uses it to compare pg_stat_progress_vacuum for heap and FLUX at the same point. No behaviour change; heap reaches the injection point but never calls the parallel helper.
pg_check_visible and pg_check_frozen read every item of a page the visibility map marks all-visible or all-frozen as a heap tuple header and run heap visibility and freezing tests on it. check_relation_relkind accepts any relation kind with a table AM, so a table of another access method gets its items decoded as heap tuples: false reports of corruption, and assertion failures in HeapTupleSatisfiesVacuum on what it takes for xmin and xmax. pgstattuple, pgrowlocks, pg_surgery, pageinspect's heap functions and amcheck's verify_heapam already refuse such tables with "only heap AM is supported"; do the same in collect_corrupt_items, which both functions use. pg_visibility, pg_visibility_map, pg_visibility_map_summary and pg_truncate_visibility_map read only the visibility map fork and the page header's PD_ALL_VISIBLE flag, and keep accepting any table AM that keeps a visibility map in that format.
FLUX is a faithful realization of what zheap was designed to be: a HEAP
replacement that stores old row versions in UNDO instead of leaving dead tuples
in the table, built on today's index and executor APIs with no new index-AM
contract. Always in-place UPDATE at a stable TID, including indexed-column
UPDATE via the nbtree delete-marking introduced earlier; table UNDO flows
through the shared per-backend engine; wide values use standard heap TOAST; a
per-tuple sLog (on the per-backend engine's LWLock-guarded flat hash) carries
the FLUX tuple-lock protocol. Registered as a built-in AM (USING flux).
FLUX is the first table AM to set am_index_delete_marking, so it drives its own
index maintenance for indexed-column UPDATE (insert the new key, delete-mark the
old one at the same TID) and its UNDO reverses that on rollback. It declares
UNDO_ENGINE_PERBACKEND and registers FluxUndoRmgrInit.
Bumps XLOG_PAGE_MAGIC (new WAL rmgrs) and CATALOG_VERSION_NO (new built-in AM).
tidbitmap.h's shared per-page ceiling is sized for the widest surviving table AM
(FLUX) so bitmap index scans of dense FLUX pages do not overflow the offset
ceiling; a StaticAssert in flux_handler.c keeps the literal bound honest against
MaxFluxItemsPerPage.
Registers the FLUX WAL resource manager (RM_FLUX_ID) and wires FLUX's 2PC
callbacks (TWOPHASE_RM_FLUX_ID) alongside the per-backend UNDO callbacks; adds
the fluxdesc rmgrdesc + pg_waldump support, FLUX logical-decoding, the FLUX
shmem subsystems, SQL regression coverage, and the flux_conflict / flux_inplace
/ flux_lostupdate isolation specs.
Folds in the full set of FLUX correctness fixes developed as follow-ups: emit
no UNDO record for INSERT (a rolled-back INSERT is reclaimed by the page's
dirty set); delta before-images for UPDATE undo records; the ON CONFLICT hang
and lost-update race fix in the tuple-lock protocol; serialize concurrent plain
UPDATE via a persistent tuple lock so EvalPlanQual re-execution cannot lose an
update; and the in-place-UPDATE growth underflow / does-not-fit handling.
Known core-contract gap for in-place AMs (not FLUX-specific):
tmfd.traversed means "the locked tuple is at a different TID than the caller
requested", so an AM with stable TIDs and in-place UPDATE must always report
false.
But nodeModifyTable.c asserts traversed at two sites after a
TUPLE_LOCK_FLAG_FIND_LAST_VERSION lock returns TM_Ok, which is to assert that
reaching the latest version of a row requires having moved -- true for heap and
false for an AM that updates in place. This commit adds
am_inplace_update_keeps_tid to TableAmRoutine, sets it for FLUX, and relaxes
both asserts to accept traversed = false from an AM that sets it. Heap leaves
the flag false, so the asserts are unchanged for heap.
AFTER-trigger OLD/NEW images and statement-level transition tables are known
open for in-place AMs. AfterTriggerExecute fetches OLD from ate_ctid1 and
NEW from ate_ctid2, both through table_tuple_fetch_row_version() under
SnapshotAny; for a stable-TID in-place AM those two calls are byte-identical at
the AM boundary, so the AM has no information to branch on. Fetching OLD and
NEW through the same (tid, SnapshotAny) call is only well defined when the old
version is a physically distinct tuple that still exists. Until core captures
OLD at queue time -- while the page still holds it -- or requests a version
explicitly, no in-place AM can answer both correctly. UPDATE ... RETURNING
old.* is correct because the fetched old tuple is materialized while its page is
still pinned, before the in-place write.
Halloween-problem fix: the reserved, unused t_gen word in FluxTupleHeader is
reused as t_cmin, carrying what heap keeps in t_cid -- the command id of the
writer identified by t_xmin. The field replaces a word that was already
reserved and never read, so the tuple header stays 40 bytes and the on-disk and
WAL byte layouts are unchanged (the tuple body, including this field, is logged
verbatim).
The invariant is that t_cmin always describes the SAME writer as t_xmin. A
FLUX in-place UPDATE re-stamps t_xmin to the updater, so it must re-stamp
t_cmin to the updater's command id; carrying the original INSERT's cid across
an in-place UPDATE pairs a new xmin with an old cid and makes the
self-visibility test compare our own curcid against a different transaction's
cid. The invariant is enforced at all three sites that stamp the pair: plain
INSERT, the CAS/out-of-place new-tuple path, and the in-place UPDATE re-stamp.
Without the field a statement cannot distinguish its own output rows from its
input rows, which is the Halloween problem: an UPDATE that relocates a row to a
new TID (FLUX's out-of-place path) can have the very same scan re-find the row
it just wrote and update it again. Measured before the field existed:
UPDATE hh SET v = v+1, pad = pad||repeat('y',500) over 2000 rows reported
UPDATE 3194 with 1194 rows left holding v=2, where heap reports UPDATE 2000 and
every row v=1. Six isolation tests also depended on it, including
freeze-the-dead ("unexpected table_tuple_lock status: 1") and a stale SUM in
total-cash.
The index-capability gate and this fix cannot ship apart. The gate widens the
set of UPDATEs that take the relocating out-of-place path, which is the path
that can re-find its own output, so it widens exposure to the Halloween defect.
Measured on the same reproducer: the published tree without either change gave
500 rows with a maximum of 1 update per row; the gate alone gave 522 with a
maximum of 2 -- the defect made visible; the gate together with this fix gives
500 with a maximum of 1, matching the published baseline while retaining the
gate.
New open bug recorded here: FK referential actions (CASCADE, SET NULL, SET
DEFAULT, RESTRICT) are dead on a parent-key UPDATE. The actions are
implemented as AFTER UPDATE triggers on the referenced table, and such a
trigger receives OLD.k == NEW.k for an in-place AM, so ri_KeysEqual() sees no
key change and returns without propagating. This is a direct consequence of
the AFTER-trigger OLD/NEW core-contract gap described above, not a separate
defect in the RI code. It is why the foreign_key regression diff grew from
26847 to 38009 bytes, with missing-error lines going from 2 to 19.
Author: Greg Burd <greg@burd.me>
Changes since v2:
- Treat an aborted, not-yet-reverted writer as invisible [a582433ff3]
- WAL-log t_verptr in the same-size CAS update record [7b8d2317a8]
- Make UNDO apply idempotent, keyed on the record's version [564afee338]
- Helping -- writers revert aborted, not-yet-reverted versions [fb9df8dcc1]
- A restore that cannot be done is an error, not a success [b11d394632]
- Fix comments that say key UPDATEs go out of place [1afdbca771]
- VACUUM reclaims rows of aborted INSERTs [6f957dc736]
- ROLLBACK TO SAVEPOINT applies every persistence level's undo [1689077e0b]
- Do not mark savepoint undo applied when it was not [6500c66015]
- Finish worker rollbacks that have nothing left to do, cap retries [232892a4f4]
- Never help on a hot standby; injection point to hold the undo worker [baa9db02b7]
- Reverse a rolled-back key UPDATE's index changes before its tuple [6745aeaa1c]
- Name the relation file an UNDO record was written against [e656b1e575]
- Delete superseded TOAST values on in-place UPDATE [c558451eb5]
- FLUX/nbtree: X2(c) prune delete-marked tombstones of live rows [11345001e4]
- Tombstone removal: horizon reasoning, untorn copy, test [edaa8e0c52]
- Keep the freeze cutoffs VACUUM FULL / CLUSTER pass in [d1071253e5]
- Index build ignores rows of aborted INSERTs [3563fb7690]
- Tombstone removal must not trust the UNCOMMITTED hint [fab0d7a3b4]
- Scan slots own a copy taken under a writer-excluding lock [8d6ef8eeb8]
- VACUUM truncates the whole empty tail, not one page [103880a71a]
- TID range and TABLESAMPLE scans reconstruct the snapshot's version [1177ed330f]
- Correct the scan comments for copied slots [06a19ab3ca]
- Index-fetch decode must not pad after a varlena [4ffd6b2fdb]
- A waiting UPDATE must not build on an aborted writer's image [47519340af]
- VACUUM must not move a live row to another page (heap control) [151b14356d]
- VACUUM must not move live rows to another page [9daa8a86c9]
- VACUUM honours TRUNCATE false and truncates only a worthwhile tail [9995069082]
- Log the overflow header on UPDATE and speculative insert [d749854b71]
- An UPDATE must not hold a tuple lock per row until commit [3407faac7a]
- VACUUM frees only the line pointers Phase I recorded [01784b5845]
- VACUUM (DISABLE_PAGE_SKIPPING) may truncate, as heap does [b34ce80707]
- Report the pages VACUUM truncated; drop dead VM and diff code [0fd8151ce9]
- A page with an LP_DEAD slot is not all-visible [d10c4bf34c]
- Mask item alignment padding in wal_consistency_checking [e2cd227580]
- Walk the whole version chain; a broken chain is an ERROR [4c3b3030ca]
- Stop using the tuple header word t_writer as a lock [a7c5b9122c]
- A row lock rolled back with its savepoint leaves nothing behind [d1e4248dae]
- A slot owns its tuple copy, in its own memory context [ae020545f3]
- A table rewrite reverts what an aborted transaction left on the page [db84c2c004]
- An existence check sees a row at its committed version while a revert is pending [1db9583be9]
- Judge a uniqueness check's index entry by the version carrying its key [638d069488]
- Report superseded row versions from tuple_lock as heap does [009c743871]
- Don't report the ON CONFLICT row version to the trigger re-lock [9761037e94]
- Lock the latest row version when tuple_lock gets no snapshot [5ff627a4e9]
- 102: replicated DELETE and UPDATE-vs-local-DELETE; drop key-update cases [2b093dfbae]
- Toast every tuple-construction path, not just the single insert [a76b88f135]
- A slot copy of a stored version keeps the version [e7a0da3aff]
- Regress test flux_deferred_trigger (red: stale deferred RI checks) [e652aa9b4b]
- AFTER triggers: an in-place AM's queued rows carry their version identity [f7da3ae7dc]
- Judge a queued trigger row by its own version, not the row's newest [3f78b215ad]
- Index builds and samples read a row's committed version while a revert is pending [03d520d5b4]
- Record the relation's storage with each tracked key [f421e9cde6]
- Commit, abort and PREPARE work skips storage a rewrite replaced [588be24f2f]
- Another transaction's aborted-insert marker hides no version of ours [96b6f0e287]
- Honor key-changing UPDATE lock conflicts in FLUX [49188b3247]
- Wrap FLUX update lock-mode arguments and comments [475f20674e]
- Keep a revived index entry when its UPDATE rolls back [f46ee71756]
- Record and reverse FLUX index revival intent [9bd6cf52d5]
- Use the authoritative FLUX before-image after waiting [8105ba6166]
- Bound FLUX reservations by undo insertion capacity [61aed71efb]
- Apply project formatting to FLUX undo changes [ef0dda76a0]
- Preserve prepared row locks across recovery [32000e1a60]
- Remove the unused sLog before-image path [94f1a5af28]
- Shrink the FLUX header and remove legacy interfaces [00bd6f2045]
- Scope tuple sLog visibility to physical storage [a230837613]
- Retire aborted INSERT markers after CLOG records abort [9e350a608d]
- Retain row lock strength per subtransaction [e685d2f1ea]
- Honor scan direction and cursor position [be0167445c]
- Include missing defaults in physical tuple deformation [07ef9f911a]
- Suppress zero-column index-only scans on delete-marking tables [7fd1c18679]
- Initialize dirty snapshot outputs in FLUX sequential scans [2d85c15f7a]
- Log FLUX replica identity images with inline TOAST values [713f97c60c]
- Decode FLUX speculative insertion outcomes [982dd588e6]
- Protect FLUX history and freeze tuples before advancing VACUUM horizons [f0cb126b60]
- Report FLUX self-modification with its writer xid [8e319365f5]
- Preserve command IDs in materialized FLUX slots [aac52fb769]
- Set the table OID on in-place AFTER-trigger images [302f7048c1]
- Stop version walks at a snapshot-visible writer [c94d739451]
- Leave FLUX subtransaction undo to the pre-abort dispatcher [1b55f46b36]
- Add SSI rw-conflict-out isolation twins for FLUX and heap [6e10bce3b8]
- Report FLUX SSI conflicts out to committed concurrent writers [482415d425]
- Report the writers a FLUX version walk steps over to SSI [4d0b8068c9]
- Reverse in-place index changes in the file the UPDATE changed [a8f31e3e0c]
- Wake row-lock waiters when a savepoint releases the lock [410e584233]
- Report rows moved to another partition as moved, not deleted [95646a89eb]
- Serve the snapshot's version of a row fetched by TID [fb87f1e6e0]
- EvalPlanQual: refetch an in-place AM's unlocked rows by snapshot [f783afec6e]
- Honour the RI crosscheck snapshot in FLUX delete and update [88c5835078]
- Count FLUX sequential scans and their rows in the table statistics [8d536c5e30]
- Make a DELETE wait for every conflicting row locker [a6dedfa7fc]
- Take a tuple's own TID from the caller, not from t_ctid [507b7afcbc]
- Link a row an UPDATE moved to its new version [8c7f0718d5]
- Sort in CLUSTER when the planner asks for it [8399592749]
- Keep what older snapshots see across CLUSTER and VACUUM FULL [530cbd9a00]
- Report apply conflicts for an UPDATE of an in-place table AM [44d628d391]
- Check only the unique indexes whose key a replicated UPDATE changes [c857ec26df]
- Detect update_deleted conflicts on a FLUX subscriber [79939397fa]
- Don't forward-declare an enum in flux_xlog.h [6a8e710076]
- Write the undo of a relation that skips WAL to the unlogged undo log [b7eef2e0a9]
- Resolve hot standby snapshot conflicts when removing tuples [320b50ce4e]
- Take a cleanup lock to compact a page in VACUUM and in redo [f9993bda0a]
- Invalidate logical slots when freezing catalog before-images [10281e5c30]
- Stamp FLUX undo records with the operation's command id [0adf2ff154]
- Copy the locked tuple before releasing its buffer lock [6d94828cc8]
- Let a non-aggressive VACUUM skip compacting a pinned page [72e54be955]
- Say in the UNDO README that FLUX writes per-backend UNDO [0ad0194478]
- Add a Limitations section to the FLUX README [4aaff47cdb]
- Decode an out-of-place UPDATE as one UPDATE [0daae8a5fa]
- Nbtree: report SSI conflict-in when a tombstone is about to be revived [9f8b2696a5]
- Log undo-log meta only for undo in the permanent undo log [b017106c04]
- Clamp line-pointer offsets to MaxHeapTuplesPerPage like heap [deb4426a68]
- Test_deletemark: delete-marking and rollback must handle LP_DEAD [576090c6e6]
- Mask FLUX pages that Undo CLRs change [a12b12c77e]
- Make DELETE redo set the tuple header exactly as the DELETE did [083f5bacb9]
- PANIC when redo cannot apply a record instead of skipping it [2b4c2804fc]
- Say why redo of an undo insert may skip a page [69fdeb161e]
- Free AFTER-trigger images of FLUX row events with their events [5462311e71]
- EvalPlanQual: fail, as for heap, when an in-place AM's reference row is gone [481c6b5839]
- Tuple_satisfies_snapshot takes only a FLUX slot [1b27d86e19]
- AFTER triggers: fire an in-place AM's queued rows in the AM's own slots [04e3e2b8fb]
- Tableam: say what an AM that updates rows in place gets from the core [0c26e2b638]
- FLUX decoding: refuse records it cannot decode instead of guessing [986f1fe97a]
- Recovery tests: wait for the row lock with poll_query_until [b690148fdb]
- Recovery/123: create the heap control table USING heap [8c4a8be06b]
- Remove agent working notes from the FLUX tree [76eb3a65b9]
- Describe the code without development labels [3b75599e8d]
- Tableam: let a block AM's size estimate use its own default fillfactor [16abd691a4]
- Estimate relation size like heap, in FLUX's page geometry [9d0ecd40b6]
- VACUUM counts rows another transaction is writing as heap does [c3d24c668a]
- ANALYZE counts live and dead rows as heap does [a65faf5592]
- Count an UPDATE in pgstat by what it leaves behind [1fa4fbfa77]
- An index fetch keeps its pin on the same page [7529430aec]
- List user_catalog_table reads during decoding as a limitation [e6e01bf3ac]
- Apply project formatting to files several layers change [59538a58cc]
- Apply project formatting (16 files) [44df951f97]
- Nbtree delete-marking: apply project formatting (9 files) [bf7c08d8ac]
- Apply pgperltidy to test scripts several layers change [630e3c34e5]
- Apply pgperltidy to its tests (31 files) [8133d9b96c]
- Nbtree delete-marking: apply pgperltidy to its tests (4 files) [f2f185bd61]
- ATM and revert worker: apply pgperltidy to its tests (8 files) [564b19efb1]
- Apply project formatting to the planner-statistics changes [87f1b4a03e]
- Reverse index changes with the index the rollback restores [67f718509e]
- Keep row-lock holders in on-demand shared radix trees [0b59fcef8c]
- Judge inserts by t_xmin, not by shared per-tuple markers [358e75b1a1]
- Drop the shared tuple flat hash [f3162ad1be]
- Keep row locks of UNDO-based table AMs in the tuple sLog [6a8204a1c8]
- Keep row locks in the tuple sLog [c11322ed19]
- Create the aborted transaction map on first use [39295668de]
- Drop the shared dirty-page map [37feb24b2a]
- Let a row-lock holder wait without queueing on the tuple lock [74ab0ccc34]
- Classify the row again after a DELETE or UPDATE waits [b9f8ff6731]
- Mark the two-phase commit record as used only by assertions [88e2077b77]
- Keep a transaction's first row locks in its own slot [b2e16a8c88]
- Keep a backend's emptied row-lock trees for its next transactions [4c0463f863]
- Update out of place while an index is not valid yet [cc90f4b533]
- Delete TOAST values at DELETE time, as heap does [2a15600109]
- Report VACUUM progress to pg_stat_progress_vacuum [35c4edd70f]
- Vacuum in rounds bounded by maintenance_work_mem [f37fb344f5]
- Vacuum indexes in parallel [cd11dace81]
- Clear the visibility map on a speculative insertion [cd6bd75e21]
- Move a row whose index change delete-marking cannot describe [e5afe3612c]
- Decode only the repacked table's changes for REPACK (CONCURRENTLY) [44b8e5e963]
- Tableam: let REPACK (CONCURRENTLY) accept any AM whose changes are decoded [e2e8f1df24]
- Support REPACK (CONCURRENTLY) [7c26afe815]
- Do not decode an UPDATE made with TABLE_UPDATE_NO_LOGICAL [55ee1f5b0b]
- Set the page LSN of an in-place UPDATE [0639007b96]
- Decide an UPDATE's index work again after waiting for a writer [9e689ca899]
- Clear the visibility map in the WAL record of the change [c4bbededee]
- Move a row whose partial-index membership changes [e1ec1f0c30]
- Report a running writer to a DELETE that does not wait [b123684a50]
- Read the visibility map size again before a change's clear [28c643295b]
- Log visibility map pages set by VACUUM as non-standard [ac2a2752b5]
- Keep the tuple lock while an UPDATE decides again [98ba5ff15d]
- Scan only the requested blocks in index_build_range_scan [24c701e86e]
- Preserve key-share locks across non-key FLUX moves [e3c9030483]
- Use UPDATE lock strength for the delete half of a FLUX move [5590970967]
- Check only relation predicate locks for FLUX inserts [7e71c78c0c]
- Doc: Describe COPY FREEZE behavior on FLUX [c5010bb277]
- Check in-place history before following a non-key move [e7f63b2e9e]
- Run heap and FLUX isolation twins sequentially [ec2b9c53be]
- Keep existing row lockers protected when moving a FLUX row [b4d2684beb]
- Let a row-lock probe visit only the holders of its TID's bucket [9253a45e5e]
- Keep a slot's spilled row locks in hash tables of its own [5731116ee5]
- Number the integrated recovery tests consecutively [c0d6845bc6]
- Per-backend undo: decide the meta record against the current redo point [169068fcf9]
- Refuse user_catalog_table [2c5187e0b1]
Tests:
- Renumber to 076/077, assert the undo-worker gate [d0838896dd]
- Prove the worker ran and skipped what writers reverted [f3c482959b]
- Drop stale recovery test names left by integration merge [d142ba40a0]
- Wait for the discard worker, filter on non-permanent logs [ea54653eb0]
- Run the FLUX isolation specs; UPDATE/DELETE races match heap [efe3d2ed61]
- Test that unlogged and temp DML writes no row-proportional WAL [5b87a5f76d]
- Red test for table rewrites while a revert is pending (A7/P3) [5351e6aaa0]
- Red test for uniqueness checks while a revert is pending [23d5370457]
- Test every update since the snapshot when locking FOR KEY SHARE [524a452a6f]
- Red test for apply-worker row locks under a BEFORE trigger [ac64e6acb7]
- Add flux_toast_foreign regression test (red on this commit) [e15dab607b]
- Red test for index builds while a revert is pending [1ef61e4f20]
- Red tests for commit, abort and PREPARE work after a rewrite [afbcfa9981]
- Test key-changing FLUX updates against existing row locks [865d9df693]
- Exercise FLUX CAS conflicts with a referenced unique key [8baf5c38c1]
- Test an indexed key that returns to an earlier value, then rolls back [f575ad45da]
- Test the A -> B -> A index rollback from an old snapshot and a deferred revert [b01d7b12f4]
- Add physical index rollback regression for FLUX [e35ed32ed4]
- Exercise physical FLUX rollback across workers and replay [5d72e1db5a]
- Exercise amendment replay without an insert full-page image [4490b96fb2]
- Renumber 111_flux_index_aba_revert to 104 [828855758a]
- Fix two schedule lines garbled by an integration merge [432115166f]
- Control trigger self-modification for tuple visit order [6d1dbfa9f1]
- Wait for the standby to replay the rolled-back UPDATE itself [1cb1894037]
- A DELETE or UPDATE waits for every conflicting row locker [6bf93820c4]
- Follow a row that an UPDATE moved to a new TID [2b18d709ab]
- CLUSTER orders the table whichever plan it uses [90511b9f04]
- CLUSTER and VACUUM FULL keep what older snapshots see [ff185c9f91]
- Replicated key update that hits a unique key [a62dbd17c2]
- Replicated UPDATE of a locally deleted row [645440bd7a]
- Undo of a relation that skips WAL at wal_level=minimal [3d8c4a78e8]
- Hot standby conflicts for removed tuples [2ace35b6f9]
- Add a FLUX regression test for a DELETE lost behind a statement trigger [7641959880]
- Replicated UPDATE racing a local key update [11863bcf33]
- Rows written while VACUUM truncates survive [1ba02a7f00]
- An out-of-place UPDATE must decode as an UPDATE [dbb7872a63]
- SSI conflict on a revived index entry [73ebb69bd0]
- Rolling back a revive on a full leaf re-marks the entry atomically [72c6130993]
- Undo-log meta record of a non-permanent undo log [0a10717309]
- Tuple offsets stay within MaxHeapTuplesPerPage [532036f607]
- Amcheck on indexes with delete-marked entries [c4a2b7b021]
- A near-limit index key must survive delete-marking [5efa72fa6b]
- Crash between ROLLBACK PREPARED's abort record and its UNDO [5ef0fd5e21]
- DELETE after an aborted out-of-place UPDATE, under WAL checking [0eebbe0c3e]
- Undo redo without full-page writes keeps a later amendment [ece85bac35]
- Add a test for AFTER-trigger image memory across a long transaction [b1893db984]
- Planner size estimates and the I/O they cost [5e5068bc72]
- The relation statistics VACUUM, ANALYZE and UPDATE report [a395e2d63a]
- An index scan in physical order visits each page once [63ea4b5988]
- ROLLBACK TO SAVEPOINT of an UPDATE then DROP INDEX on FLUX [c0096e65b0]
- FLUX rollback of an in-place UPDATE followed by DDL on its storage [6f9ba7fe2e]
- Row locks must need no fixed shared memory or capacity [5a54afe55d]
- Inserts must need no per-tuple shared marker [3f20f4eebf]
- A row-lock upgrader must not queue behind a waiter [9e0b79b870]
- A writer must classify the row again after a wait [da3357f19b]
- A row-lock conflict probe must never miss a lock [bbb1b25c44]
- Row-lock probes must find locks however few rows a holder locks [8d5c2a7452]
- A row lock that runs the sLog out of memory changes nothing [8b419deae0]
- A row-lock tree kept for reuse must not alias a prepared one [a0975132c8]
- Add isolation test for FLUX concurrent index builds under key UPDATEs [07f91f941c]
- Test that VACUUM handles the TOAST values of the rows it removes [007c4cd62f]
- Test the paths that retire a version's TOAST values [96121fdeb1]
- Renumber recovery tests added by concurrent tracks [1ed5f983da]
- Add isolation test for REPACK (CONCURRENTLY) while another table changes [4096da0629]
- Add isolation test for FLUX REPACK (CONCURRENTLY) with concurrent changes [6569a5231e]
- Add isolation test for decoding while FLUX REPACK (CONCURRENTLY) replays [1a2a87965a]
- Add TAP test for FLUX REPACK (CONCURRENTLY) across crashes and on a standby [0d80b4b0fd]
- Make flux_cic's REINDEX CONCURRENTLY completion order deterministic [1851e7bd4d]
- A change must set the LSN of the pages it logs [021575a149]
- Test an UPDATE that waits for a writer changing the same key [7b33708dd7]
- Test that a change's WAL record clears the visibility map [ae87569765]
- Renumber recovery tests that share a number [e2d8a49835]
- A tombstone revived with another image [49e14c3efb]
- A row that leaves a partial index [fc13d0c2bc]
- A DELETE that does not wait [e46cdd871b]
- A change after VACUUM created the visibility map [cb4487eaf3]
- VACUUM's visibility map bits after replay [a19bb86c4d]
- A waiter keeps its place when it decides its UPDATE again [2be8f9f0ba]
- An UPDATE whose row changes after its index decision [9d85d137d9]
- Hold the WAL writer before the crash [fcde30418c]
- Keep the crash target dirty until eviction [b8eeeee054]
- Describe the REINDEX rollback window precisely [b35d5df66f]
- Use the configured TOAST page size [f76c697bba]
- Allow a safe relfrozenxid equality [e8b6297880]
- BRIN summarizes only the block range it was asked for [233b103b11]
- Pg_visibility's tuple checks refuse a FLUX table [a7012d50a6]
- Test key-share locking across moved non-key updates [719333d9f6]
- Test subsequent non-key updates of a key-share-locked row [325c1e2339]
- Test SSI conflict detection for bulk FLUX insertion [fbaf032019]
- Test replay of non-key FLUX move metadata [83b47e721e]
- Test spilled row-lock holders across their lifetime [de6e5bb7a8]
- Test row-lock allocation failures caught by savepoints [d987cbdfd6]
There was a problem hiding this comment.
🔍 OCR found 195 issue(s).
- 25 inline, 170 in summary (inline capped at 25)
⚠️ 1 warning(s) during review
📄 src/backend/access/flux/README
This limitation is factually wrong on both counts and describes behavior the implementation does not have:
-
FLUX VACUUM does use parallel index vacuuming --
flux_vacuum_indexes_parallel()(flux_operations.c:7533) wrapsparallel_vacuum_init/parallel_vacuum_bulkdel_all_indexes, is reached from the main VACUUM path (flux_operations.c:7991, 8416), and honorsVACUUM (PARALLEL n)(flux_operations.c:7700-7717). -
FLUX VACUUM does report progress to pg_stat_progress_vacuum -- it calls
pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM, ...)and updatesPROGRESS_VACUUM_STARTED_BY(flux_operations.c:7645-7654), andflux_vacuum_one_indexsetsivinfo.report_progress = true(flux_operations.c:7500).
Reviewers will check these claims against the code. Please drop this stale sentence (both the parallel-vacuum and the progress-reporting clauses).
💡 Suggested change
Before:
+ It does not use parallel index vacuuming and does not report progress in
+ pg_stat_progress_vacuum.
After:
+ A non-aggressive VACUUM does not compact a page
📄 src/backend/access/flux/flux_pvs.c (L586-L588)
The walk skeleton here (Brent cycle guard + FluxPbuFetchVersion + flux_pvs_prior_image + replace-candidate) is duplicated almost verbatim across four functions: flux_pvs_walk, FluxFindVersionSuperseder, FluxVersionChainOldestXid, and FluxRewriteChain. Four copies of the same chain-walking loop are a maintenance hazard: a fix to the cycle guard, the discarded-record handling, or the candidate-buffer lifecycle must be applied in all four, and they will drift. Consider extracting a shared iterator (e.g. a small struct holding candidate/allocated/mark/steps/limit with an advance() step that fetches, reconstructs, and swaps the candidate) that each function drives, so the per-tuple chain semantics live in one place.
📄 src/backend/access/flux/flux_pvs.c (L586-L586)
Comment inaccuracy: the cycle guard it refers to lives in flux_pvs_walk (the static helper behind both FluxReconstructVisibleVersion and ...Ex), not in a function named FluxReconstructVisibleVersionEx. Referencing a function that doesn't contain the code it points at will mislead the next reader. Suggest pointing at flux_pvs_walk.
💡 Suggested change
Before:
/* Same loop guard as FluxReconstructVisibleVersionEx (Brent). */
After:
/* Same loop guard as flux_pvs_walk (Brent). */
📄 src/backend/access/flux/flux_pbu.c (L92-L92)
Static state (flux_pbu_prepared, the pinned undo buffers reserved by PrepareUndoInsert, and the undo engine's buffer_idx) is never reset on transaction abort. Verified against the callers in flux_operations.c: FluxPbuPrepareInsert runs BEFORE START_CRIT_SECTION, and the DELETE path (lines 2172-2192) then calls GetCurrentTransactionId() and FluxVMPinForChange(), while the UPDATE path's PG_CATCH (flux_operations.c:6475) only releases overflow buffers and re-throws. Any ereport(ERROR) in that window (or in PrepareUndoInsert itself, which can palloc/extend the log/ereport) leaves flux_pbu_prepared = true, leaks the uur_payload StringInfo, and leaves pbu_undoinsert.c's buffer_idx non-zero. Neither FluxXactCallback (flux_mvcc.c:440) nor FluxSLogXactCallback (flux_operations.c:9136) call UnlockReleaseUndoBuffers/reset this state on XACT_EVENT_ABORT. The next DML op in a new transaction then overwrites the static with stale buffer_idx still set in the undo engine. Register an abort/subabort cleanup (XactCallback) that calls FluxPbuCancelUndo() when flux_pbu_prepared is set, mirroring how the engine expects UnlockReleaseUndoBuffers() to be reached on every path.
📄 src/backend/access/flux/flux_pbu.c (L150-L151)
This Assert is user-reachable, not a can't-happen invariant. Because the prepared static state is never cleared on transaction abort (see the leak noted above), an ereport(ERROR) between a prior op's FluxPbuPrepareInsert and its FluxPbuFinishUndo leaves flux_pbu_prepared == true, so the NEXT ordinary DML in a fresh transaction trips this Assert in assert-enabled builds (and silently proceeds with a stale undo-engine buffer_idx in production builds). Per project convention Assert() must only guard invariants that cannot be reached via a user-triggered error path; fix by ensuring abort reliably resets the state rather than relying on this assertion.
📄 src/backend/access/flux/flux_pbu.c (L470-L473)
FluxPbuCancelUndo has no callers anywhere in the tree (verified by full-repo search: only the definition here and the declaration in flux_pbu.h). The header comment advertises it for "a same-size CAS update that finds no byte actually changed", but the CAS path in flux_operations.c always calls FluxPbuInsertUndo() once prepared and never cancels. This is dead code and the stated no-op-CAS use case is not wired up. Either wire it into the abort/no-op paths (which would also fix the strand-on-error bug above) or remove it (YAGNI).
📄 src/backend/access/flux/flux_pvs.c (L158-L159)
delta->old_len is read from the (untrusted) undo payload and used directly as the palloc size before any validation. FluxUndoDeltaReconstruct only validates the prefixlen/suffixlen/delta_len relationship after this allocation. A corrupt old_len larger than MaxAllocSize therefore raises a generic "invalid memory alloc request size" (ERRCODE_PROGRAM_LIMIT_EXCEEDED) instead of the clean flux_pvs_corrupt() DATA_CORRUPTED report this path otherwise strives for. Consider bounding old_len (e.g. against BLCKSZ/MaxAllocSize) with a flux_pvs_corrupt() call before allocating.
📄 src/backend/access/flux/flux_pbu.c (L560-L565)
The redo-side undo-block search is too narrow and will silently fail to rebuild the undo record for UPDATE/DELETE records that spill to many overflow pages.
FluxPbuRedoUndo() is always called as FluxPbuRedoUndo(record, 1) (flux_xlog.c lines 1504/1941/1968/2521), i.e. first_block_id == 1. This loop then probes only block ids 1 .. 1 + FLUX_PBU_MAX_UNDO_BLOCKS - 1 == 1..4.
But at DO time the undo buffers are registered at flux_undo_block_id, which is the next free block id AFTER all forward blocks: in FluxXLogUpdate/FluxXLogInsert that is 1 + (unique overflow pages) + (cross-page destination). overflow_buffers->count is bounded only by XLR_MAX_BLOCK_ID (32), so flux_undo_block_id can be far greater than 4.
When an UPDATE has 4+ unique overflow pages (TOAST-like out-of-line records), blocks 1..4 are all forward-op blocks, so this loop continues past all of them (the !XLogRecHasBlockRef break never fires) and exits returning InvalidUndoRecPtr. The before-image undo record is then never re-inserted during crash recovery, so the t_verptr version pointer stamped into the replayed tuple dangles: rollback of a loser transaction and MVCC version-chain reads break after recovery.
FLUX_PBU_MAX_UNDO_BLOCKS is the max number of undo pages in one record; it is not a valid search window when the search starts at a fixed block id 1 but undo is registered after a variable number of forward blocks. The redo caller must pass the actual flux_undo_block_id used at DO time (e.g. carried in the forward record's xlrec), or the search must scan the full 1..XLogRecMaxBlockId(record) range for the first undo block.
📄 src/backend/access/flux/flux_slot.c (L872-L876)
The ItemPointerIsValid(&htup->t_data->t_ctid) test is a heuristic sentinel, not an authoritative marker that the heap tuple carries FLUX version identity. heap_form_tuple leaves t_ctid invalid, but a genuine heap tuple read from a real heap-table page has a valid t_ctid (heap sets t_ctid = t_self for live, never-updated tuples). If a non-FLUX TTSOpsHeapTuple slot holding such a tuple is ever copied into a FLUX slot (e.g. certain cross-AM copy paths), this branch will cache that heap tuple's xmin/xmax/cmin as FLUX version identity. It happens to be harmless today only because (a) FLUX uses heap-shaped xmin/xmax so the cached values are at least coherent, and (b) the insert path re-stamps via FluxSlotSetVersion before any sysattr read. That is a fragile contract. Consider a stronger, explicit marker (e.g. require the source slot's tts_tableOid to match a FLUX relation, or key off am_inplace_update_keeps_tid) rather than relying on a t_ctid heuristic that ordinary heap tuples also satisfy.
📄 src/backend/access/flux/flux_slot.c (L137-L147)
This cleanup in release is dead code. ExecDropSingleTupleTableSlot and ExecResetTupleTable always call clear() (tts_flux_clear) before release(); by the time release runs, TTS_SHOULDFREE is cleared, rslot->tuple/values_block are NULL, and buffer is InvalidBuffer. The standard heap/buffer-heap slot release callbacks are deliberately empty no-ops for exactly this reason. Duplicating the free logic here is harmless but diverges from the established slot-ops pattern and risks confusing future readers into thinking release can run on a non-cleared slot. Prefer an empty release body matching tts_heap_release/tts_buffer_heap_release.
📄 src/backend/access/flux/flux_slot.c (L122-L127)
tts_flux_init re-initializes a subset of the custom fields (tuple, tuple_len, off, buffer, values_block) but omits sys_valid/sys_xmin/sys_xmax/sys_cmin. This is not a live bug because MakeTupleTableSlot allocates the slot with palloc0, so every field (including the sys_* cache) is already zero before init runs. But the partial, redundant re-zeroing is inconsistent: either rely on palloc0 and drop these assignments, or initialize all custom fields including the sys_* cache. As written it reads as if init is responsible for field setup while silently depending on palloc0 for the MVCC cache.
📄 src/backend/access/flux/flux_vm.c (L125-L132)
FluxVMSet mutates the VM page, MarkBufferDirty, then conditionally XLogInsert and PageSetLSN with no surrounding START_CRIT_SECTION/END_CRIT_SECTION. The sole caller is FluxVMVacuumPage, invoked from VACUUM (flux_operations.c:8354), which is NOT inside a critical section. Heap's analog visibilitymap_set() asserts InRecovery || CritSectionCount > 0 and relies on its caller's critical section so the map mutation, WAL insert and PageSetLSN are atomic. Here, if an error fires between MarkBufferDirty(vmBuf) and PageSetLSN(page, recptr) (e.g. an interrupt/elog during XLogInsert), the VM page is left dirty with no matching WAL record or LSN, which can produce a torn/inconsistent VM page and replica divergence. Wrap the map modification + MarkBufferDirty + XLogInsert + PageSetLSN in START_CRIT_SECTION()/END_CRIT_SECTION() (or require the caller to hold one, matching heap).
📄 src/backend/access/flux/flux_vm.c (L219-L222)
Same critical-section gap as FluxVMSet: the map byte is cleared, MarkBufferDirty, then XLogInsert/PageSetLSN run outside any critical section (FluxVMVacuumPage -> FluxVMClear is called from VACUUM, not in a crit section). An error between MarkBufferDirty and PageSetLSN leaves a dirty VM page without a matching WAL record/LSN. Wrap in START_CRIT_SECTION()/END_CRIT_SECTION() to match heap's visibilitymap discipline.
📄 src/backend/access/flux/flux_stats.c (L50-L52)
This entire file is dead code: FluxCollectRelationStats() and FluxLogRelationStats() are declared in flux.h but have no callers anywhere in the tree (the header comment claims they are reached "during ANALYZE" and "made available through the FluxCollectRelationStats() interface so that the planner can incorporate FLUX-specific cost adjustments", but nothing calls them). Worse, the scan interprets FLUX_TUPLE_COMPRESSED / FluxCompressionHeader and FLUX_TUPLE_HAS_OVERFLOW / FluxIsOverflowRecordInline, while flux_tuple.c states FLUX never compresses and uses TOAST rather than on-page overflow, so this accounting reads structures the AM never produces. Per YAGNI/minimalism, drop the file (and its declarations) until an ANALYZE hook actually consumes it.
📄 src/backend/access/flux/flux_stats.c (L247-L251)
Non-portable 64-bit formatting: int64/uint64 are logged with "%lld"/"%llu" plus (long long)/(unsigned long long) casts. PostgreSQL requires INT64_FORMAT/UINT64_FORMAT for 64-bit values (the rest of the FLUX code already uses UINT64_FORMAT, e.g. flux_pbu.c). Also these diagnostic strings are not wrapped in _() and are emitted as several separate ereport() calls per relation, which is unusual for ANALYZE output.
📄 src/backend/access/flux/flux_tuple.c (L317-L321)
FluxFormTupleUpdate() has no callers anywhere in the tree, and flux_form_tuple_internal() ignores old_ovptrs/old_ovpresent entirely (the body comment states FLUX has no on-page overflow and does not compress). The doxygen comment above describes COW overflow reuse against the old tuple's on-page overflow pointers, which the implementation does not do. This is dead scaffolding with a misleading comment; remove the wrapper and its header declaration (or implement the documented behavior).
📄 src/backend/access/flux/flux_tuple.c (L352-L357)
The force_shrink parameter (and old_ovptrs/old_ovpresent) are unused: flux_form_tuple_internal() never reads force_shrink, so FluxFormTupleForceShrink() does not actually force any varlena off-page as its comment claims ("forces every inline varlena attribute larger than an overflow pointer off-page"). Since FLUX TOASTs wide values via the standard heap path and has no on-page overflow, work_values just mirrors values and has_compressed/has_overflow stay false. Either remove the dead force_shrink/old_ovptrs/old_ovpresent plumbing and the FluxFormTupleForceShrink wrapper, or fix the comments to describe what the code actually does.
📄 src/backend/access/flux/flux_tuple.c (L829-L832)
FluxPageUpdateTuple() has no callers in the tree. It also mutates a shared buffer page (memcpy over the slot, ItemIdSetNormal, FluxPageIndexTupleDelete + PageAddItem) with no WAL logging and no START/END_CRIT_SECTION, so it would be a crash/replica-consistency hazard if ever wired up. Remove it (and its flux.h declaration) rather than leave an unsafe, unused page-mutating helper for a future caller to misuse.
📄 src/backend/access/flux/flux_tuple.c (L900-L902)
FluxPageGetLiveTuples() has no callers (its own comment admits "only an estimate (no current callers)" and that snapshot_ts is unused). Dead code; remove it and its declaration per YAGNI.
📄 src/backend/access/flux/flux_xlog.c (L996-L1000)
FluxXLogCompress() emits XLOG_FLUX_COMPRESS, but no forward path in the FLUX AM ever calls it (code_search finds no emitter), and the AM comments state FLUX does not compress attributes. The matching redo handler flux_xlog_compress_redo() is therefore dead too, and the WAL format carries an unused opcode/subtype. Remove the unused COMPRESS WAL emit + redo path (and the XLOG_FLUX_COMPRESS opcode) unless an emitter is actually added; dead WAL opcodes are a long-term maintenance/format hazard.
📄 src/backend/access/flux/flux_xlog.c (L965-L968)
FluxXLogOverflowWrite() emits XLOG_FLUX_OVERFLOW_WRITE but has no caller (FLUX uses the standard heap TOAST path, not on-page overflow). The standalone overflow redo handler flux_xlog_overflow_write_redo() and the overflow-replay loops folded into INSERT/UPDATE redo are consequently exercising a WAL format no forward path produces. This dead overflow WAL machinery should be removed to keep the WAL format minimal and avoid future divergence between a non-existent write path and its redo.
📄 src/include/access/flux_xlog.h (L16-L16)
Remove #include "postgres.h". PostgreSQL headers must not include postgres.h; the including .c file does so first. This is especially wrong here because the header has a #else /* FRONTEND */ branch and is meant to be usable by frontend tools (pg_waldump via flux_desc/flux_identify), which include postgres_fe.h, not postgres.h. The analogous nbtxlog.h includes only access/transam.h, access/xlogreader.h, lib/stringinfo.h, storage/off.h. This breaks headerscheck and the FRONTEND separation.
📄 src/include/access/flux_xlog.h (L58-L60)
This comment is factually inverted and misleading. XLR_INFO_MASK is 0x0F -- it occupies the LOWER 4 bits, not the upper bits; and the opcodes below use the UPPER nibble (0x10..0xF0), not the lower nibble. The redo dispatcher confirms this: info = XLogRecGetInfo(record) & ~XLR_INFO_MASK. Also the comment header WAL record types for FLUX is duplicated immediately above. Fix the description to match the code.
💡 Suggested change
Before:
* Each opcode must be unique. The info byte uses bits 0-7 with
* XLR_INFO_MASK occupying the upper bits, so we have the lower
* nibble(s) available for opcodes.
After:
* Each opcode must be unique. XLR_INFO_MASK (0x0F) occupies the low 4
* bits of the info byte, so the upper nibble (0x10..0xF0) is available for
* opcodes. The redo dispatcher recovers the opcode with
* (XLogRecGetInfo(record) & ~XLR_INFO_MASK).
📄 src/include/access/flux.h (L675-L676)
%% is a printf-format escape and is wrong in a plain C comment -- it renders literally as 100%% / ~10%%. This indicates copy-paste from an elog() format string. Use a single %.
💡 Suggested change
Before:
* packing pages 100%% full (heap's default) guarantees that any in-place
* growth on a full page aborts. The default reserves ~10%% of each page,
After:
* packing pages 100% full (heap's default) guarantees that any in-place
* growth on a full page aborts. The default reserves ~10% of each page,
📄 src/include/access/flux_xlog.h (L131-L139)
This multi-line comment describes FLUX_VM_BLOCK_ID (the VM block id, "the highest one"), but it is detached from the macro it documents: it sits directly above FLUX_WAL_KEY_INTACT 0x0400 (an unrelated flag with its own inline comment), and #define FLUX_VM_BLOCK_ID XLR_MAX_BLOCK_ID appears only afterwards. Move this comment down to immediately precede the FLUX_VM_BLOCK_ID definition so it documents the right symbol.
💡 Suggested change
Before:
/*
* Block id of the visibility map page an INSERT, UPDATE or DELETE record
* clears bits on (with FLUX_WAL_ALL_VISIBLE_CLEARED). The highest one, so
* that it never takes the place of an overflow, destination or undo block,
* which follow block 0 without gaps.
*/
#define FLUX_WAL_KEY_INTACT 0x0400 /* SUPERSEDE did not change key columns */
#define FLUX_VM_BLOCK_ID XLR_MAX_BLOCK_ID
After:
#define FLUX_WAL_KEY_INTACT 0x0400 /* SUPERSEDE did not change key columns */
/*
* Block id of the visibility map page an INSERT, UPDATE or DELETE record
* clears bits on (with FLUX_WAL_ALL_VISIBLE_CLEARED). The highest one, so
* that it never takes the place of an overflow, destination or undo block,
* which follow block 0 without gaps.
*/
#define FLUX_VM_BLOCK_ID XLR_MAX_BLOCK_ID
📄 src/include/access/flux.h (L951-L951)
flux_lazy_uncommitted_clear is declared extern in this public header but is only defined and used inside flux_operations.c (never set anywhere -- always false, and not wired to a GUC). Either it is genuinely module-private (drop the extern from the header and make it static), or it is intended for cross-module/extension use, in which case it needs PGDLLIMPORT or it will fail to link on Windows/MSVC. As written it is an unused, unwired knob exposed for no reason.
📄 src/backend/access/flux/flux_slot.c (L227-L227)
Dead scaffolding: tuple_has_compressed is declared, assigned a constant false, immediately cast to void to silence the unused-variable warning, and never read. FLUX does not compress attributes, so this flag models a path that does not exist. Per the YAGNI/minimalism discipline, remove the variable (declaration, assignment, and the (void) cast) entirely; the comment "FLUX does not compress attributes" already documents the invariant. (high confidence)
📄 src/include/access/flux.h (L167-L172)
Stale, self-contradicting comment. This block claims "t_cid removed: command ID is now obtained from the sLog entry (FluxSLogEntry.cid)" and "This saves 4 bytes per tuple". Neither is true in this version: the struct has a dedicated t_cmin field (documented just above as REQUIRED FOR CORRECTNESS), and the code reads tuple->t_cmin directly (flux_mvcc.c:1134, flux_slot.c:412/485/552/1053) -- there is no FluxSLogEntry.cid lookup for the command id anywhere. The word was not saved; t_cmin occupies it. This comment describes a prior design and must be updated to reflect that the command id now lives in t_cmin.
📄 src/include/access/flux_pbu.h (L76-L77)
This comment documents FluxPbuBlockIsUndo ("True iff the given block reference ... is a per-backend UNDO page", returns bool), but it is detached from it: it sits directly above void FluxPbuResolveRecoveryConflict(...), which has no documentation of its own, while FluxPbuBlockIsUndo follows on the next line. Move the comment down to immediately precede FluxPbuBlockIsUndo, and add a separate description for FluxPbuResolveRecoveryConflict.
📄 src/include/access/flux_undo.h (L101-L101)
Misleading field name: pad is not padding. It is written and read as the byte length of the trailing index-list area (flux_pbu.c:268 hdr.pad = index_area;, :279 if (hdr.pad > 0), :286 appends hdr.pad bytes). A field named pad invites a future reader -- or a mask/normalization routine -- to treat it as ignorable and zero it, which would corrupt the on-disk/WAL undo payload length. Rename it to something that states its meaning (e.g. index_area_len); the comment already admits it is "index-list area length".
💡 Suggested change
Before:
uint16 pad; /* reserved index-list area length */
After:
uint16 index_area_len; /* bytes of trailing index-list area */
📄 src/include/access/flux.h (L353-L354)
FluxOverflowPtr is an on-disk format (stored inline in tuples and in WAL), and the surrounding comment makes a hard guarantee that it "stays 20 bytes" so FLUX_OVERFLOW_PTR_SIZE is unchanged. Unlike FluxTupleHeader and FluxUndoIndex, this struct has no StaticAssertDecl enforcing its size. Add one so an accidental field/padding change cannot silently alter the on-disk layout: StaticAssertDecl(sizeof(FluxOverflowPtr) == 20, "FluxOverflowPtr layout changed");.
💡 Suggested change
Before:
uint32 ov_content_hash; /* 32-bit content-hash prefilter for COW */
} FluxOverflowPtr;
After:
uint32 ov_content_hash; /* 32-bit content-hash prefilter for COW */
} FluxOverflowPtr;
StaticAssertDecl(sizeof(FluxOverflowPtr) == 20,
"FluxOverflowPtr on-disk layout changed");
📄 src/backend/access/flux/flux_vm.c (L72-L79)
FluxVMInit() is dead code: it has an empty body (the comment even says the work happens elsewhere) and no caller anywhere in the tree (only its forward declaration in flux.h references it). Per YAGNI, remove the function and its declaration in src/include/access/flux.h.
📄 src/backend/access/flux/flux_vm.c (L392-L395)
FluxVMExtend() has no callers in the tree; the VM fork is grown on demand via flux_vm_readbuf()/flux_vm_extend() from FluxVMSet/FluxVMPinForChange. Dead code; remove it and its declaration in flux.h.
📄 src/backend/access/flux/flux_vm.c (L413-L417)
FluxVMGetPageSize() is unused (no caller anywhere in the tree). Dead code; remove it and its declaration in flux.h.
📄 src/backend/access/flux/flux_vm.c (L422-L426)
FluxVMMapHeapToVM() is unused (no caller anywhere in the tree). Dead code; remove it and its declaration in flux.h.
📄 src/backend/access/flux/flux_vm.c (L370-L373)
FluxVMPinBuffer() is unused (no caller anywhere in the tree). Dead code; remove it and its declaration in flux.h.
📄 src/backend/access/flux/flux_xlog.c (L1152-L1155)
FluxXLogWriteDict() emits XLOG_FLUX_WRITE_DICT for a "compression-dictionary fork", but it has no caller: code_search finds XLOG_FLUX_WRITE_DICT emitted only here. FLUX does not compress attributes (per the AM comments throughout flux_tuple.c), so there is no dictionary fork to log. Consequently the redo handler flux_xlog_write_dict_redo(), the XLOG_FLUX_WRITE_DICT dispatch case, and the dict-page special-casing in flux_mask() are all dead, and the WAL format carries an unreachable opcode. Remove the dead dictionary-fork WAL path (emitter, redo handler, dispatch case, mask special-case, and the opcode/struct declarations) per YAGNI; this mirrors the FluxXLogCompress/FluxXLogOverflowWrite dead-WAL findings.
📄 src/backend/access/flux/flux_xlog.c (L2298-L2299)
The VM byte/offset arithmetic here open-codes the HEAPBLK_TO_MAPBYTE/HEAPBLK_TO_OFFSET macros from flux_vm.c with a hand-inlined (BLCKSZ - MAXALIGN(SizeOfPageHeaderData)) * 4 (duplicated again in flux_xlog_vm_clear_redo). If the VM page layout macros in flux_vm.c ever change, these two redo sites will silently diverge and corrupt the VM on replay. Expose the mapping macros (e.g. in flux.h) and use them here instead of re-deriving the constant, so the set/clear forward and redo paths stay in lockstep.
📄 src/backend/access/flux/flux_undo.c (L401-L405)
UndoRecPtr is a uint64; logging it with "%llu" + (unsigned long long) cast deviates from the PostgreSQL convention of UINT64_FORMAT, which the rest of the FLUX code already uses (e.g. flux_pbu.c). This pattern recurs throughout flux_undo.c (apply, index-first, delta, desc paths). Switch to UINT64_FORMAT for consistency and to avoid relying on the cast matching the format specifier.
📄 src/backend/access/undo/undo_xlog.c (L157-L160)
Redo of UNDO_CLR_HAS_TUPLE on the BLK_NEEDS_REDO path lacks the bounds check the forward-apply path enforces. flux_undo.c:apply_flux_undo_restore_tuple() refuses the restore when ItemIdGetLength(lp) < old_len; here redo only checks ItemIdIsUsed/ItemIdHasStorage, then unconditionally does ItemIdSetNormal(lp, ..., tuple_len) and memcpy(htup, data, tuple_len). If the on-disk line pointer's storage is smaller than xlrec->tuple_len, the memcpy overruns the item into adjacent page data. The Assert(datalen >= tuple_len) only guards the WAL payload size and is compiled out in production. Mirror the forward path's guard before writing: verify ItemIdGetLength(lp) >= xlrec->tuple_len (bail out / report corruption otherwise) rather than trusting tuple_len.
📄 src/backend/access/undo/atm.c (L459-L461)
The pg_undo_atm checkpoint file reuses WAIT_EVENT_TWOPHASE_FILE_WRITE/SYNC/READ. These waits will show up in pg_stat_activity / pg_wait_events mislabeled as two-phase-commit file I/O, which is a monitoring-accuracy defect. Add dedicated ATM wait events via src/backend/utils/activity/wait_event_names.txt (e.g. WAIT_EVENT_UNDO_ATM_FILE_WRITE/SYNC/READ) and use them here (same at lines 511 and 573).
📄 src/backend/access/undo/undo.c (L60-L63)
These per-backend engine entry points are re-declared locally instead of being pulled from their owning headers (pbu_undolog.h, pbu_undorequest.h, pbu_undoworker.h), which already declare them. Duplicating the prototypes defeats the compiler's cross-translation-unit signature checking: if a definition's signature changes, this copy will silently diverge and only fail at link time (or, worse, mismatch undetected). Note PendingUndoShmemSize() is declared 'int' here and in its header, yet UndoShmemSize() feeds it to add_size() which takes Size; the shmem sizing API should be Size throughout. Prefer resolving the cited header type-collision so the real headers can be included, rather than hand-maintaining these externs.
📄 src/backend/access/undo/undoinsert.c (L56-L60)
These are no-op shims whose comments say they are kept for callers that "haven't been updated yet" (future-tense/aspirational). They still have live callers in xactundo.c (UndoWalBatchFlush at 4 sites, UndoWalBatchReset at 1), so this is dead-weight scaffolding threaded through the abort/commit path. Per the minimalism discipline, either finish the migration and delete these functions plus their call sites, or drop the shims entirely. At minimum the comments must describe current behavior, not a pending-update intent.
📄 src/backend/access/undo/README (L728-L728)
Performance claims ("Abort latency increases approximately 20-50%", and "Checkpoint time increases 7-15% ... Recovery time increases 10-20%" below) are stated without a reproducible benchmark. Project discipline requires performance numbers to cite a benchmark/thread, or be dropped. Either back these with a reproducible measurement reference or remove the specific percentages.
📄 src/backend/access/undo/README (L656-L656)
This parenthetical describes the per-backend UNDO logs as "added in a later patch", but the per-backend engine (access/undo/perbackend/, listed in this directory's Makefile and documented earlier in this same README) is present in this changeset. The future-tense note is stale/misleading; reword to describe the current behavior.
📄 src/backend/access/undo/xactundo.c (L1122-L1123)
Memory-context leak and wrong CurrentMemoryContext on the goto path. goto inline_undo_done; jumps past MemoryContextSwitchTo(old_ctx); MemoryContextDelete(undo_ctx); (lines 1174-1175), so when UndoValidateBatchLSN() fails: (1) undo_ctx (a child of TopMemoryContext) is never deleted and leaks for the life of the backend, accumulating on every abort that hits an invalid batch LSN; (2) CurrentMemoryContext stays set to the leaked undo_ctx through the subsequent ATMAddAborted() call, the record-set free loop, and ResetXactUndo(). Restore the context and delete undo_ctx before (or at) the label. Confidence: high.
💡 Suggested change
Before:
undo_applied = false;
goto inline_undo_done;
After:
undo_applied = false;
MemoryContextSwitchTo(old_ctx);
MemoryContextDelete(undo_ctx);
goto inline_undo_done;
📄 src/backend/access/undo/undostats.c (L102-L104)
Memory leak: SLogTxnSnapshotForCheckpoint() returns a palloc'd array that the caller owns and must pfree (see its header comment and the matching pfree(entries) in atm.c's CheckPointATM). pg_stat_get_undo_retention() never frees entries. It leaks into the SRF's call context on every invocation. Add a pfree(entries) after the loop (guarding for n==0 where entries is NULL).
💡 Suggested change
Before:
n = SLogTxnSnapshotForCheckpoint(&entries);
for (int i = 0; i < n; i++)
{
After:
n = SLogTxnSnapshotForCheckpoint(&entries);
for (int i = 0; i < n; i++)
{
📄 src/backend/access/undo/undoworker.c (L127-L130)
Non-async-signal-safe write and data race. shutdown_requested is declared as plain bool in shared memory (undoworker.h) yet is written here directly from a signal handler, and is also written under LWLock in UndoWorkerRequestShutdown() and read without any synchronization in the main loop. A signal handler must only touch a volatile sig_atomic_t flag. Use the standard pattern: a local static volatile sig_atomic_t flag set by the handler (or reuse SignalHandlerForShutdownRequest / ShutdownRequestPending), and drop the plain-bool shared field for signal purposes. Also note this handler dereferences the global UndoWorkerShmem pointer, which is itself unsafe in a signal handler. Confidence: high.
📄 src/backend/access/undo/undoworker.c (L282-L284)
Non-portable 64-bit format. retained_mb is uint64 but is printed with %lu after a cast to (unsigned long). On LLP64 platforms (64-bit Windows) unsigned long is 32 bits, truncating the value. Use UINT64_FORMAT with the uint64 directly.
💡 Suggested change
Before:
ereport(WARNING,
(errmsg("UNDO WAL retention (%lu MB) exceeds undo_max_wal_retention_size (%d MB)",
(unsigned long) retained_mb, undo_max_wal_retention_size),
After:
ereport(WARNING,
(errmsg("UNDO WAL retention (" UINT64_FORMAT " MB) exceeds undo_max_wal_retention_size (%d MB)",
retained_mb, undo_max_wal_retention_size),
📄 src/backend/access/undo/undoworker.c (L331-L332)
worker_proc is a plain ProcNumber written here as MyProcNumber and read by arbitrary backends in WakeUndoDiscardWorker() with no barrier. A concurrent reader may observe a stale/invalid ProcNumber and SetLatch on the wrong PGPROC. This mirrors the WAL-writer pattern which uses a lock or at least documents the single-writer invariant; consider pg_atomic_* or a write barrier for this cross-backend handoff. Confidence: medium.
📄 src/backend/access/undo/undolog.c (L170-L186)
Dead scaffolding. With the WAL-integrated design no code ever sets UndoLogControl.in_use = true (the only in_use = true in the tree is in the unrelated pbu_undoworker.c), so the entire UndoLogShared->logs[] machinery is inert: UndoLogDiscard(), UndoLogGetOldestDiscardPtr(), UndoLogGetInsertPtr(), UndoLogGetDiscardPtr() have no callers, and CheckPointUndoLog()'s loop over logs[] never executes a non-skipped iteration. This is exactly the speculative/dead code the project asks authors to remove. Also note UndoLogGetOldestDiscardPtr() compares full UndoRecPtr values (log->discard_ptr < oldest) across logs while UndoLogDiscard() compares via UndoRecPtrGetOffset(): since the log number lives in bits 40-63 (undolog.h), a raw < across different logs orders by log number, not age -- a latent correctness bug were this ever wired up. Recommend deleting the unused logs[]-based functions and fields rather than leaving them.
📄 src/backend/access/undo/undostats.c (L97-L99)
proc->xid and proc->databaseId are read from a shared PGPROC without ProcArrayLock. Between the atomic read of backend_undo_lsns[i] and dereferencing GetPGProcByNumber(i), the backend may exit and the ProcNumber slot be reused by another backend, so the reported pid/xid/database may be stale or belong to an unrelated session. For a monitoring SRF this is an accuracy issue rather than corruption, but it is worth either holding ProcArrayLock LW_SHARED around the proc field reads or documenting the best-effort nature. Confidence: medium.
📄 src/backend/access/undo/logical_revert_worker.c (L514-L521)
This cluster-wide PANIC test is wrong and will fire in the common case. GetRedoRecPtr() returns the redo pointer of the last completed checkpoint, not the oldest retained WAL segment. WAL is retained back to UndoGetOldestBatchLSN() (via KeepLogSeg), which is typically far older than the latest checkpoint redo pointer. Any aborted transaction whose UNDO batch predates the last checkpoint — the normal situation this worker exists to handle — satisfies last_batch_lsn < redo while its WAL is still perfectly retained. So any transient error in the PG_TRY block (lock conflict, OOM, an injection-point error, etc.) will PANIC the entire cluster instead of retrying.
Use the same recycled-WAL test the UNDO_CHAIN_INCOMPLETE path already uses: compare the segment against XLogGetLastRemovedSegno(), which actually tracks removed/recycled segments.
💡 Suggested change
Before:
XLogRecPtr redo = GetRedoRecPtr();
if (XLogRecPtrIsValid(last_batch_lsn) &&
last_batch_lsn < redo)
{
elog(PANIC, "logical revert worker: UNDO WAL for "
"un-reverted xid %u at %X/%X was recycled "
"(redo at %X/%X); retention invariant violated "
After:
XLogSegNo seg;
XLByteToSeg(last_batch_lsn, seg, wal_segment_size);
if (XLogRecPtrIsValid(last_batch_lsn) &&
seg <= XLogGetLastRemovedSegno())
{
elog(PANIC, "logical revert worker: UNDO WAL for "
"un-reverted xid %u at %X/%X was recycled; "
"retention invariant violated "
"(ATMGetOldestUnrevertedLSN -> "
"UndoGetOldestBatchLSN -> KeepLogSeg must pin "
"this WAL until ATMForget) -- guaranteed "
"rollback can no longer be honored",
xid, LSN_FORMAT_ARGS(last_batch_lsn));
}
📄 src/backend/access/undo/logical_revert_worker.c (L547-L555)
Dead code: the latch is waited with WL_EXIT_ON_PM_DEATH, which makes WaitLatch exit the process internally on postmaster death and never return the WL_POSTMASTER_DEATH bit. So rc & WL_POSTMASTER_DEATH is always false and proc_exit(1) is unreachable. Either drop the manual check (rely on WL_EXIT_ON_PM_DEATH) or switch the wait set to WL_POSTMASTER_DEATH to make the check meaningful. The launcher uses WL_POSTMASTER_DEATH + manual proc_exit; pick one convention.
📄 src/backend/access/undo/logical_revert_worker.c (L590-L599)
Registration failure is swallowed and the caller cannot tell. The launcher appends dboid to next_known unconditionally after calling this, so a database whose worker failed to register (e.g. no free bgworker slots) is permanently treated as "has a live worker" and is never retried on later scans. Its aborted-transaction UNDO is then never applied, which pins WAL indefinitely via the retention chain. Return the result of RegisterDynamicBackgroundWorker and have the launcher only track on success. Also, handle is obtained but never used.
📄 src/backend/access/undo/logical_revert_worker.c (L582-L583)
Use snprintf/strlcpy into these fixed-size bgw_* buffers, not unbounded sprintf. The strings are constant so there is no actual overflow today, but it violates the project convention (bounded string ops only) and is inconsistent with the snprintf used for bgw_name/bgw_type just below and in LogicalRevertLauncherRegister.
💡 Suggested change
Before:
sprintf(worker.bgw_library_name, "postgres");
sprintf(worker.bgw_function_name, "LogicalRevertWorkerMain");
After:
snprintf(worker.bgw_library_name, BGW_MAXLEN, "postgres");
snprintf(worker.bgw_function_name, BGW_MAXLEN, "LogicalRevertWorkerMain");
📄 src/backend/access/undo/logical_revert_worker.c (L321-L324)
User-facing messages that count objects should use errmsg_plural() so singular/plural is correct and localizable; "%d UNDO record(s)" and "%d batch(es)" are the parenthesized workaround the convention forbids. This applies to the "will retry", "giving up", and errdetail messages in this function.
📄 src/backend/access/undo/logical_revert_worker.c (L300-L302)
Minor ordering nit: XLByteToSeg is computed on res.unread_batch before the XLogRecPtrIsValid(res.unread_batch) check. The macro has no side effects and the validity check still guards the PANIC, so this is not a live bug (UndoWalkChain always sets a valid unread_batch for UNDO_CHAIN_INCOMPLETE), but moving the segno computation inside the validity-checked block reads more clearly and matches the invariant the comment relies on.
📄 src/include/access/logical_revert_worker.h (L25-L25)
PostgreSQL headers must never include postgres.h; it is included first by each .c file (and frontend code uses postgres_fe.h/c.h instead). Including it here breaks the convention and any frontend/extension translation unit that includes this header. Remove it — the only types this header needs (Datum, Oid, Size) are already available from the includer's postgres.h/c.h. (The headerscheck tooling will flag this.)
📄 src/include/access/logical_revert_worker.h (L7-L10)
Comment drifted from the implementation. The worker does not "open the target relation" and does not apply "via the per-AM apply callback"; logical_revert_worker.c applies the chain through the WAL-based ApplyUndoChain() and never opens a relation. Update this description to match the WAL-based apply path to avoid misleading readers.
📄 src/backend/access/undo/undoapply.c (L326-L331)
Bounds-check gap in the corruption-defense layer. The first pass validates urec_len fits within the batch, but urec_payload_len is never validated against urec_len. In the second pass the payload is delimited solely by header.urec_payload_len (payload = record_starts[i] + SizeOfUndoRecordHeader, length = header.urec_payload_len) and handed to the RM callback. For a recycled/overwritten or otherwise corrupt WAL batch -- exactly the case this routine is written to tolerate (see the truncated/invalid-size checks right above) -- a bogus urec_payload_len that exceeds urec_len - SizeOfUndoRecordHeader makes the RM callback read past the record body and potentially past the batch buffer (SIGSEGV/SIGBUS). Validate payload length against the record length during the forward scan.
💡 Suggested change
Before:
if (hdr.urec_len < SizeOfUndoRecordHeader ||
(Size) (end - pos) < hdr.urec_len)
{
ereport(WARNING,
(errmsg("UNDO rollback: invalid record size %u in batch at %X/%X",
hdr.urec_len, LSN_FORMAT_ARGS(batch_lsn))));
After:
if (hdr.urec_len < SizeOfUndoRecordHeader ||
(Size) (end - pos) < hdr.urec_len ||
hdr.urec_payload_len > hdr.urec_len - SizeOfUndoRecordHeader)
{
ereport(WARNING,
(errmsg("UNDO rollback: invalid record size %u in batch at %X/%X",
hdr.urec_len, LSN_FORMAT_ARGS(batch_lsn))));
📄 src/backend/access/undo/undo_bufmgr.c (L235-L237)
Footgun: last_block is accepted but ignored. DropRelationBuffers() drops every block >= first_block regardless of last_block, so a caller passing a bounded [first_block, last_block] range will have blocks beyond last_block silently invalidated too. The comment admits this. If/when this module is kept, either implement the upper bound or remove the misleading parameter so the signature cannot be misused.
📄 src/backend/access/undo/undorecord.c (L308-L324)
Portability: unaligned write hazard. Records are packed back-to-back with uset->buffer_size += record_size where record_size = SizeOfUndoRecordHeader + payload_len, and payload_len is caller-supplied and frequently odd (e.g. nbtree_undo_write passes a variable-length IndexTuple). The second and subsequent records therefore start at a non-8-aligned offset, so header = (UndoRecordHeader *) dest; header->urec_xid = ...; header->urec_prev = ... performs unaligned 32/64-bit stores. This SIGBUSes on strict-alignment platforms (PostgreSQL's portability gate forbids unaligned access). The read side is safe because undoapply.c uses memcpy into an aligned local, but this write side casts directly. Either MAXALIGN each record's size when advancing buffer_size (and in the reader's pos += hdr.urec_len scan), or build the header in an aligned local and memcpy it in.
📄 src/backend/access/undo/undorecord.c (L360-L365)
Same unaligned write hazard as in UndoRecordAddPayload: header points into the record buffer at uset->buffer + uset->buffer_size, which is not guaranteed to be aligned once a prior record had an odd-length payload. Writing header->urec_len/urec_xid/urec_prev through this pointer is an unaligned 32/64-bit store and will SIGBUS on strict-alignment architectures. Fix together with the other occurrence by MAXALIGN-padding records (write and read sides) or assembling the header in an aligned local and memcpy'ing it.
📄 src/backend/access/undo/undoworker.c (L455-L457)
Dead code. WaitLatch is called with WL_EXIT_ON_PM_DEATH, not WL_POSTMASTER_DEATH; these are distinct flags. With WL_EXIT_ON_PM_DEATH the latch infrastructure calls proc_exit() internally on postmaster death and never returns with WL_POSTMASTER_DEATH set, so rc & WL_POSTMASTER_DEATH is always false and this branch is unreachable. Either drop the check, or switch the wait flag to WL_POSTMASTER_DEATH and keep the explicit proc_exit(1). As written it misleads readers into thinking PM-death is handled here.
📄 src/backend/access/undo/logical_revert_worker.c (L11-L11)
Comment drift (two inaccuracies):
-
Step 1 says the chain is applied via
ApplyUndoChainFromWAL(), butprocess_revert_entry()actually callsApplyUndoChain()(the variant that reports UNDO_CHAIN_DONE / INCOMPLETE / RECORD_ERROR / MALFORMED so the worker can distinguish retryable from fatal).ApplyUndoChainFromWAL()is never called here. -
The "Shared memory" paragraph claims the LWLock protects "the running flag and a counter".
LogicalRevertStatehas no running flag -- onlylockandnext_worker_id. The lock guards only the worker-id counter.
Comments must describe what the code does now; please correct both.
💡 Suggested change
Before:
* 1. Applies the WAL-based UNDO chain via ApplyUndoChainFromWAL()
After:
* 1. Applies the WAL-based UNDO chain via ApplyUndoChain()
📄 src/backend/access/undo/logical_revert_worker.c (L87-L91)
YAGNI: this entire shared-memory apparatus -- the LogicalRevertState struct, the LWTRANCHE_UNDO_WORKER LWLock, and LogicalRevertShmemSize()/LogicalRevertShmemInit() wired into undo.c -- exists solely to hand out next_worker_id, which is used only to print a worker number in two elog(LOG) lines ("worker %d started" / "worker %d shutting down"). That is a heavyweight mechanism for a cosmetic, monotonically-growing log counter. The per-db worker already identifies itself by its database OID in bgw_name and the log lines, so the ID adds nothing operationally. Consider dropping the shared state entirely (and the ShmemSize/Init calls) and logging dboid instead.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1112-L1112)
memset only clears the first element of each queue array, not the whole array. UndoXidQueueElems points to ROLLBACK_REQUEST_QUEUE_SIZE (1024) UndoXidQueue structs (UndoXidQueueElemsShmSize() = 1024 * sizeof(UndoXidQueue)), but sizeof(UndoXidQueue) zeroes only element [0]. The remaining 1023 elements are left in whatever state ShmemAllocRaw() returned (it does NOT zero its allocations). UndoXidQueueGetFreeElem() relies on full_xid == InvalidFullTransactionId (which is 0) to find free slots; uninitialized shmem is not guaranteed to be zero, so slot allocation can be corrupted and PushXidQueueElem() may overwrite a live element. Same defect for the Size and Error arrays. Use the ShmSize helpers (which already multiply by ROLLBACK_REQUEST_QUEUE_SIZE).
💡 Suggested change
Before:
memset(UndoXidQueueElems, 0, sizeof(UndoXidQueue));
After:
memset(UndoXidQueueElems, 0, UndoXidQueueElemsShmSize());
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1116-L1116)
Same off-by-array-length memset defect: this zeroes only the first UndoSizeQueue element, leaving the other 1023 shmem elements uninitialized. Use UndoSizeQueueElemsShmSize().
💡 Suggested change
Before:
memset(UndoSizeQueueElems, 0, sizeof(UndoSizeQueue));
After:
memset(UndoSizeQueueElems, 0, UndoSizeQueueElemsShmSize());
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1120-L1120)
Same off-by-array-length memset defect for the error queue: only element [0] is zeroed. Use UndoErrorQueueElemsShmSize().
💡 Suggested change
Before:
memset(UndoErrorQueueElems, 0, sizeof(UndoErrorQueue));
After:
memset(UndoErrorQueueElems, 0, UndoErrorQueueElemsShmSize());
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L705-L709)
Inconsistent cur_undo_queue side effect. Only the ERROR_QUEUE branch bumps cur_undo_queue when the queue is empty; the XID_QUEUE and SIZE_QUEUE branches do not. In the UndoGetWork() round-robin loop the caller already does cur_undo_queue++ after this returns false (see the if (!GetRollbackHashKeyFromQueue(...)) { cur_undo_queue++; continue; } block), so for the empty-error-queue case cur_undo_queue is incremented twice, skewing round-robin fairness and skipping a queue on the next call. This function is documented as a pure accessor ("Returns the hash key corresponding to the nth element") and should not mutate module state. Drop the stray increment.
💡 Suggested change
Before:
if (GetErrorQueueSize() <= n)
{
cur_undo_queue++;
return false;
}
After:
if (GetErrorQueueSize() <= n)
return false;
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L9-L9)
Wrong IDENTIFICATION path in the file header. This file lives at src/backend/access/undo/perbackend/pbu_undorequest.c, not src/backend/access/undo/undorequest.c. (The sibling pbu_helpers.c has the correct path and uses an explicit IDENTIFICATION label.)
💡 Suggested change
Before:
* src/backend/access/undo/undorequest.c
After:
* IDENTIFICATION
* src/backend/access/undo/perbackend/pbu_undorequest.c
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L318-L322)
These helpers return int but mul_size() returns Size (size_t), and their results feed Size-typed expressions (add_size(), the .size field, and the memset fix above). For 1024 elements this fits, but truncating Size to int is inconsistent with PendingUndoShmemSize() (which uses Size) and is a portability/overflow-hygiene wart. Return Size.
💡 Suggested change
Before:
static int
UndoXidQueueElemsShmSize(void)
{
return mul_size(ROLLBACK_REQUEST_QUEUE_SIZE, sizeof(UndoXidQueue));
}
After:
static Size
UndoXidQueueElemsShmSize(void)
{
return mul_size(ROLLBACK_REQUEST_QUEUE_SIZE, sizeof(UndoXidQueue));
}
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L770-L773)
Crash-window leak: UndoGetWork(false, true, ...) removes the request from its queue AND marks the hash entry in_progress=true, but this runs BEFORE UndoWorkerAttach() registers before_shmem_exit(UndoWorkerOnExit). If this worker dies between here and attach, UndoWorkerOnExit never runs; and even if it does, UndoWorkerCleanup() only resets the worker slot, not the hash entry. The in_progress entry is then owned by nobody: no backend/worker will retry it (the error-queue retry path only runs via UndoWorkerPerformRequest after attach) and the discard worker will skip it forever (in_progress), so the transaction's undo can never be discarded. Consider attaching (so cleanup is registered) before claiming/removing work, or have the exit cleanup re-queue/clear the in_progress request it was holding.
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L64-L67)
These are labeled "GUC parameters" but are plain file-scope statics with hardcoded values (5 and 10000); there is no GUC registration in guc_tables.c binding to them (unlike undo_rollback_overflow_size in pbu_undorequest.c, which is bound to a real GUC). As written, max_undo_workers is a silent fixed cap that gates UndoLauncherRegister(), IsUndoWorkerAvailable() and WakeupUndoWorker(), and undo_worker_quantum_ms is a fixed constant. Either wire them to GUCs or fix the misleading comment and treat them as compile-time constants (#define).
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L10-L10)
Wrong IDENTIFICATION path: this file is src/backend/access/undo/perbackend/pbu_undoworker.c, not src/backend/postmaster/undoworker.c.
💡 Suggested change
Before:
* src/backend/postmaster/undoworker.c
After:
* src/backend/access/undo/perbackend/pbu_undoworker.c
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L439-L445)
Stale zheap-era comment: this is the undo worker's on-exit callback, not a logical replication worker. Fix the comment to describe what the function actually does.
💡 Suggested change
Before:
/*
* Cleanup function.
*
* Called on logical replication worker exit.
*/
static void
UndoWorkerOnExit(int code, Datum arg)
After:
/*
* Cleanup function.
*
* Called on undo apply worker exit.
*/
static void
UndoWorkerOnExit(int code, Datum arg)
📄 src/include/access/perbackend/pbu_compat.h (L149-L151)
These three tranche IDs are dead code and the comment is factually wrong. No .c file references LWTRANCHE_UNDOLOG/LWTRANCHE_UNDODISCARD/LWTRANCHE_DISCARD_UPDATE, and there is no LWLockRegisterTranche() call anywhere in the pbu engine. The real locks are initialized with LWTRANCHE_UNDO_LOG_PBU (see pbu_undolog.c:267-269,1903-1905), which is registered properly via PG_LWLOCKTRANCHE(UNDO_LOG_PBU, UndoLogPBU) in lwlocklist.h. Remove these unused macros and the inaccurate "Registered dynamically via LWLockRegisterTranche()" comment.
📄 src/include/access/perbackend/pbu_compat.h (L26-L27)
This shim redefines extremely generic identifiers (RelFileNode, ShmemVariableCache, GetOldestXmin, RBM_ZERO, target_prefetch_pages, plus the LWLock names below) project-wide for any translation unit that includes it. Today containment holds (only pbu/flux .c files include this header, verified), but there is nothing structural preventing a future pbu header from being pulled into shared code and silently rewriting these names cluster-wide. At minimum, add a prominent warning that this header must only ever be included from pbu/flux .c files and never from a header that core code includes.
📄 src/include/access/perbackend/pbu_undolog.h (L89-L90)
UndoRecPtrFormat expands to "%016" INT64_MODIFIER "X", but INT64_MODIFIER is no longer defined anywhere in the tree except the fallback in pbu_compat.h. This header does not include pbu_compat.h, so any TU that includes pbu_undolog.h and uses UndoRecPtrFormat without also including pbu_compat.h fails to compile. This is latent include-order fragility. Prefer defining the format in terms of the always-available UINT64_FORMAT family (as UndoLogOffsetFormat already does) rather than relying on a compat-only INT64_MODIFIER fallback.
📄 src/include/access/perbackend/pbu_undorecord.h (L23-L34)
The undorectype enum is vendored dead scaffolding. No pbu .c file ever assigns any of these constants to uur_type/urec_type; the type field is set from subsystem-specific subtypes instead (FLUX_UNDO_*, NBTREE_UNDO_INSERT_LEAF, HASH_UNDO_INSERT). Per the minimalism rule, unused enum members should be removed.
📄 src/include/access/perbackend/pbu_undorecord.h (L89-L90)
UREC_INFO_PAYLOAD_CONTAINS_SLOT and UREC_INFO_PAYLOAD_CONTAINS_SUBXACT are never referenced by any pbu .c file (only RELATION_DETAILS/BLOCK/PAYLOAD/TRANSACTION are used). These are unused vendored flags; drop them.
📄 src/include/access/perbackend/pbu_undoaction_xlog.h (L3-L4)
Stale header block: the title says "undoaction_xlog.h" and the internal path comment says "src/include/access/undoaction_xlog.h", but the actual file is src/include/access/perbackend/pbu_undoaction_xlog.h. Fix the title and path to match the real filename.
📄 src/include/access/perbackend/pbu_undodiscard.h (L3-L4)
Wrong header block copied from undoinsert.h: title says "undoinsert.h" and the path comment says "src/include/access/undodiscard.h". The actual file is src/include/access/perbackend/pbu_undodiscard.h. Fix both.
📄 src/include/access/perbackend/pbu_undoinsert.h (L3-L4)
Stale path header: internal path comment says "src/include/access/undoinsert.h" but the file is src/include/access/perbackend/pbu_undoinsert.h. Correct it.
📄 src/include/access/perbackend/pbu_undolog.h (L12-L12)
Stale path header: internal path comment says "src/include/access/undolog.h" but the file is src/include/access/perbackend/pbu_undolog.h. Correct it.
📄 src/include/access/perbackend/pbu_undolog_xlog.h (L9-L9)
Stale path header: title/path say "undolog_xlog.h" / "src/include/access/undolog_xlog.h" but the file is src/include/access/perbackend/pbu_undolog_xlog.h. Correct both.
📄 src/include/access/perbackend/pbu_undorecord.h (L9-L9)
Stale path header: title/path say "undorecord.h" / "src/include/access/undorecord.h" but the file is src/include/access/perbackend/pbu_undorecord.h. Correct both.
📄 src/backend/access/undo/perbackend/pbu_undoinsert.c (L554-L558)
Buffer-overflow / wrong-variable Assert. This loop walks idx_undo_buffers[i] and increments i, but the bound check asserts idx (the xact_urec_info slot selector, a function parameter), not i. idx never changes in the loop, so the Assert cannot catch an overrun of the fixed-size idx_undo_buffers[MAX_BUFFER_PER_UNDO] (MAX_BUFFER_PER_UNDO == 3). A transaction header spanning more than MAX_BUFFER_PER_UNDO buffers reads/writes past the array (stack/shmem corruption). This is the known zheap bug; the guard must be on i, and ideally placed before the index use rather than after i++.
💡 Suggested change
Before:
starting_byte = UndoLogBlockHeaderSize;
i++;
Assert(idx < MAX_BUFFER_PER_UNDO);
} while (true);
After:
starting_byte = UndoLogBlockHeaderSize;
i++;
Assert(i < MAX_BUFFER_PER_UNDO);
} while (true);
📄 src/backend/access/undo/perbackend/pbu_undofile.c (L493-L496)
File validity test convention. PathNameOpenFile() returns a File that is -1 on failure and >= 1 on success (VfdCache[0] is a reserved list header, never handed out). Treating file <= 0 as failure works by accident but deviates from the in-tree convention (md.c uses file < 0). More importantly, user-facing file-open errors should use ereport(errcode_for_file_access(), errmsg(...)) rather than plain elog(ERROR, "...%m"); the same applies to the other file-IO error sites in this file.
💡 Suggested change
Before:
file = PathNameOpenFile(path, O_RDWR | PG_BINARY);
if (file <= 0 && (!missing_ok || errno != ENOENT))
elog(ERROR, "cannot open undo segment file \"%s\": %m", path);
After:
file = PathNameOpenFile(path, O_RDWR | PG_BINARY);
if (file < 0 && (!missing_ok || errno != ENOENT))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not open undo segment file \"%s\": %m", path)));
📄 src/backend/access/undo/perbackend/pbu_undofile.c (L71-L71)
The backend-local MRU caches an open File keyed by (logno, spcNode, segno) for the backend's lifetime, but no code path invalidates it when PbuUndoLogDiscard() unlinks/recycles the underlying segment (undofile_forget_sync only forgets the checkpointer sync request). In the backend that performs the discard, a later read of a since-unlinked (logno, segno) would hit the stale MRU and read the deleted inode's contents instead of reopening. The PbuUndoLogDiscard() contract ("no attempts to access the discarded range") mitigates this, but relying on an unenforced caller contract across a backend-local cache is fragile; consider invalidating undofile_mru on discard/unlink of a matching segment.
📄 src/backend/access/undo/perbackend/pbu_helpers.c (L38-L43)
GetEpochForXid() reinvents an existing in-tree helper. The epoch-derivation here (read nextFullXid, decrement epoch when xid > low32(nextXid)) is byte-for-byte the logic of FullTransactionIdFromAllowableAt(nextFullXid, xid) in access/transam.h, which also returns the full xid directly. Prefer calling that helper (DRY) instead of duplicating the wraparound reasoning; it also carries the oldestXid-vs-SetTransactionIdLimit caveat callers should heed.
📄 src/backend/access/undo/perbackend/pbu_discardworker.c (L9-L10)
IDENTIFICATION path is wrong: this is src/backend/access/undo/perbackend/pbu_discardworker.c, not "src/backend/postmaster/discardworker.c". Fix the header path (and normalize the stale "1996-2019" copyright) so it matches the real location.
💡 Suggested change
Before:
* IDENTIFICATION
* src/backend/postmaster/discardworker.c
After:
* IDENTIFICATION
* src/backend/access/undo/perbackend/pbu_discardworker.c
📄 src/backend/access/undo/perbackend/pbu_undoaction.c (L9-L9)
IDENTIFICATION path is stale zheap path: this file is src/backend/access/undo/perbackend/pbu_undoaction.c, not "src/backend/access/undo/undoaction.c". Also normalize the stale "1996-2019" copyright. Same stale-path issue applies to pbu_undoactionxlog.c, pbu_undodiscard.c, pbu_undoinsert.c, pbu_undolog.c and pbu_undorecord.c.
💡 Suggested change
Before:
* src/backend/access/undo/undoaction.c
After:
* src/backend/access/undo/perbackend/pbu_undoaction.c
📄 src/backend/access/undo/perbackend/pbu_undolog.c (L743-L745)
Aspirational/speculative comment that violates the "comments describe the code as it is now" rule. The long musing ("Not sure how we actually avoid waking up the undo work though...", the discard_last optimization that isn't implemented) is zheap-era design chatter, not an explanation of the current code. Trim it to what UndoLogAllocate() actually does.
📄 src/backend/access/undo/perbackend/pbu_undodiscard.c (L350-L351)
Speculative future-tense comment. "In future if multiple worker can perform discard then we may need to use compare and swap" describes a path that isn't wired up; pgsql-hackers rejects aspirational TODO-style comments. Either implement it or drop the note.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1319-L1321)
A full transaction (StartTransactionCommand/dbid_exists/CommitTransactionCommand) is run while RollbackRequestLock is held in LW_EXCLUSIVE mode (acquired at the top of UndoGetWork and only released at line 1339/1457). dbid_exists() does a syscache lookup, and transaction start/commit acquire other LWLocks and run arbitrary cleanup. Holding an LWLock across a transaction boundary and catalog access violates the LWLock contract (short, non-blocking critical sections, fixed lock ordering) and is a deadlock hazard. Perform the dbid existence check before taking RollbackRequestLock, or release the lock around the transaction and re-validate the hash entry afterward.
📄 src/backend/access/undo/perbackend/pbu_undorequest.c (L1701-L1704)
RollbackHTCleanup() mutates the shared hash table via hash_search(..., HASH_REMOVE, ...) while holding RollbackRequestLock only in LW_SHARED mode. A shared lock permits concurrent readers; a concurrent UndoGetWork()/RegisterRollbackReq() holding the same lock (SHARED or EXCLUSIVE) can read/modify RollbackHT at the same time, corrupting the dynahash structure. The lock must be LW_EXCLUSIVE for any removal.
💡 Suggested change
Before:
/* Fetch the rollback requests */
LWLockAcquire(RollbackRequestLock, LW_SHARED);
Assert(hash_get_num_entries(RollbackHT) <= UndoRollbackHashTableSize());
After:
/* Fetch the rollback requests */
LWLockAcquire(RollbackRequestLock, LW_EXCLUSIVE);
Assert(hash_get_num_entries(RollbackHT) <= UndoRollbackHashTableSize());
📄 src/backend/access/undo/perbackend/pbu_undoworker.c (L902-L903)
Comment/behavior mismatch: the comment says "Stop one of the lingering worker" (singular) and the function header says "we try to stop some worker", but this loop has no break, so it sends SIGTERM to every lingering worker not in the given database. If that is intentional, fix the comments; otherwise add a break after the first kill.
📄 src/include/access/perbackend/pbu_undorecord.h (L101-L102)
Portability/encoding bug: urec_fork is a ForkNumber, which is an enum and therefore int-sized (4 bytes), but SizeOfUndoRecordRelationDetails is offsetof(..., urec_fork) + sizeof(uint8) = 1 byte. The pack/unpack path (pbu_undorecord.c InsertUndoBytes/ReadUndoBytes) serializes and deserializes only this 1 byte of the 4-byte field. On little-endian this happens to carry the low byte (correct for small fork numbers); on big-endian (s390x, PPC64 BE) the single byte is the MSB, so the fork number round-trips as garbage. Since work_rd is a file-scope static, stale upper bytes can also leak in on decode. This depends on endianness/struct layout for a serialized format, which the portability gate forbids. Either serialize the field at its true width or store the fork number in a fixed-width (e.g. uint8) field explicitly.
📄 src/include/access/perbackend/pbu_undolog.h (L196-L197)
Footgun: the do { ... } while (false); idiom must not include the trailing semicolon inside the macro. With the trailing ;, every call site produces a double semicolon (an empty statement), and more importantly the macro cannot be used safely in if (c) UndoRecPtrAssignRelFileNode(...); else ... because the stray ; terminates the if. Drop the trailing semicolon so callers supply it, matching the standard PostgreSQL multi-statement-macro convention.
💡 Suggested change
Before:
(rfn).relNumber = UndoRecPtrGetRelNode(urp); \
} while (false);
After:
(rfn).relNumber = UndoRecPtrGetRelNode(urp); \
} while (false)
📄 src/include/access/perbackend/pbu_undoworker.h (L16-L18)
UndoWorkerDelay is declared extern here but is never defined or referenced in any .c file (confirmed via tree-wide search). The actual "undo worker sleep time" GUC is undo_worker_naptime (registered in guc_parameters.dat, declared in undolog.h). This is a dead declaration left over from the copied template; remove it.
📄 src/include/access/perbackend/pbu_undoworker.h (L3-L9)
Header boilerplate is inaccurate: the comment names the file undoworker.h / src/include/access/undoworker.h, but the actual file is pbu_undoworker.h under access/perbackend/, and it exports from pbu_undoworker.c, not undoworker.c. The Portions Copyright (c) 1996-2019 stamp is also stale for a newly added file. Fix the identity lines so they describe this file.
💡 Suggested change
Before:
* undoworker.h
* Exports from undoworker.c.
*
* Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
* src/include/access/undoworker.h
After:
* pbu_undoworker.h
* Exports from pbu_undoworker.c.
*
* Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
* src/include/access/perbackend/pbu_undoworker.h
📄 src/include/access/perbackend/pbu_undorequest.h (L33-L34)
undo_rollback_overflow_size is a GUC-backed global (registered PGC_SIGHUP in guc_parameters.dat). GUC variables in this tree are consistently declared with PGDLLIMPORT (see guc.h, miscadmin.h) so extensions can read them on Windows/MSVC. This declaration (and the duplicate in undolog.h) lacks it. Add PGDLLIMPORT.
💡 Suggested change
Before:
/* GUC: rollback-request size threshold (defined in pbu_undorequest.c) */
extern int undo_rollback_overflow_size;
After:
/* GUC: rollback-request size threshold (defined in pbu_undorequest.c) */
extern PGDLLIMPORT int undo_rollback_overflow_size;
📄 src/include/access/perbackend/pbu_undorequest.h (L33-L34)
undo_rollback_overflow_size is already declared extern in src/include/access/undolog.h (line 145). Declaring the same global in two headers is a maintainability hazard: the two can drift (e.g. one gets PGDLLIMPORT, the other does not) and any TU including both now has a redundant declaration. Keep a single canonical declaration (undolog.h) and drop it here, or vice versa.
📄 src/include/access/perbackend/pbu_undorequest.h (L28-L28)
The sentinel -1 is stored into undo_worker_queue, whose type is the enum UndoWorkerQueueType (lowest member XID_QUEUE = 0, no negative member). The compiler may pick an unsigned underlying type for this enum, in which case the sentinel is stored as a large positive value and would never compare equal to -1. It happens to be harmless today only because no code ever reads the field back against InvalidUndoWorkerQueue. To be portable, either give the enum an explicit negative member or make the field a plain int.
📄 src/include/access/perbackend/pbu_undorequest.h (L117-L124)
Macro-hygiene inconsistency and footgun: ResetUndoRequestInfo parenthesizes its argument as (urinfo)->..., but SetUndoRequestInfoFromRHEntry uses the bare urinfo->... / rh->.... If a caller ever passes a non-trivial expression, the unparenthesized form breaks. Parenthesize all macro parameter uses for consistency.
💡 Suggested change
Before:
#define SetUndoRequestInfoFromRHEntry(urinfo, rh, cur_queue) \
( \
urinfo->full_xid = rh->full_xid, \
urinfo->start_urec_ptr = rh->start_urec_ptr, \
urinfo->end_urec_ptr = rh->end_urec_ptr, \
urinfo->dbid = rh->dbid, \
urinfo->undo_worker_queue = cur_queue \
)
After:
#define SetUndoRequestInfoFromRHEntry(urinfo, rh, cur_queue) \
( \
(urinfo)->full_xid = (rh)->full_xid, \
(urinfo)->start_urec_ptr = (rh)->start_urec_ptr, \
(urinfo)->end_urec_ptr = (rh)->end_urec_ptr, \
(urinfo)->dbid = (rh)->dbid, \
(urinfo)->undo_worker_queue = (cur_queue) \
)
📄 src/include/access/perbackend/pbu_undorequest.h (L148-L149)
Comment path drift: these functions are defined in pbu_undoaction.c (confirmed), not undoaction.c. Update the comment to name the real source file.
💡 Suggested change
Before:
/* functions exposed from undoaction.c */
extern UndoRecInfo *UndoRecordBulkFetch(UndoRecPtr *from_urecptr,
After:
/* functions exposed from pbu_undoaction.c */
extern UndoRecInfo *UndoRecordBulkFetch(UndoRecPtr *from_urecptr,
📄 src/include/access/undo_bufmgr.h (L187-L188)
The entire undo_bufmgr module is dead code. None of the functions declared here (ReadUndoBuffer, ReadUndoBufferExtended, ReleaseUndoBuffer, UnlockReleaseUndoBuffer, MarkUndoBufferDirty, InvalidateUndoBuffers, InvalidateUndoBufferRange, UndoMakeBufferTag, IsUndoBufferTag) nor the inline helpers (UndoLogGetRelFileLocator, IsUndoRelFileLocator) nor the macros (UndoRecPtrGetFileOffset, UNDO_USABLE_BYTES_PER_PAGE, UndoLogicalToFileSize) have any caller in the tree -- undo_bufmgr.h is included only by undo_bufmgr.c itself, and the only grep hits for "UnlockReleaseUndoBuffer" are the distinct plural-named UnlockReleaseUndoBuffers() in pbu_undoinsert.c. The live per-backend engine does its own buffer handling via pbu_undofile.c/pbu_undolog.h. Per the YAGNI/minimalism discipline, drop this unused module (header + undo_bufmgr.c) rather than ship a ~290-line "legacy backward compatibility" wrapper with zero consumers. (high confidence)
📄 src/include/access/undo_bufmgr.h (L47-L47)
UNDO_DB_OID (9), UNDO_DEFAULT_TABLESPACE_OID (1663) and UndoLogForkNum (MAIN_FORKNUM) duplicate the authoritative definitions UndoLogDatabaseOid (9) and UndoLogForkNum in access/perbackend/pbu_undolog.h, which is what the live code (smgr.c, pbu_undofile.c, pbu_undolog.c) actually keys buffer tags on. Having the same reserved pseudo-OID defined under two different names in two headers is a maintenance footgun: a future change to the reserved value in one place silently diverges from the other. If this module is kept at all, it must reuse the single canonical definition rather than redeclare it. (high confidence)
📄 src/include/access/undo_bufmgr.h (L243-L245)
Declaring UndoMakeBufferTag/IsUndoBufferTag only inside #ifdef BUFMGR_INTERNALS_H makes the header's contract depend on whether another header's include guard happened to be defined first -- a caller that includes undo_bufmgr.h before (or without) storage/buf_internals.h silently loses these declarations and gets an implicit-declaration error or, worse, a wrong implicit prototype. This is exactly the "#ifdef fencing your feature" pattern the conventions forbid (POLA). Move these two declarations into undo_bufmgr.c (file-local, since BufferTag is a buf_internals.h type) or require/forward-declare properly. (medium confidence; moot if the dead module is removed as noted above.)
📄 src/include/access/undolog.h (L65-L68)
Dead fields/enum values. UNDO_LOG_SEALED and UNDO_LOG_DISCARDABLE, and the seal_ptr/sealed_time fields in UndoLogControl, are only ever written during init in undolog.c (log->state = UNDO_LOG_FREE; seal_ptr/sealed_time zeroed) and never read anywhere in the tree. The header comment above even states the ACTIVE->SEALED->DISCARDABLE rotation "no longer occurs," so these are retained-for-nothing. Per the YAGNI discipline, drop the unused states and the seal_ptr/sealed_time fields (and the UndoLogState enum if nothing else survives) rather than carry dead state in a shared-memory struct. (high confidence)
📄 src/backend/access/undo/perbackend/pbu_undoinsert.c (L1392-L1397)
Memory leak on the two NULL-return paths of UndoFetchRecord(). urec was palloc0'd at the top of the function, but when the log is gone (log == NULL) or the record is already discarded (!UndoRecordIsValid(urp)), the function releases urec->uur_buffer and returns NULL without freeing urec. The normal path returns urec to the caller (freed via UndoRecordRelease), but these early exits leak sizeof(UnpackedUndoRecord) every call. The "already discarded" case is common and reached from hot paths (discard worker, visibility checks, undo apply) that are not always inside a short-lived MemoryContext, so the leak accumulates. Add pfree(urec); before each return NULL; here (and in the !UndoRecordIsValid(urp) block below).
💡 Suggested change
Before:
if (log == NULL)
{
if (BufferIsValid(urec->uur_buffer))
ReleaseBuffer(urec->uur_buffer);
return NULL;
}
After:
if (log == NULL)
{
if (BufferIsValid(urec->uur_buffer))
ReleaseBuffer(urec->uur_buffer);
pfree(urec);
return NULL;
}
📄 src/backend/access/undo/perbackend/pbu_undoinsert.c (L1406-L1411)
Same urec leak on this second early-return path: urec (palloc0'd at the top of UndoFetchRecord) is not freed before return NULL. Free it here too.
💡 Suggested change
Before:
if (!UndoRecordIsValid(urp))
{
if (BufferIsValid(urec->uur_buffer))
ReleaseBuffer(urec->uur_buffer);
return NULL;
}
After:
if (!UndoRecordIsValid(urp))
{
if (BufferIsValid(urec->uur_buffer))
ReleaseBuffer(urec->uur_buffer);
pfree(urec);
return NULL;
}
📄 src/include/access/undo_xlog.h (L126-L128)
WAL on-disk struct must not depend on enum width. xl_undo_batch is a WAL record body: it is written verbatim via XLogRegisterData((char *) &xlrec, SizeOfUndoBatch) in undoinsert.c and read back by memcpy in undo_xlog.c, and SizeOfUndoBatch is derived from sizeof(UndoPersistenceLevel). UndoPersistenceLevel is a bare C enum whose underlying type is implementation-defined (width and signedness vary by compiler/ABI). PostgreSQL requires WAL/on-disk layouts to use fixed-width integer types so that a standby or crash recovery built with a different compiler decodes the same bytes. Store persistence as a uint8 (or uint32) field here instead of the enum, same for xl_undo_inflight_xact arrays sized by NUndoPersistenceLevels. There is no StaticAssert pinning sizeof(UndoPersistenceLevel) anywhere. (medium confidence)
📄 src/include/access/undorecord.h (L75-L75)
urec_prev is written into every UNDO record header (undorecord.c sets header->urec_prev = uset->prev_undo_ptr) but, as this comment itself documents, is never read by the apply path -- the real chain is between batches via xl_undo_batch.chain_prev. That is 8 WAL-durable bytes per record (plus the 4 bytes of alignment padding the README notes) burned on every UNDO record on a hot path, purely for "format stability." Either drop the field to shrink the record, or justify the on-disk cost; a write-only durable field is also a maintenance footgun inviting someone to "use" the stale value later. (medium confidence)
📄 src/include/access/perbackend/pbu_undorequest.h (L127-L127)
PendingUndoShmemSize() is declared (and defined in pbu_undorequest.c) to return int, but it computes its result in a Size via add_size/mul_size and does return size;, truncating Size to int. The caller in undo.c feeds it straight into add_size(size, PendingUndoShmemSize()), which expects a Size. This breaks the tree-wide convention (every other *ShmemSize() here, e.g. UndoLauncherShmemSize, UndoShmemSize, UndoLogShmemSize, returns Size) and silently truncates on LP64 if the hash table + queues ever exceed INT_MAX bytes, producing a wrong (possibly negative) size and an undersized shared segment. Change the return type to Size here and in the definition.
💡 Suggested change
Before:
extern int PendingUndoShmemSize(void);
After:
extern Size PendingUndoShmemSize(void);
📄 src/include/access/perbackend/pbu_undorequest.h (L138-L139)
Parameter name drift versus the definition in pbu_undorequest.c (UndoGetWork(bool allow_peek, bool remove_from_queue, UndoRequestInfo *urinfo, bool *in_other_db_out)). The second parameter is named is_undo_launcher here but is actually remove_from_queue in the implementation (and the callers pass true/false to control whether the item is popped from the queue, not whether the caller is the launcher). The names describe different semantics and will mislead readers. Rename to match the implementation (remove_from_queue, and in_other_db_out).
💡 Suggested change
Before:
extern bool UndoGetWork(bool allow_peek, bool is_undo_launcher,
UndoRequestInfo *urinfo, bool *in_other_db);
After:
extern bool UndoGetWork(bool allow_peek, bool remove_from_queue,
UndoRequestInfo *urinfo, bool *in_other_db_out);
📄 src/include/access/xactundo.h (L144-L144)
undo_instant_abort_threshold is a GUC variable (registered in guc_tables.c, defined in xactundo.c). Exported GUCs/globals must be marked PGDLLIMPORT or they fail to link from loadable modules/extensions on Windows/MSVC. The convention is applied consistently elsewhere in this subsystem (e.g. flux.h marks TTSOpsFluxTuple PGDLLIMPORT, and xact.h marks every exported GUC such as synchronous_commit/MyXactFlags). Add the marker here too.
💡 Suggested change
Before:
extern int undo_instant_abort_threshold;
After:
extern PGDLLIMPORT int undo_instant_abort_threshold;
📄 src/include/access/undoworker.h (L49-L50)
undo_worker_naptime is an exported GUC (defined in undolog.c, read in undoworker.c). It must be declared PGDLLIMPORT to be linkable cross-module on Windows/MSVC, matching the project convention for exported GUCs. Additionally, this same variable is already declared in undolog.h (line 144), which this header includes (line 21) -- the declaration is duplicated. Declare the GUC in exactly one header (and with PGDLLIMPORT) and drop the other.
💡 Suggested change
Before:
/* GUC parameters */
+ extern int undo_worker_naptime;
After:
/* GUC parameters: undo_worker_naptime is declared in access/undolog.h */
📄 src/include/access/undoworker.h (L22-L22)
fmgr.h is included but no fmgr symbols are used in this header. UndoWorkerMain(Datum main_arg) only needs Datum, which comes from postgres.h/c.h, not fmgr.h. Drop the unused include to keep the header minimal.
📄 src/backend/access/rmgrdesc/atmdesc.c (L30-L36)
The XLOG_ATM_ABORT desc omits the fileops_owned flag, which is a real recorded field with ownership/WAL-retention semantics (it is written in ATMAddAbortedBounded() and consumed by atm_redo()). A desc routine for a crash-recovery-critical rmgr should surface every meaningful field so pg_waldump output is usable for debugging recovery. Suggest printing fileops_owned.
💡 Suggested change
Before:
appendStringInfo(buf,
"xid %u, last_batch_lsn %X/%X, stop_lsn %X/%X, first_batch_lsn %X/%X, dboid %u",
xlrec->xid,
LSN_FORMAT_ARGS(xlrec->last_batch_lsn),
LSN_FORMAT_ARGS(xlrec->stop_lsn),
LSN_FORMAT_ARGS(xlrec->first_batch_lsn),
xlrec->dboid);
After:
appendStringInfo(buf,
"xid %u, last_batch_lsn %X/%X, stop_lsn %X/%X, first_batch_lsn %X/%X, dboid %u, fileops_owned %d",
xlrec->xid,
LSN_FORMAT_ARGS(xlrec->last_batch_lsn),
LSN_FORMAT_ARGS(xlrec->stop_lsn),
LSN_FORMAT_ARGS(xlrec->first_batch_lsn),
xlrec->dboid,
xlrec->fileops_owned);
📄 src/backend/access/rmgrdesc/atmdesc.c (L40-L48)
atm_desc()'s switch has no default case: an unknown/corrupt info byte leaves buf empty with no indication, which hides corruption in exactly the crash-recovery testing this subsystem relies on. Note this is inconsistent with atm_redo() (which elog(PANIC)s on unknown op codes) and with the sibling fileopsdesc.c in this patch (which prints "unknown fileops op code %u"). Add a default branch to emit the unknown info code.
💡 Suggested change
Before:
case XLOG_ATM_FORGET:
{
xl_atm_forget *xlrec = (xl_atm_forget *) data;
appendStringInfo(buf, "xid %u", xlrec->xid);
}
break;
}
}
After:
case XLOG_ATM_FORGET:
{
xl_atm_forget *xlrec = (xl_atm_forget *) data;
appendStringInfo(buf, "xid %u", xlrec->xid);
}
break;
default:
appendStringInfo(buf, "unknown atm op code %u", info);
break;
}
}
📄 src/include/access/atm_xlog.h (L37-L37)
reloid is a dead on-disk field: it is always written as InvalidOid (atm.c:221), the comment itself documents it as "kept for struct layout", and no reader derives behavior from it. Per YAGNI/minimalism, carrying and WAL-logging an unused field bloats every abort record and invites divergence. Consider removing it (and the corresponding sLog reloid plumbing) rather than preserving dead layout; at minimum this should not ship documented as a placeholder.
📄 src/include/access/undoworker.h (L42-L42)
shutdown_requested is written from a signal handler (undo_worker_sigterm() in undoworker.c sets it directly, outside any lock) and polled in the worker's main loop (while (!UndoWorkerShmem->shutdown_requested)). A plain bool is not safe here: writes from a signal handler to a non-sig_atomic_t object are not guaranteed well-defined, and the compiler may cache the main-loop read, so the worker can miss the SIGTERM request. The backend convention for signal-handler-set flags is volatile sig_atomic_t (see shutdown_requested in src/backend/postmaster/startup.c). Note this also conflicts with the LWLock-protected write path in UndoWorkerRequestShutdown(); a signal-handler flag cannot be reliably guarded by an LWLock. Use a volatile sig_atomic_t and set it unlocked in both paths.
💡 Suggested change
Before:
bool shutdown_requested; /* Worker should exit */
After:
sig_atomic_t shutdown_requested; /* Worker should exit (set in signal handler) */
📄 src/include/access/undodefs.h (L20-L20)
UndoRecPtr, UndoRecordSize (uint16) and UndoLogNumber (int) are typedef'd here with exactly the same definitions as in access/perbackend/pbu_undolog.h (lines 30/33/120 there), and neither copy is guarded. A single translation unit that pulls in both headers hits a duplicate typedef: e.g. flux_pbu.c includes access/undormgr.h (which includes this file) and access/perbackend/pbu_undolog.h. Identical typedef redefinition is only legal in C11; it is a constraint violation under the C99 baseline the tree targets and will error on strict/-Werror builds. Have one header own these types and have the other include it (or guard the typedefs), rather than defining the same names in two trees.
📄 src/include/access/undorecord.h (L107-L112)
Dead/speculative scaffolding. UndoRecordSetChunkHeader has no reader or writer anywhere in the tree (only the README and typedefs.list mention it); its doc claims "the data is organized into chunks" but no chunk code exists. Likewise the type field of UndoRecordSet is only ever written once (uset->type = URST_TRANSACTION in undorecord.c) and never read, so URST_INVALID, URST_MULTI, and URST_EPHEMERAL are entirely unused. Per the minimalism (YAGNI) rule, drop the unused chunk header, the type field, and the unused enum values until a consumer exists.
📄 src/include/access/undolog.h (L144-L145)
Duplicate extern declarations of the same GUC variables. undo_worker_naptime is also declared in access/undoworker.h, and undo_rollback_overflow_size is also declared in access/perbackend/pbu_undorequest.h (its defining module). Two extern declarations of one variable in two headers invite drift and are redundant; each GUC should be declared in exactly one header. undo_rollback_overflow_size in particular is defined in pbu_undorequest.c and does not belong in undolog.h.
📄 src/include/access/atm.h (L10-L12)
This file-header comment is inaccurate on two counts. (1) "All ATM functions are thin wrappers around sLog operations" is false: CheckPointATM() and ATMReloadFromCheckpoint() (declared below) do their own flat-file serialization (OpenTransientFile/write/read/CRC/durable_rename in atm.c), not sLog wrapping. (2) "The ATM is now backed by..." and "preserving the existing API and WAL format" narrate a migration; such change-history wording goes stale in the final tree, where a reader has no "before" to contrast against. Comments should describe current behavior, not how it got here. Suggest describing the sLog backing without the "now/existing" framing and acknowledging the checkpoint-persistence functions as the exception. (low confidence)
📄 src/backend/access/nbtree/nbtree_undo.c (L90-L90)
The struct does NOT pack with no holes. heap_tid (ItemPointerData, 6 bytes, 2-byte aligned per itemptr.h) sits at offset 12 and ends at offset 18; key_digest (uint32) requires 4-byte alignment, so the compiler inserts 2 bytes of padding and places it at offset 20, giving a 24-byte struct, not 22. SizeOfNbtreeUndoInsertLeaf (offsetof + sizeof = 24) absorbs this correctly for the memcpy round-trip, but (a) the comment is factually wrong, and (b) the 2-byte hole is uninitialized memory copied verbatim into the persistent WAL/undo stream (reproducibility/valgrind hazard). Prefer reordering so the uint32 precedes the 6-byte TID to eliminate the hole, and fix the arithmetic in the comment.
💡 Suggested change
Before:
* The fields pack with no holes: 4 + 4 + 2 + 2 + 6 + 4 = 22 bytes.
After:
* Reorder key_digest before heap_tid to avoid alignment padding:
* 4 + 4 + 2 + 2 + 4 + 6 = 22 bytes with no holes.
📄 src/backend/access/nbtree/nbtree_undo.c (L479-L483)
BTPageGetOpaque() is dereferenced via P_ISLEAF(opaque) before verifying the page is initialized. A block within the relation's bounds can still be PageIsNew (all-zero, e.g. a page extended but not yet initialized, or after a crash mid-split). On such a page pd_special is 0, so the opaque pointer lands in the page header and P_ISLEAF/btpo_next read garbage. The sibling hash_undo.c guards this explicitly (if (PageIsNew(page) || PageGetSpecialSize(page) == 0) break;) before touching the opaque. Add the same guard here.
💡 Suggested change
Before:
page = BufferGetPage(buffer);
opaque = BTPageGetOpaque(page);
if (!P_ISLEAF(opaque))
break;
After:
page = BufferGetPage(buffer);
if (PageIsNew(page) || PageGetSpecialSize(page) == 0)
break;
opaque = BTPageGetOpaque(page);
if (!P_ISLEAF(opaque))
break;
📄 src/backend/access/nbtree/nbtree_undo.c (L525-L527)
The forward kill path marks the item LP_DEAD but, unlike _bt_killitems() (nbtutils.c), never sets opaque->btpo_flags |= BTP_HAS_GARBAGE. For heapkeyspace (v4+) indexes this is harmless since the flag is no longer a gating condition, but for !heapkeyspace (pre-v4) indexes the flag gates the "getting tired" LP_DEAD reclamation during inserts; without it the entries this RM kills linger until VACUUM, partly defeating the purpose of index UNDO on those indexes. The flag is an un-logged hint (masked by btree_mask), so no WAL change is needed to match the existing kill path.
📄 src/backend/access/nbtree/nbtree_undo.c (L562-L565)
Inconsistent with the sibling hash_undo.c, whose default case returns UNDO_APPLY_SKIPPED. Here an unknown subtype returns UNDO_APPLY_ERROR and logs a WARNING. Because nbtree records are only ever applied inline and SKIPPED on every later walk, an ERROR here marks the entire inline rollback as not-fully-applied and defers the whole chain to the ATM/revert worker unnecessarily (where every nbtree record will then be skipped anyway). A genuinely corrupt/short payload returning ERROR is defensible, but an unknown subtype should be SKIPPED to match hash and avoid the needless hand-off.
📄 src/backend/access/nbtree/nbtree_undo.c (L525-L527)
The forward kill path marks the item LP_DEAD but, unlike _bt_killitems() (nbtutils.c:390), never sets opaque->btpo_flags |= BTP_HAS_GARBAGE. For heapkeyspace (v4+) indexes this is harmless since the flag is no longer a gating condition, but for !heapkeyspace (pre-v4) indexes the flag gates the "getting tired" LP_DEAD reclamation during inserts; without it the entries this RM kills linger until VACUUM, partly defeating the purpose of index UNDO on those indexes. The flag is an un-logged hint (masked by btree_mask), so no WAL change is needed to match the existing kill path.
📄 src/backend/access/nbtree/nbtree_undo.c (L221-L222)
hdr is declared without an initializer and only its named fields are assigned, but NbtreeUndoInsertLeaf has 2 bytes of padding between heap_tid (offset 12-17) and the 4-byte-aligned key_digest (offset 20-23). SizeOfNbtreeUndoInsertLeaf therefore equals 24 and includes those padding bytes, so nbtree_undo_write() copies 2 uninitialized stack bytes into the UNDO/WAL payload.
This makes the emitted WAL non-deterministic for identical operations and will trip wal_consistency_checking and Valgrind. Zero the struct before populating it so the padding is deterministic. (The sibling hash_undo.c shares the same layout; worth fixing there too, but it is outside this review group.)
Confidence: high.
💡 Suggested change
Before:
NbtreeUndoInsertLeaf hdr;
ItemPointer heap_tid;
After:
NbtreeUndoInsertLeaf hdr;
ItemPointer heap_tid;
/* Zero padding bytes so the WAL-logged record is deterministic. */
memset(&hdr, 0, sizeof(hdr));
📄 src/backend/access/hash/hashinsert.c (L242-L249)
These two adjacent comment blocks are redundant. The first block ("Write UNDO record ... after WAL logging but while we still hold the buffer pin") and the second block ("Write index UNDO only when the parent table asks for it ...") both introduce the same call, separated by a blank line. The claim "while we still hold the buffer pin (needed for BufferGetBlockNumber)" is also imprecise: at this point the code still holds BUFFER_LOCK_EXCLUSIVE on both buf and metabuf (the metapage lock is only dropped two lines below at LockBuffer(metabuf, BUFFER_LOCK_UNLOCK)), not merely a pin. Consolidate into one block to keep the diff minimal and the rationale accurate.
💡 Suggested change
Before:
/*
* Write UNDO record for the insertion if the parent table AM supports
* UNDO. This must happen after WAL logging but while we still hold the
* buffer pin (needed for BufferGetBlockNumber).
*/
/*
* Write index UNDO only when the parent table asks for it (the index_undo
After:
/*
* Write index UNDO only when the parent table asks for it (the index_undo
📄 src/backend/access/hash/hash_undo.c (L450-L453)
Inconsistent with the sibling nbtree implementation on the unknown-subtype path. nbtree_undo_apply()'s default case emits ereport(WARNING, ...) and returns UNDO_APPLY_ERROR, surfacing a corrupt/unknown record; here an unknown info is silently swallowed as UNDO_APPLY_SKIPPED, hiding a potential bug or data corruption. Since the only valid subtype is HASH_UNDO_INSERT, an unknown value is a can't-happen condition that should be reported, not silently skipped. Align with nbtree_undo_apply().
💡 Suggested change
Before:
default:
return UNDO_APPLY_SKIPPED;
}
}
After:
default:
ereport(WARNING,
(errmsg("hash UNDO: unknown subtype %u", info)));
return UNDO_APPLY_ERROR;
}
}
📄 src/backend/access/rmgrdesc/fluxdesc.c (L62-L68)
These frontend-only (_fe) struct copies do not match the WAL records the backend actually emits, so pg_waldump will misparse every FLUX record.
Examples verified against src/include/access/flux_xlog.h and the emitter in flux_xlog.c:
- Real
xl_flux_insertis{Oid relid; OffsetNumber offnum; uint16 flags; uint32 tuple_len; uint64 commit_ts;}(emitted viaXLogRegisterData(&xlrec, sizeof(xl_flux_insert))at flux_xlog.c:397).xl_flux_insert_festarts withuint16 offnum, omits the leadingOid relidandtuple_len, and invents axact_tsfield that does not exist anywhere in FLUX WAL. Every field is read at the wrong offset andcommit_ts/xact_tsread past/into the wrong bytes. xl_flux_delete_fe/xl_flux_update_felikewise inventxact_tsand droptuple_len/old_tuple_len/new_tuple_len.xl_flux_vacuum_fedeclares five fields but the realxl_flux_vacuumhas a singleuint32 ntuples.xl_flux_lock_feusesuint32 xmax; uint16 infomask; uint16 infomask2but the realxl_flux_lockhas onlyuint8 infomask; uint8 lock_mode.xl_flux_supersede_fesplits the TID into new_blkhi/new_blklo/new_offnum, but the real record stores anItemPointerData new_tid.
Crucially, flux_xlog.h already provides correct FRONTEND-safe definitions under #else /* FRONTEND */ (lines 458-518). Delete these duplicates and include access/flux_xlog.h instead, which is both correct and avoids silent drift.
📄 src/backend/access/rmgrdesc/fluxdesc.c (L404-L406)
The XLOG_FLUX_CAS_UPDATE case hardcodes field offsets (data, data + 4, data + 6) and a magic datalen >= 14 with a "- 2 padding" comment, baking struct-padding/ABI assumptions into the parser. The real xl_flux_cas_update is {OffsetNumber offnum; uint16 flags; uint16 data_offset; uint16 data_len; uint64 new_commit_ts;}. Parse it via the struct (ideally the frontend-safe copy from flux_xlog.h) rather than hand-computed byte offsets, which are non-portable across compilers/ABIs.
📄 src/backend/access/rmgrdesc/undodesc.c (L31-L36)
These multi-blank-line gaps inside the switch bodies (also present at the top of each switch and around the APPLY_RECORD/BATCH cases in undo_identify) will not survive pgindent and are dead vertical space left from removed cases. Remove the extra blank lines so the file is pgindent-clean.
📄 src/backend/access/rmgrdesc/undodesc.c (L106-L106)
Inconsistent LSN formatting: XLOG_UNDO_BATCH prints chain_prev %X/%X, while XLOG_UNDO_INFLIGHT above uses the modern %X/%08X. Use %X/%08X here too to match the current LSN_FORMAT_ARGS convention.
💡 Suggested change
Before:
"total_len %u, chain_prev %X/%X, "
After:
"total_len %u, chain_prev %X/%08X, "
📄 src/bin/pg_waldump/rmgrdesc.c (L37-L42)
These hand-written extern prototypes duplicate declarations that already exist in access/perbackend/pbu_undolog_xlog.h and access/perbackend/pbu_undoaction_xlog.h. Duplicating them here risks silent signature drift and defeats the compiler's cross-check. Include the proper headers instead. Also, placing this extern block between #include "rmgrdesc.h" and #include "storage/standbydefs.h" splits the include list; move declarations/includes so the include block stays contiguous.
📄 src/bin/pg_waldump/rmgrdesc.c (L20-L22)
New includes are inserted out of alphabetical order: access/flux_xlog.h and access/fileops_xlog.h appear after access/multixact.h but before access/nbtxlog.h, and access/atm_xlog.h/access/undo_xlog.h after access/spgxlog.h. Reorder them alphabetically to match the surrounding block and keep the diff pgindent/clean.
📄 src/backend/access/transam/rmgr.c (L8-L9)
#include "storage/fileops.h" is placed between postgres.h and the first access/* include, outside the IWYU keep block, breaking the alphabetical include grouping. Move it into the keep block in alphabetical position (next to the other storage/* and access/* includes). Note the asymmetry with flux: fileops.h is included unconditionally while flux_xlog.h is guarded by #ifdef USE_FLUX; since USE_FILEOPS is always defined this is harmless today, but the ordering/grouping should still be fixed.
📄 src/backend/access/transam/rmgr.c (L44-L48)
These new includes are appended to the end of the IWYU keep block instead of being interleaved alphabetically with the existing access/* includes (brin_xlog.h ... xact.h). Reorder for consistency so the block stays alphabetized.
📄 src/include/access/rmgrlist.h (L53-L58)
RM_FLUX_ID and RM_FILEOPS_ID are registered under #ifdef USE_FLUX/#ifdef USE_FILEOPS, but both macros are unconditionally defined (src/Makefile.global.in sets USE_FLUX=1/USE_FILEOPS=1 via CPPFLAGS; meson.build sets them in pg_config via cdata.set). The guards are therefore dead and only create a footgun: because the rmgr IDs are positional in this table, any build that ever saw a differing value would shift all subsequent IDs and make WAL undecodable across builds. Since the feature always ships, drop the #ifdef fencing and register these rmgrs unconditionally (the no-ifdef-fencing-of-shipped-features convention). If conditional compilation is genuinely intended, the IDs must be reserved unconditionally so the table layout never shifts.
📄 src/backend/storage/file/fileops.c (L634-L637)
The partial-write fixup trims pendingFileOps->data_len by (len - after_len) but does not correspondingly decrement the global inverseBytes, which was incremented by the full payload_len + 8 at the earlier AddPendingFileOpWithData call. When this entry is later freed, FreePendingFileOp subtracts the now-smaller data_len, leaving (len - after_len) bytes permanently leaked into the static inverseBytes accumulator (it is never reset at transaction end). Over the life of a backend this budget drifts upward and will eventually falsely trip the ERRCODE_PROGRAM_LIMIT_EXCEEDED check in fileops_pathlen(), rejecting all further transactional file operations. Decrement inverseBytes by the same (len - after_len) amount here.
💡 Suggested change
Before:
inverse->after_len = nbytes > 0 ? nbytes : 0;
inverse->new_length = nbytes > 0 ?
Max(st.st_size, offset + nbytes) : st.st_size;
pendingFileOps->data_len -= len - inverse->after_len;
After:
inverse->after_len = nbytes > 0 ? nbytes : 0;
inverse->new_length = nbytes > 0 ?
Max(st.st_size, offset + nbytes) : st.st_size;
inverseBytes -= len - inverse->after_len;
pendingFileOps->data_len -= len - inverse->after_len;
📄 src/backend/storage/file/fileops.c (L374-L378)
This comment is inaccurate. RelationPreserveStorage() (catalog/storage.c) operates only on the smgr pendingDeletes list and does not call FileOpsCancelPendingDelete(). In fact there is no in-core caller of FileOpsCancelPendingDelete() anywhere in src/backend/, so it is dead code with a misleading doc. Separately, when XLogIsNeeded() is true the constructive ops (FileOpsCreate/Symlink/Link) register no PENDING_FILEOP_DELETE at all (only an UNDO record), so even if a caller existed this function could not suppress abort-time removal driven by the UNDO chain at wal_level>=replica. Either wire up the intended caller and handle the replica-level UNDO case, or drop the function and fix/remove the comment.
📄 src/backend/storage/file/fileops_undo.c (L777-L784)
The fileops_undo_desc switch omits FILEOPS_UNDO_WRITE, so UNDO records for the WRITE subtype fall through to the default arm and are rendered as "UNKNOWN" in pg_waldump/debug output, even though WRITE is a valid, inserted UNDO subtype. Add a case FILEOPS_UNDO_WRITE: opname = "WRITE"; break; arm.
💡 Suggested change
Before:
switch (info)
{
case FILEOPS_UNDO_CREATE:
opname = "CREATE";
break;
case FILEOPS_UNDO_TRUNCATE:
opname = "TRUNCATE";
break;
After:
switch (info)
{
case FILEOPS_UNDO_CREATE:
opname = "CREATE";
break;
case FILEOPS_UNDO_WRITE:
opname = "WRITE";
break;
case FILEOPS_UNDO_TRUNCATE:
opname = "TRUNCATE";
break;
📄 src/include/storage/fileops.h (L41-L53)
The XLOG_FILEOPS_* record-type macros are defined identically here and in access/fileops_xlog.h. fileops.c includes both headers in one TU, so these are redefined (allowed by C only because the token sequences match exactly, but a drift of a single value would silently corrupt WAL interpretation, and it violates DRY). Define these constants in exactly one header and have the other include it.
📄 src/include/storage/fileops.h (L507-L510)
FileOpsRmdirRecursive is already declared earlier in this header (under "Directory lifecycle API"). This is a duplicate declaration; remove one.
💡 Suggested change
Before:
/* Recursive directory removal */
extern void FileOpsRmdirRecursive(const char *path, bool at_commit);
#endif /* FILEOPS_H */
After:
#endif /* FILEOPS_H */
📄 src/backend/storage/file/fileops.c (L1071-L1077)
This recursion is unbounded and uncancellable: it recurses once per subdirectory with no depth limit (a deep tree can overflow the C stack), and the enumeration loop has no CHECK_FOR_INTERRUPTS(), so a large recursive scan cannot be cancelled. Since lstat() follows the entry only to classify it, this is also vulnerable to very deep structures built by an attacker-controlled directory layout. Add a CHECK_FOR_INTERRUPTS() in the loop; consider an iterative approach (as rmtree() uses) rather than C recursion.
💡 Suggested change
Before:
while ((de = ReadDirExtended(dir, path, LOG)) != NULL)
{
char subpath[MAXPGPATH];
struct stat st;
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
continue;
After:
while ((de = ReadDirExtended(dir, path, LOG)) != NULL)
{
char subpath[MAXPGPATH];
struct stat st;
CHECK_FOR_INTERRUPTS();
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
continue;
📄 src/include/storage/fileops.h (L405-L413)
This on-disk/durable UNDO payload embeds off_t, uint16 with implicit struct padding and is memcpy'd verbatim into UNDO records (and the sibling FileopsUndo* structs embed mode_t/uid_t/gid_t/dev_t/ino_t). Their width, signedness, and padding vary across platforms (Windows vs. 32/64-bit Unix). WAL/UNDO replay is same-architecture, so this is tolerable there, but FileopsUndoMkdir/FileopsUndoLink also carry dev_t/ino_t as identity, which the code itself notes is meaningless on a standby or restored backup. Prefer fixed-width int fields with explicit layout for anything written to a durable record, to avoid silent drift if the struct is ever read by a differently-built binary.
📄 src/backend/storage/file/fileops.c (L2273-L2273)
"sec2" is internal shorthand (presumably a design-doc/section reference) that is meaningless to a reader of this file; the same applies to "sec3 portability bug" in the REMOVEXATTR abort case. Comments should explain the reasoning in terms of the code, not cite external section labels. Please drop these tags.
💡 Suggested change
Before:
* sec2: intentional no-op during redo.
After:
* Intentional no-op during redo.
📄 src/port/pg_xattr.c (L87-L88)
Comment contradicts the code: this fallback returns -1 (with errno=ENOTSUP), it does not "succeed". The "succeed in WAL" wording describes caller policy (fileops.c treats ENOTSUP as best-effort), not this function's return value. Fix the comment to describe what the function actually does, e.g. "Unsupported platform: no xattr support; set ENOTSUP and return -1". Comments must describe current behavior. [moderate confidence]
💡 Suggested change
Before:
/* Unsupported platform: succeed in WAL but no-op locally */
(void) path;
After:
/* Unsupported platform: no xattr support, report ENOTSUP */
(void) path;
📄 src/port/pg_xattr.c (L148-L156)
Windows short-buffer semantics diverge from POSIX and break a caller contract. POSIX getxattr()/extattr_get_file() return -1 with errno=ERANGE when the value is larger than the supplied buffer, and callers rely on this: fileops_undo.c:171 explicitly checks errno == ERANGE. Here, if the ADS stream is larger than size, ReadFile fills the buffer and returns bytesRead == size with no error, so the caller silently gets a truncated value and treats it as a successful full read. Detect the short-buffer case (fileSize.QuadPart > size) and return -1 with errno=ERANGE to match the other platforms. [high confidence]
💡 Suggested change
Before:
if (!ReadFile(hFile, value, (DWORD) size, &bytesRead, NULL))
{
_dosmaperr(GetLastError());
CloseHandle(hFile);
return -1;
}
CloseHandle(hFile);
return (ssize_t) bytesRead;
After:
if ((unsigned long long) fileSize.QuadPart > (unsigned long long) size)
{
CloseHandle(hFile);
errno = ERANGE;
return -1;
}
if (!ReadFile(hFile, value, (DWORD) size, &bytesRead, NULL))
{
_dosmaperr(GetLastError());
CloseHandle(hFile);
return -1;
}
CloseHandle(hFile);
return (ssize_t) bytesRead;
📄 src/port/pg_xattr.c (L65-L67)
snprintf() can silently truncate the ADS path when strlen(path)+strlen(name)+1 >= MAXPGPATH, which would target the wrong stream (and then CREATE_ALWAYS could clobber an unrelated/base stream). snprintf returns the length that would have been written; check it against the buffer size and fail with ENAMETOOLONG instead of operating on a truncated path. This affects all three Windows branches (set/get/remove). [moderate confidence]
💡 Suggested change
Before:
snprintf(ads_path, sizeof(ads_path), "%s:%s", path, name);
hFile = CreateFileA(ads_path, GENERIC_WRITE, 0, NULL,
After:
if (snprintf(ads_path, sizeof(ads_path), "%s:%s", path, name) >= (int) sizeof(ads_path))
{
errno = ENAMETOOLONG;
return -1;
}
hFile = CreateFileA(ads_path, GENERIC_WRITE, 0, NULL,
📄 src/port/pg_xattr.c (L56-L58)
FreeBSD partial-write hazard: extattr_set_file() returns the number of bytes actually written and (ret >= 0) ? 0 : -1 reports success even if ret < size (a short write), silently storing a truncated attribute. Treat ret != (ssize_t) size as failure. [moderate confidence]
💡 Suggested change
Before:
ret = extattr_set_file(path, EXTATTR_NAMESPACE_USER,
name, value, size);
return (ret >= 0) ? 0 : -1;
After:
ret = extattr_set_file(path, EXTATTR_NAMESPACE_USER,
name, value, size);
if (ret < 0)
return -1;
if ((size_t) ret != size)
{
errno = ENOSPC;
return -1;
}
return 0;
📄 src/port/pg_xattr.c (L60-L63)
The Windows ADS branches reference MAXPGPATH, CreateFileA, HANDLE, DWORD, _dosmaperr, etc., but this file only includes "c.h", <errno.h> and "port/pg_xattr.h". On a real MSVC build these identifiers come from the Windows headers that c.h pulls in via <windows.h>/win32 port headers -- but this entire branch is compile-tested only on Windows, so verify it actually builds there (none of the buildfarm's non-Windows animals exercise it). If MAXPGPATH is not visible here, this will not compile on Windows. [moderate confidence]
📄 src/include/port/pg_xattr.h (L30-L32)
The last-resort PG_ENOATTR fallback to ENOENT conflates "attribute not found" with "file not found". Callers that branch on PG_ENOATTR (e.g. fileops.c treating removal of a missing attribute as success) would then also swallow a genuine missing-file ENOENT, masking real errors. On every platform this file actually targets (Linux has ENODATA, FreeBSD/macOS have ENOATTR), so the ENOENT branch is dead for supported platforms; prefer a distinct non-collidable value or a compile-time error rather than silently aliasing ENOENT. [moderate confidence]
📄 src/backend/access/rmgrdesc/undodesc.c (L41-L54)
The operation_type -> name mapping is wrong for FLUX CLRs. xl_undo_apply.operation_type carries the access-method-local UNDO subtype (set from info in flux_undo.c/hash_undo.c/nbtree_undo.c), not a shared numbering scheme. FLUX uses FLUX_UNDO_UPDATE=0x0002, FLUX_UNDO_DELETE=0x0003, FLUX_UNDO_UPDATE_DELTA=0x0004 (src/include/access/flux_undo.h). This table maps 0x0002->DELETE, 0x0003->UPDATE, 0x0004->PRUNE, so pg_waldump will print the wrong op name for every FLUX undo CLR (UPDATE/DELETE swapped, UPDATE_DELTA shown as PRUNE). The subtype namespace is not global, so a single flat table cannot be correct across AMs. Either decode per-rmid, or drop the invented names and just print the raw operation_type value.
📄 src/backend/access/rmgrdesc/fileopsdesc.c (L31-L32)
mode (mode_t) is passed to %o without the (unsigned int) cast used in the mkdir/chmod cases below (lines 96, 118). %o expects unsigned int; if mode_t promotes to a signed int this is a technical mismatch. Cast for consistency and portability.
💡 Suggested change
Before:
appendStringInfo(buf, "create \"%s\" flags 0x%x mode 0%o",
path, xlrec->flags, xlrec->mode);
After:
appendStringInfo(buf, "create \"%s\" flags 0x%x mode 0%o",
path, xlrec->flags, (unsigned int) xlrec->mode);
📄 src/backend/access/rmgrdesc/Makefile (L35-L37)
The OBJS list is otherwise kept in alphabetical order, but pbu_undoactiondesc.o and pbu_undologdesc.o are inserted after tblspcdesc.o instead of in their alphabetical position (between nbtdesc.o and relmapdesc.o). This matches meson.build's (equally misplaced) ordering, so builds are consistent, but reorder both for alphabetical consistency with the rest of the list. undodesc.o is correctly placed before xactdesc.o.
📄 src/port/pg_xattr.c (L6-L7)
Stale/inaccurate comment: the file claims platform detection "avoid[s] meson.build/configure.ac changes", but meson.build was in fact modified to add 'pg_xattr.c' (src/port/meson.build:17), same as the Makefile. The comment no longer describes reality and will mislead future maintainers. Drop the claim or reword to just state the detection mechanism.
[moderate confidence]
💡 Suggested change
Before:
* Platform detection uses compiler-defined macros rather than
* configure-time checks, avoiding meson.build/configure.ac changes.
After:
* Platform detection uses compiler-defined macros rather than
* configure-time checks.
📄 src/port/pg_xattr.c (L115-L117)
FreeBSD short-buffer semantics diverge from the POSIX getxattr() contract that callers rely on. On FreeBSD, extattr_get_file() with a too-small buffer does NOT fail with errno=ERANGE; it copies as many bytes as fit and returns that (smaller) count. fileops_undo.c:171 explicitly branches on errno == ERANGE, which will never fire here, and the probe-then-read pattern in fileops.c (pg_getxattr(NULL,0) then pg_getxattr(buf,vlen)) can silently return a truncated value if the attribute grows between the two calls. Compare the returned length against the probed size and set errno=ERANGE on a short read to match the Linux/POSIX contract.
[moderate confidence]
📄 src/port/pg_xattr.c (L75-L76)
size_t -> DWORD truncation on 64-bit Windows. size is size_t (64-bit) but is cast to the 32-bit DWORD for WriteFile; the subsequent written != (DWORD) size check compares against the already-truncated length, so a size >= 4GB would silently write a truncated attribute and still report success. Guard against size > MAXDWORD (or > DWORD_MAX) and fail with an appropriate errno before the cast. The same truncation exists in pg_getxattr's ReadFile call.
[low confidence]
📄 src/include/access/twophase_rmgr.h (L45-L49)
These FILEOPS 2PC callback prototypes do not belong in twophase_rmgr.h. The established convention (and the pattern followed elsewhere in this very change) is that each resource manager declares its own 2PC callbacks in its own subsystem header: lock_twophase_* lives in storage/lock.h, flux_twophase_* lives in access/flux.h, and pbu_twophase_* lives in access/perbackend/pbu_undorequest.h. twophase_rmgr.h should only carry the generic infrastructure (TwoPhaseCallback typedef, the callback arrays, RegisterTwoPhaseRecord).
The comment on line 45 even states these are "defined in storage/file/fileops.c", and storage/fileops.h already declares the rest of the FILEOPS public API (fileops_redo, fileops_desc). Move these two prototypes to storage/fileops.h for consistency. As written, FILEOPS is the sole outlier, and fileops.c is forced to #include "access/twophase_rmgr.h" merely to see its own prototypes (a layering inversion). (moderate confidence)
📄 src/backend/storage/file/fileops.c (L410-L414)
This doc comment describes FileOpsSync but sits directly above FileOpsCreate; the two are unrelated. FileOpsSync's actual definition (further down, ~line 927) has no comment, and FileOpsCreate now has two stacked comment blocks. The FileOpsSync comment is orphaned here. Move it to FileOpsSync's definition.
📄 src/include/storage/fileops.h (L199-L205)
These forward WAL records (xl_fileops_write/chmod/chown and the create/truncate variants) embed platform-variable types -- off_t, mode_t, uid_t, gid_t -- directly into the on-wire WAL layout, and SizeOfFileOps* is derived from offsetof()+sizeof() of those types. WAL is replayed on physical standbys and after pg_upgrade/restore across machines; off_t width (32 vs 64 / _FILE_OFFSET_BITS), mode_t (unsigned short on some platforms, int on others), and uid_t/gid_t width/signedness differ across OS and ABI. A standby whose type widths differ from the primary's will misread the field offsets, corrupting the replayed operation. Use fixed-width types (int64 for offset/length, uint32 for mode/uid/gid) for the record fields, same as required for the FileopsUndo* payloads. This is distinct from the UNDO-payload portability issue: these are the forward WAL records.
📄 src/backend/storage/file/README (L181-L181)
FileOpsCancelPendingDelete() is listed as a live transaction-lifecycle hook, but it has no in-core caller (dead code; see also its own misleading comment in fileops.c). Documenting it as an active API compounds the inaccuracy. Drop this line (or the function) until there is a real caller.
| flux_pbu.o \ | ||
| flux_pvs.o | ||
|
|
||
| include $(top_srcdir)/src/backend/common.mk No newline at end of file |
There was a problem hiding this comment.
Missing newline at end of file (the diff shows \ No newline at end of file). Every other backend Makefile, including the sibling access/undo/Makefile added in this same change, terminates the final include line with a newline. Add a trailing newline to match convention and keep git diff --check / POSIX text-file expectations clean. (low confidence on functional impact, high confidence it diverges from tree convention)
| include $(top_srcdir)/src/backend/common.mk | |
| +include $(top_srcdir)/src/backend/common.mk |
| (am_undo_engine = UNDO_ENGINE_PERBACKEND) and its rm_undo also reverses the | ||
| in-place index maintenance, which is why nbtree/hash write no separate index | ||
| UNDO for a FLUX-parented index. | ||
| * Two-phase commit: TWOPHASE_RM_FLUX_ID (=5), flux_twophase_* callbacks. |
There was a problem hiding this comment.
The claimed value is wrong: twophase_rmgr.h defines TWOPHASE_RM_FLUX_ID as 7 (5 is TWOPHASE_RM_FILEOPS_ID). This is a concrete, verifiable number that reviewers will check against the header; please fix it.
| * Two-phase commit: TWOPHASE_RM_FLUX_ID (=5), flux_twophase_* callbacks. | |
| * Two-phase commit: TWOPHASE_RM_FLUX_ID (=7), flux_twophase_* callbacks. |
| * Row locks are held in the shared tuple sLog, not on the row. Its hash | ||
| partitions have a fixed size; a row lock that does not fit fails with | ||
| "could not store row lock in sLog". A prepared transaction's row locks are | ||
| made durable with a FLUX two-phase record (FLUX_2PC_ROWLOCK). |
There was a problem hiding this comment.
This limitation is stale and describes removed behavior. The current implementation (slog_tuple.c) stores row locks in a DSA created on first use with growable per-relation tables (slog_tl_grow_table); there is no fixed-size capacity that fails, and the error string "could not store row lock in sLog" no longer exists anywhere in the backend (grep finds it only here and in a test comment describing the OLD design). The companion test 132_flux_rowlock_store.pl is titled "no fixed shared memory, no fixed capacity". Only the holder-index bucket count is fixed, and overflowing it costs extra probe time, not an error. Please rewrite this bullet to match the DSA-backed, growable design.
| /* Log page initialization */ | ||
| if (RelationNeedsWAL(rel)) | ||
| { | ||
| uint64 init_commit_ts = FluxGetCommitTimestamp(); |
There was a problem hiding this comment.
FluxGetCommitTimestamp() is called inside the critical section (started by START_CRIT_SECTION() above). That function does elog(ERROR, "FLUX MVCC not initialized") when FluxMvccShmem == NULL; throwing an error inside a critical section escalates to PANIC and crashes the server. The other caller of this helper (flux_handler.c FluxGetCommitTimestamp() call) deliberately fetches the timestamp before entering its critical section for exactly this reason. Fetch init_commit_ts before START_CRIT_SECTION().
| BlockNumber old_nblocks = RelationGetNumberOfBlocks(rel); | ||
|
|
||
| if (new_nblocks < old_nblocks) | ||
| { | ||
| /* Truncated - update FSM */ | ||
| FreeSpaceMapPrepareTruncateRel(rel, new_nblocks); | ||
| } |
There was a problem hiding this comment.
This truncation branch is dead code. The caller (flux_operations.c:8496) invokes FluxVacuumFSM(onerel, RelationGetNumberOfBlocks(onerel)), so new_nblocks equals the value RelationGetNumberOfBlocks(rel) re-reads into old_nblocks here; new_nblocks < old_nblocks is therefore never true and FreeSpaceMapPrepareTruncateRel() is never called. It is also redundant: the physical truncation path (FluxTruncateRelation -> RelationTruncate) already calls FreeSpaceMapPrepareTruncateRel() and truncates the FSM fork. Either drop this dead branch or have the VACUUM caller pass the real pre-truncation block count; as written the function and its header comment claim an FSM truncation that never executes.
| FluxTupleHasCommittedUpdateAfter(Relation rel, | ||
| const FluxTupleHeader *tuple, | ||
| Size tuple_len, |
There was a problem hiding this comment.
FluxTupleHasCommittedUpdateAfter() declares two parameters, rel and tuple_len, that are never used in the function body (confirmed by searching the whole file). This is dead API surface on an exported function (declared in flux.h). Drop both parameters, or add a brief comment justifying why they must stay in the signature. Unused parameters on a public API are a YAGNI/minimalism red flag the community will flag on -hackers.
| return FluxTupleSatisfiesMVCC(tuple, snapshot, relid, | ||
| (snapshot != NULL && | ||
| snapshot->snapshot_type == SNAPSHOT_MVCC) | ||
| ? snapshot->curcid : InvalidCommandId, | ||
| buffer, tid); |
There was a problem hiding this comment.
Contract inconsistency / latent NULL deref. FluxTupleVisibleToSnapshotDual() defends against a NULL snapshot in the ternary below (snapshot != NULL && ...), advertising NULL tolerance, but it then passes snapshot straight into FluxTupleSatisfiesMVCC(), whose first use of the snapshot is if (!IsMVCCSnapshot(snapshot)) -- which dereferences snapshot->snapshot_type and crashes on NULL. All current callers happen to pre-check snapshot != NULL, so this is not reachable today, but the two functions disagree on whether NULL is allowed. Either make FluxTupleSatisfiesMVCC() handle NULL explicitly, or drop the misleading snapshot != NULL guard here and document that a non-NULL snapshot is required.
| pg_write_barrier(); | ||
| FluxMvccShmem->xact_start_ts_slots[my_slot] = | ||
| MyFluxXactState->xact_start_ts; |
There was a problem hiding this comment.
Misplaced write barrier. The barrier is emitted BEFORE the publishing store to the slot, which is backwards: to let a reader that observes a bumped generation also observe this slot write, the ordering must be store slot; pg_write_barrier(); bump generation. Here the barrier precedes the store, so it orders nothing useful. The code happens to be correct only because the subsequent pg_atomic_fetch_add_u32() on the generation counter is itself a full barrier -- not because of this pg_write_barrier(). Compare with FluxCleanupTransactionState(), which correctly places the barrier after the slot store. Move the barrier after the assignment for consistency and to not rely on the atomic's incidental fencing.
| * Returns the number of tuples pruned. | ||
| */ | ||
| int | ||
| FluxPagePruneOpt(Relation relation, Buffer buffer) |
There was a problem hiding this comment.
Return-type/contract mismatch: FluxPagePruneOpt is declared int and documented "Returns the number of tuples pruned", but the success path returns true (==1) while early-exit paths return 0. The computed ndead count is never actually returned. Every caller uses it as a bool (if (FluxPagePruneOpt(...))), so behavior is correct today, but a future caller trusting the documented count would be silently wrong. Make the contract honest: either change the return type to bool and reword the comment to "Returns true if any tuple was pruned", or actually return ndead;.
| * Future enhancement: Log overflow block/offset in WAL DELETE record | ||
| * so UNDO log pruning can also clean up overflow chains. |
There was a problem hiding this comment.
Informal/aspirational comment markers (ponytail:, Future enhancement:, BUG 2/4/5 fix, BUG N:) throughout this file violate the stated comment conventions for a pgsql-hackers patch: comments must describe current behavior and explain why, not carry TODO-style future work or internal bug-tracker references. These read as noise to list reviewers. Rewrite them to describe only the code as it stands now (e.g. drop ponytail: minimalism notes, convert Future enhancement: into a plain statement of the current deferred-to-VACUUM behavior, and remove the BUG N labels while keeping the substantive rationale).
No description provided.