Durable Lane state: poll_passes and poll_lanes, plus their store surface #139
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent
Spec #134.
What to build
Lane observability stops dying with the process. Today Lane state lives in the poller's memory, so the one page whose job is proving the poller is alive says "no data yet" for up to an hour after every deploy. This ticket lands the durable home for that state and the store surface over it. Nothing reads it yet -- the poller starts writing it and the page starts reading it in later tickets -- so this one is verified at the store seam.
Two tables. A pass log, one row per Lane Pass, append-only, roughly 120 rows a day across six Sites: the Site, the run stamp in unix ms from the poller's clock, a
skiptext column (empty meaning the pass reached its loop, otherwise why it returned early), the pass figures (due, checked, gap, clamped) and five named outcome counts (refused, unreachable, no chapter, unfetchable, errors).(site, ran_at)is the whole index budget: one goroutine per Lane writes sequentially so the pair is unique without a surrogate id, and it serves both reads -- latest row per Site, and a per-Site window sum. Retention scans, which at roughly 1.7k live rows is cheaper than a second index.A per-Site Lane state row holding
paused_untilandrefuse_until, both defaulting to zero -- one row read at the top of a pass serving two gates.Store methods, over a
LanePassrow type that carries the pause and refusal stamps joined from the Lane row on read rather than as pass columns:RecordLanePassinserts and prunes in the same call -- delete-on-insert, so the Lane goroutine is the pruner and no ticker enters a backend that has none (sessions already expire lazily at lookup for the same reason). The retention cutoff is caller-supplied, keeping the store clockless as the due query already is. Retention is 14 days and is not the display window -- the window is what the owner is shown, retention is how far back a question can reach, and the two must not be collapsed.LatestLanePass(site)-- the carry-forward read; the recorder needs one Site, not six.LatestLanePasses()--DISTINCT ON (site)newest-first, left-joined to the Lane row: one query serving both the Lanes page and the landing verdict's sum.LanePassOutcomes(since)-- per-Site outcome sums over a window.SetLaneRefusal,PauseLane(rejects a non-future expiry),ResumeLane(zeroes the pause stamp and keeps the row),PausedLanes,LanePausedUntil.Migrations take the next free numbers. Measured 2026-08-21: the tree's migrations stop at 0011 and everything the parent map specified is unbuilt. Migrations are globbed by version, so contiguity at merge time is what matters, not agreement with any paper numbering.
Acceptance criteria
RecordLanePassprunes rows older than the caller's cutoff in the same call that insertsLatestLanePassesreturns exactly one row per Site, newest per Site, with the pause and refusal stamps joined inLanePassOutcomessums the five counts per Site over the given window and ignores rows outside itPauseLanerejects a non-future expiry;ResumeLanezeroes the pause stamp rather than deleting the rowgo test ./...greenBlocked by
None — can start immediately.
Acceptance check complete against commit
fd1131d.Ready to close.