Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

.nornir/clone-event-stream.md

Clone / populate event stream — why a member didn't populate, readable remotely

Design + guide + idiot-guide for PLAN #6: make per-member clone/fetch/populate failures first-class, persisted, and readable from a thin viz/CLI client instead of eprintln!-only on the server log. The durable twin of the monitor's eprintln!("…fetch error…"). Reuses the release_events / airgap_events event-table shape and the EPIC N-data run→persist→live pattern.

1. The problem (seen live)

When nornir-server's poll loop (or a Workspaces.Fetch RPC, or nornir workspace add) clones+fetches a monitored workspace's git members, a per-member failure used to surface only as a line on the server's stderr:

nornir-monitor: nordisk::korp fetch error: clone-fetch https://… Couldn't obtain Username

A thin viz/CLI client (nornir … --server, nornir viz against oden) has no access to that log. It just sees the member's data missing and no reason why — was it an auth failure (https→ssh), a vanished/renamed repo, a private repo, a network blip? Invisible. The populate outcome was log-only, not data.

2. The fix — one row per member attempt, persisted to the warehouse

New append-only Iceberg table clone_events (LAW: persist to the warehouse, not /tmp; on the fast warehouse disk), one row per fetched member attempt:

col type meaning
ts_micros timestamptz when the attempt ran (UTC)
workspace string the workspace being fetched
member string the member (or * for a workspace-level republish)
remote string the git remote that was cloned/fetched
op string clone-fetch | republish
status string ok | error
detail string the error chain ({e:#}) on error; the resolved SHA / snapshot id on ok
elapsed_ms long wall-clock duration of the op

Scalar columns only (no depends_on DAG edges — a fetch has no inter-member ordering), so SQL readers (DuckDB / PyIceberg) query it directly. Schema: src/warehouse/iceberg_schema.rs::clone_events. Writer/reader: src/warehouse/clone_events.rs (mirrors release_events.rs / airgap_events.rs).

Where the monitor writes failures

src/monitor.rs::fetch_workspace now records a MemberOutcome { member, remote, op, status, detail, elapsed_ms } for every fetched member (ok OR error) onto FetchReport::outcomes. Because nornir-server holds the single-writer redb lock, the monitor itself does not open the warehouse — the call sites that own a handle persist the report via clone_events::record_fetch_report(&wh, &report) (best-effort + non-fatal — a logging-row failure never aborts a fetch/republish):

Call site file:symbol
Poll-loop sweep src/bin/nornir-server.rs::server_sweep (served handle)
Workspaces.Fetch RPC src/bin/nornir-server.rs WorkspacesSvc::fetch (served handle)
Eager workspace add src/bin/nornir-server.rs eager-register spawn_blocking
Fat CLI workspace fetch / populate src/bin/nornir.rs::persist_clone_events (own handle)

3. Expose it over gRPC — Viz.CloneEvents

proto/nornir.proto: rpc CloneEvents (VizTimelineRequest) returns (JsonResponse) on service Viz. Handler in src/bin/nornir-server.rs (VizSvc::clone_events) resolves the served workspace via AppState::ws(metadata) and returns query_clone_events(&wh, CloneSelector::Workspace(name)) as a JSON array of CloneEventRow — the SAME rows the embedded read returns (mirrors Viz.ReleaseEvents / Viz.TestResults / Viz.BenchTelemetry). Client decoder: src/viz/remote.rs::fetch_clone_events.

4. Show it in the viz — 🧬 nornir root pane

src/viz/nornir_tab.rs gains a 🩺 populate status panel for the active workspace: a per-member grid (member · op · ok/failed · detail) with a green/red summary (✓ all N ok / ✗ K member(s) failed to populate). It loads on a ⟳ button and auto-loads on a workspace switch:

LAW #6 (components emit what they render): state_json()["populate_status"] lists { workspace, total, failed, members:[{member, op, status, detail, remote, ts, elapsed_ms}] }, and the button is discoverable as { id:"populate_status", rpc:"Viz.CloneEvents" } so the headless matrix mechanically covers the surface.

5. CLI parity — nornir workspace events <name>

nornir workspace events nordisk          # human view, newest-first
nornir workspace events nordisk --json    # raw CloneEventRow array
nornir --server oden:7878 workspace events nordisk   # thin path over Viz.CloneEvents

Local path reads <root>/<name>/builds via open_read_only; thin path calls Viz.CloneEvents with the nornir-workspace header set to the target. No viz-only capability — the CLI prints the same event list (LAW: CLI parity).

6. Tests (inject-assert, not "didn't panic")

7. Place in the constellation

This is the EPIC N-data read+write loop for the fetch/populate surface: the producer (the monitor) streams outcomes into the warehouse; the thin client reads them back over Viz.CloneEvents or nornir workspace events. It complements release_events (release op-DAG) and airgap_events (bring-up DAG) — the same event-table shape, the same "see why it stalled" goal, now for the clone step that feeds every other table.