OUTDATE lost-record races

polardb15 · logindex replay · commit 0b3b45ef372

The two lost-record races, and how the watermark closes them

Skipping the mini transaction for single-page WAL records (c80f8ebba44) removed a hidden serialization: polar_logindex_mini_trans_end ran only after lastReplayedEndRecPtr was published, so a backend could never consume a buffer's OUTDATE flag for a record its replay couldn't reach. Without it, two interleavings lose the record and leave the buffer at resting state 0x4 — every later reader trusts the page and serves it stale, forever.

R — single-page record touching page P, start S, end E
F — replay frontier, lastReplayedEndRecPtr. Sits on record boundaries, so S ≥ F until R is published
OUT — the POLAR_REDO_OUTDATE bit: “this page needs replay before use”
wm — the fix: polar_outdate_lsn, highest E this buffer was marked OUTDATE for

W1Flag consumed before the logindex insert

Path 1 of polar_logindex_outdate_parse arms OUTDATE before inserting the record into the logindex (taken when the page is mid-IO or a backend is replaying it). The backend's iterator can't find R — it isn't in the index yet.

tprocesseventshared state
1startup parses R, arms OUTDATE on P's descriptor — logindex entry for R not inserted yet F=F₀ · OUT=1
same locked section also records the debt: wm ← E wm=E
2backend sees OUTDATE, starts page replay: clears the flag, scans logindex over [start, F₀) F=F₀ · OUT=0 · wm=E
3backend finds nothing — R has no entry, and would be out of range anyway (S ≥ F₀) page still lacks R
4startup inserts R into the logindex, then publishes the frontier F=E

Before — record lost

Post-replay check sees OUTDATE unset and simply exits. Flag gone, page missing R, OUT=0 forever: every reader skips replay. Resting state 0x4.

After — flag re-armed

Post-replay check compares the replay's exact bound with the watermark: F₀ < wm=E → re-arm OUTDATE. The next reader, seeing F ≥ E, scans a range that now contains R and applies it.

W2Replay bounded by an unpublished frontier

Even with the insert done, a backend without the mini-transaction page lock bounds its scan by lastReplayedEndRecPtr. The in-flight record starts at or after that frontier, so the range [start, F₀) provably excludes it.

tprocesseventshared state
1startup parses R: arms OUTDATE and inserts R's logindex entry at S F=F₀ · OUT=1
wm ← E under the same redo-state lock wm=E
2backend consumes OUTDATE, replays with bound F₀ — R's entry exists but S ≥ F₀ puts it outside [start, F₀) F=F₀ · OUT=0 · wm=E
3startup publishes the frontier past R F=E

Before — record lost

Same ending: flag already consumed at t=2, nothing re-checks after the publish. Reproduced by TAP test 015 on pre-fix code: final read returns 'old'.

After — flag re-armed

F₀ < wm=E at the post-replay check → OUTDATE survives until a reader observes F ≥ E and applies R. Test 015 passes: final read returns 'new'.

The rule that closes both

Both windows collapse into one invariant, enforced at the only place OUTDATE is cleared (polar_logindex_lock_apply_page_from):

Still open, tracked separately

A third window is not closed here: if R is parsed while P is not in the buffer pool, there is no descriptor to carry the watermark. A concurrent page fault whose read-in replay samples the frontier before the publish misses R with no flag armed at all. Also a c80f8ebba44 regression — the mini transaction covered the fault-in read through the page_added → GetCurrentReplayRecPtr bound (verified: test 016 fails on current code, passes with the mini-trans removal reverted). Fix design in jira-logindex-unbuffered-page-loss.md.