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

nordisk/dwarves

.nornir/daemonize-and-durable-cron.md

dwarves daemonize — dwarves serve + durable server-side cron

Class: design · Function axis: f0 the long-running forge daemon · f1 one shared server bootstrap · f2 durable server-side cron · f3 multi-target builds via nornir-build-thing. Strict: this file states only what the daemon IS — its parts and why they are shaped this way. Sibling: the systemd wrapping is service-lifecycle-L1.md; the client modes are dwarves-parity-spec.md.

dwarves is the CI/CD forge, and a forge has to stay up: hold the pipeline registry, answer gRPC, and fire scheduled pipelines on their own minute without anyone watching. This is that daemon — one server bootstrap reached through two doors (dwarves serve and the dwarves-server bin), with a durable cron sweep that fires each scheduled pipeline exactly once per matching minute, even across a restart.

The system as one — the function map (f0)

part lives in what it is
f1 one server bootstrap crate::server::serve / serve_blocking binds the addr, creates the warehouse, resolves the bearer token, spins the cron thread, serves dwarf.v1 until shutdown — the single source both doors call
f1a in-process door main.rs serve_cmd the dwarves serve subcommand (fat, --features server) → serve_blocking
f1b standalone door bin/dwarves-server.rs the thin bin — skidbladnir L1 verbs, then hands off to the SAME serve
f2 durable cron server_cron::CronStore / sweep_cron a redb KV (pipeline-key → last-fired minute) that makes sindri's in-memory dedup survive a restart
f3 multi-target build runner::RealRunner → nornir-build-thing each Variant step is one build_variant call — holger's 3 execs are the first consumer

Why one bootstrap (Law #1: never a twin)

Before this landed, the whole dwarf.v1 service, the bearer interceptor, the jera-Scheduler wiring and the serve-loop lived inside the dwarves-server bin, and dwarves serve could only print a pointer at it. That is a twin waiting to drift. The fix folds the entire boot into the lib (crate::server), so both entrypoints run the identical daemon — same forge, same cron, same jera Scheduler. The dwarves-server bin keeps only what is genuinely its own: the skidbladnir lifecycle verbs (--unit/--install/--start/--stop/--status), which act and exit before the shared serve boots.

serve(real) is async (tonic); serve_blocking(real) builds a multi-thread tokio runtime and drives it — the seam the synchronous CLI subcommand needs. real selects the cargo-building RealRunner over the hermetic DemoRunner (also flipped by $DWARVES_REAL_RUNNER).

Place in the nordisk constellation

The daemon reuses, never hand-rolls (Law #1):

The server interceptor deliberately mirrors nornir-server (same authorization: Bearer <token>, same dwarves-workspace metadata header, same serve-loop) as a clean extraction candidate. server.rs carries the standing TODO(single-source): lift the server + thin-client scaffold into a shared edda crate nornir-service consumed by nornir + holger + dwarves. Until that lands this is the honest stopgap, not a competing framework.

Durable server-side cron

sindri's enqueue_cron already dedups a cron pipeline to at most once per matching minute — but only in memory. The server rebuilds a fresh Sindri every sweep, so without help that dedup would be lost across sweeps and across a restart, and the pipeline would double-fire.

server_cron makes the dedup durable with the smallest possible addition — one redb table:

CRON_LAST : &str -> &str      // "project/pipeline"  ->  "YYYY-MM-DDTHH:MM"

Each sweep is seed → tick → persist:

  1. rebuild a Sindri from the current registry,
  2. seed_cron_last(store.load()?) — pull the persisted last-fired map back in,
  3. tick_scheduled(now, …) — cron match → enqueue → drain through the jera Scheduler,
  4. store.save(sindri.cron_last()) — write the updated map back.

Because step 2 reads from disk, a minute that already fired does not fire again — whether the sweep merely runs twice or the whole process restarted. The scheduling decision stays in sindri's pure cron_matches/enqueue_cron; server_cron adds only the KV. The loop ticks every 60s (not 300s) so minute-granular crons are never skipped, and runs on a std::thread because Sindri::tick is synchronous by design.

Warehouse / storage seam

Tests (inject-assert, LAW 1)

No robotui scenario: the daemon and cron sweep are a headless gRPC + timer surface, not a UI. The UI surfaces (Forge/Funnel/Claude panes) carry their own robotui scenarios; see three-pane-shell.md and funnel-live-graph.md.

Source map

See also: service-lifecycle-L1.md · dwarves-parity-spec.md · the-two-dags.md · menu-features.md.