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.
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.
| t | process | event | shared state |
|---|---|---|---|
| 1 | startup | 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 | ||
| 2 | backend | sees OUTDATE, starts page replay: clears the flag, scans logindex over [start, F₀) |
F=F₀ · OUT=0 · wm=E |
| 3 | backend | finds nothing — R has no entry, and would be out of range anyway (S ≥ F₀) | page still lacks R |
| 4 | startup | 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.
| t | process | event | shared state |
|---|---|---|---|
| 1 | startup | 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 | ||
| 2 | backend | consumes OUTDATE, replays with bound F₀ — R's entry exists but
S ≥ F₀ puts it outside [start, F₀) |
F=F₀ · OUT=0 · wm=E |
| 3 | startup | 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
polar_logindex_lock_apply_page_from):
- Arm: whenever OUTDATE is set for record R, record
wm = max(wm, E)in the same locked section. - Clear: after replay, keep the flag unless the replay's exact upper bound reached
wm. Never re-samplelastReplayedEndRecPtrhere — it can advance after the scan and vouch for a record the scan never saw. - Defer, don't spin: a reader that already applied once and still sees
F < wmreturns the page at the frontier (polar_lock_buffer_ext) instead of re-running no-op replays until the publish lands. The in-flight record is above every snapshot's horizon, so nothing can miss it.
Still open, tracked separately
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.