A record parsed while its page sits outside the buffer pool can be lost when a backend faults that page in. Two mechanisms hand the record to that backend: the one on the branch, and the prototype that replaces it.
A single-page record takes no mini-transaction page lock, so nothing keeps a reader out while the startup process parses it. The logindex entry is already inserted, but lastReplayedEndRecPtr still sits below the record, and that frontier is what bounds a backend's fault-in replay.
R.end, the page goes valid with the flag clear, and the record is served stale until something else replays it.What is on the branch today. Before the parse begins, the startup writes the record's page tags and EndRecPtr into one global slot under a seqlock; every fault-in reads that slot from polar_logindex_io_lock_apply and arms the buffer's watermark when a tag matches.
The prototype. The startup publishes where it discovers the problem — the buffer-table miss inside polar_logindex_outdate_parse — into a slot for that mapping partition, while holding the partition lock it took for the lookup. The backend reads that slot at BufTableInsert, under the same lock in exclusive mode.
| Variant A · on the branch | Variant B · prototype | |
|---|---|---|
| Published where | before the parse, in polar_logindex_parse_xlog | at the buffer-table miss, in polar_logindex_outdate_parse |
| Published what | block 0's tag + derived VM tag + LSN | the tag the lookup used + LSN |
| Synchronization | dedicated seqlock | the buffer mapping partition lock, already held on both sides |
| Backend read | seqlock read per fault-in, after insert and I/O | plain read at BufTableInsert |
| Startup cost per record | seqlock write region + a clear | one 32-byte store, only on a miss |
| Shared memory | one padded slot | 128 KB |
| Invariant to hold | published tags must cover every page the rmgr parse callbacks touch | the startup parses serially and the frontier passes record N before N+1 is parsed |
| Spurious replay passes | none | only while a record is live in the same partition |
| C diff vs TTP_15_STABLE | +308 / −7 | +237 / −5 |
B removes the seqlock, the tag prediction, the fork derivation, the two coverage assertions, the arm_lsn plumbing and the in_flight_published out-parameter — and does less work per record on the startup's path, which is the path the mini-transaction removal was bought for.
Outcome: A ships. B leaves one interleaving uncovered (below), and closing it forces the probe back into the replay path keyed by partition rather than by page. That trades A's exact tag match for partition-granular arming — a page can be armed because a different page of its partition owes a record. The simplification is not worth giving up that precision.
BufferAlloc inserts a placeholder id (bufmgr.c:1336) under the partition lock and writes the real buf_id only later, outside it. A startup lookup landing in that gap gets a negative id, treats it as a miss (polar_logindex_redo.c:297) and publishes after the backend has already read the slot — so the record is lost exactly as in figure 1. A's probe runs after the insert and the I/O, so it sees a publication made anywhere in the parse window. Neither test covers this interleaving: 016 suspends the startup after the parse, so the publication always comes first.polar_evict_buffer and the victim path in BufferAlloc — each under the partition lock it already holds.slot->lsn > frontier), with the tag match kept as the exact common path. This is what makes B independent of how many pages a record touches.polar_logindex_parse_xlog has one caller, ApplyWalRecord in the startup. If standby parallel replay ever grows a second parser, this needs exclusive mode or a monotonic atomic.polar_in_flight_read stubbed to return invalid, the replica serves the stale row after catchup — got: 'old', expected: 'new'. Reaching that assertion means skipping 016's arm-injector wait first: that wait polls for 600 seconds before erroring, so with the arm gone the test spends ten minutes there before it ever tests the outcome.