📡 Live Run — hydrated from the warehouse (C3)
Epic C, item C3 of THE_BIG_PLAN. The viz Live Run pane's durable source is now the warehouse
release_eventstable, not/tmp. A live file/stream tail remains only as a fast accelerator on top of that durable baseline. This satisfies the project LAW persist-to-warehouse-stream-not-tmp.
Why this changed
Before C3 the Live Run pane (src/viz/live.rs) read a release run only from <workspace>/.nornir/logs/release-run-*.events.ndjson — a /tmp-class file the producer flushes per line. That file is a stopgap sink: it is GC'd, lost on a reboot, and invisible to any other client. So a run that had already happened — and is durably recorded in the warehouse — showed an empty Live Run pane on load. That violates the LAW: the durable record must be the warehouse, and the viz must read primarily from the warehouse.
The 🚀 Release tab already does the right thing: it reads release_events via query_release_events. C3 makes Live Run read from the same table by the same path, so the two surfaces share one durable source of truth and Live Run survives a reboot / is visible to other clients.
The two-layer model
Live Run now has a durable source and a live-tail accelerator layered on top of it:
durable (source of truth) live-tail (fast accelerator, optional)
───────────────────────── ──────────────────────────────────────
warehouse release_events ──▶ local: .nornir/logs/release-run-*.events.ndjson
(read on load + every reload) remote: nornir-server Release.Progress stream
-
Durable hydrate — on first draw and on every reload/workspace-switch, the pane opens the warehouse read-only (lock-tolerant, coexisting with a live server, exactly like
nornir release events) and callsquery_release_events(.., EventSelector::All). It picks the newest run (maxts_micros, the same "newest run on top" rule the Release tab uses), sorts its rows byseq, and converts eachrelease_eventsrow into theLiveEventthe pane already renders. This is the baseline the pane shows even when no/tmplog exists. -
Live-tail accelerator — a background thread keeps the pane moving at sub-second latency while a run is in flight:
- local mode tails the newest
release-run-*.events.ndjson(cheaper than a warehouse append per event); when a live file appears it supersedes the hydrated baseline (it is the same run, just faster). - remote (thin-client) mode streams the run from a
nornir-serveroverRelease.Progress— there the server's warehouse is the durable source and the stream is both the hydrate and the tail.
- local mode tails the newest
The tail is a fallback accelerator, never the source of truth: if the /tmp log is gone (reboot, GC) the warehouse-hydrated events still render, and a reload re-reads the warehouse.
Row → event mapping
events_from_release_rows (in src/viz/live.rs) folds a durable release_events row's op × phase × status boundary into the live event stream:
release_events row |
LiveEvent |
op == "run", phase == start |
RunStart (run_id, workspace from detail) |
op == "run", phase == end |
RunEnd (ok = status == ok) |
any op, phase == start |
PhaseStart { repo = component, phase = op } |
any op, phase == end |
PhaseEnd { ok, duration_ms = elapsed_ms } |
phase == skip / other |
(not rendered) |
So the warehouse table and the /tmp/SSE live tail feed one identical render path — the pane never branches on where an event came from.
What it exposes (LAW #6 — see-what-the-user-sees as data)
LiveRunState::state_json() is folded into the app's state_json under "live", carrying the rendered run + proof of its source:
"live": {
"durable_source": "warehouse /…/warehouse", // or "server … (release_events)"
"hydrated_from_warehouse": true, // the run was read from the table
"hydrated_run_id": "run-…",
"live_tail": "(none yet)", // file/stream once a live run arrives
"run_id": "run-…", "workspace": "holger", "run_ok": null,
"current_repo": "holger", "current_phase": "gate",
"total_pass": 3, "total_fail": 0, "events": 4,
"lines": ["▶ run run-… (holger)", "… znippy test start",
"✓ znippy test (1.2s)", "… holger gate start"]
}
It also emits the $NORNIR_VIZ_TRACE stream (src/viz/trace.rs): live.hydrate IN {source: "warehouse.release_events", root} → OUT {run_id, events} (or {error}), so an external agent reads back exactly the durable run the pane hydrated.
How to verify
- Test
tests/viz_live.rs::live_view_hydrates_from_warehouse_release_eventsseedsrelease_eventsrows into a real on-disk warehouse, deletes nothing to/tmp, points the app at that warehouse, hydrates the Live Run pane the way a load/reload does, and assertsstate_json()["live"]exposeshydrated_from_warehouse == true, therun_idread back, and the rendered event lines — the reboot / other-client situation, proven warehouse-sourced. - Unit
src/viz/live.rs::testsproves the row→event mapping picks the newest run, reduces the header, and renders the right lines. - Manual —
nornir release runonce, kill/tmp/...events.ndjson, relaunchnornir viz(or reload), open 📡 Live Run: the run is still there, read from the warehouse.
Files
src/viz/live.rs— the durable hydrate (hydrate_from_warehouse,events_from_release_rows),state_json, trace, the/tmp/SSE tail kept as accelerator.src/viz/app.rs—LiveRunState::new(log_dir, warehouse_root)wiring,"live"instate_json,reload()re-hydrates, theinject_*_for_test/live_hydrate_from_warehouse_for_testhooks.src/warehouse/release_events.rs— the durable table +query_release_eventsread path (unchanged; reused). See../dwarves/.nornir/release-events.md.tests/viz_live.rs— the inject-and-assert + real-warehouse proofs.