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

nordisk/korp-collectors

.nornir/collectors-daemon-design.md

korp Collectors — the daemon architecture (design)

Collector vs Pipeline. A collector deals with OUTSIDE data — it ingests an external source into korp. A korp pipeline is in-house data movement + transformation (SDP, Iceberg→FalkorDB). Collectors = ingress daemons; pipelines = internal transforms. This repo is collectors only.

A collector is a continuous daemon that strives to reach and hold ONLINE for everything in its config, self-healing across restarts. It is a state machine, not a one-shot job. osm-cut (planet → regions → the lettered a–g cut) is the first implementor; the existing Iceberg collectors (DCAT-AP-SE, PxWeb, ENTSO-E, REST) are redone to this design.

1. The traits (object-safe, so new collectors just slot in)

trait CollectorConfig  // what you SET     → Settings tab
    fn enabled(&self) -> bool
    fn schema()  -> ConfigSchema           // fields, incl. multi-target (region pick-list)
    (serde in/out of korp's settings JSON: collectors.<name> = { enabled, …custom })

trait CollectorState   // what it's DOING  → infra status matrix + click-expand
    fn phase(&self) -> Phase               // the state-machine value (see §3)
    fn healthy(&self) -> bool              // green when Online+healthy
    fn facet(&self) -> &dyn Facet          // its OWN custom UI area (state_json + ui)

trait Collector : CollectorConfig + CollectorState   // the daemon
    fn run(&mut self, ctx: CollectorCtx) -> !          // the never-ending loop
    // ticks its state machine toward ONLINE for every configured target

CollectorCtx hands the collector its infra: both storage backends (§4), a jera daemon-job handle (§5), the normal log, and the BIOS bus (§6).

2. Continuous daemon (not a one-shot)

The collector loop runs forever: reconcile config → do the next needed work → sleep/watch → repeat. For osm-cut: keep the planet fresh, keep every configured region cut into <work_dir>/<region>/, re-cut when a region is added or the planet updates. "Idle & Online" is just the steady state of the same loop.

3. State machine (pure facts — BIOS-style, never an "error" to be not-yet-online)

Per target and overall:

Offline → Downloading(planet, %) → Indexing(planet → region catalog)
        → Cutting(region, "<letter> <step>") → Online   (+ Degraded/Failed{reason} off any)

Being Downloading / not online yet is a normal fact, not a failure. Transitions are appended to the BIOS bus (§6). The machine is restart-safe: on korp-server restart it resumes from what's already on disk / in the bucket (batched files are reused, not re-fetched).

4. Storage — every collector gets BOTH

Some data is servable (belongs in S3); some needs random access and is dumb to keep in S3 (the planet file, indexes). So CollectorCtx provides:

5. jera — a TRUE never-ending daemon job (jera gets extended)

Today jera models fire-and-complete jobs. Add a first-class daemon job kind:

One daemon job per enabled collector. Its phase/progress is exactly what the State trait surfaces to the matrix.

6. Two logs — the work log AND the BIOS bus

collector map:    not online yet (downloading planet 12%)
collector map:    indexing regions…
collector energy: ONLINE
collector map:    ONLINE

Startup does not wait for collectors; the bus just reports facts as each tunes in. The infra matrix (§8) and a startup banner render this bus.

7. Region pre-list, sucked out of the planet (map collector)

After the torrent completes, osm-katana enumerates the planet's regions/sections (it's fast, gatling-parallel) into a cached region catalog on disk. Then:

8. Config (Settings) + Status (infra matrix) — the two faces

9. Torrent code moves INTO the collector

The librqbit planet-torrent fetch (currently korp/src/download.rs) moves into the osm-cut collector — the collector owns its own acquisition. download.rs becomes a thin re-export or is absorbed.

10. Bencher — prove the tiling uses ALL cores via gatling (NOT rayon)

Add a bencher (osm-katana), heavy + non-heavy, asserting the region cut (convert) saturates all cores through znippy-zoomies gatling fork-join — the whole point of osm-katana being rayon-free. Non-heavy = a small fixture (CI); heavy = a Sweden-scale cut. Follows the bencher law (host = a new row, same-window comparisons only; "has a bencher" is the gate, "fast enough" is not).

11. Adopt the existing collectors

DCAT-AP-SE / PxWeb / ENTSO-E / REST are redone as Collector daemons (Config + State

WAVE 1 — DONE (src/iceberg_daemons.rs, feature daemon): osm-admin, dcat, pxweb adopted as the first three warehouse-sink daemons; daemon_registry() now lists 4 (osm-cut + these three, under osm-daemon; the three alone under daemon). Two new CollectorCtx seams make the pattern generic over both sink kinds:

Each adopted daemon reuses its existing pure parser (dcat::parse_dcat_rdfxml, pxweb::parse_jsonstat2, osm_admin::extract_admin_units, rest::parse_records, …) and reaches ONLINE through the shared Offline → Downloading → Indexing → Online skeleton (empty-is-RED → Failed).

WAVE 2 — DONE (src/iceberg_daemons.rs, feature daemon): the mechanical follow-on, in batches, each a new iceberg_daemon!(name, url, table, parse, schema) line — batch 1: sdmx · usgs · nordpool; batch 2: openaq · erddap · wfs; batch 3: energy-charts · rss · entsoe{,-day-ahead,-generation}; batch 4: gtfs-rt-vehicles · gtfs-stops; batch 5: worldbank · gbif · eea-wise-water; batch 6: the Swedish real-time public-sector trio — polisen-events and smhi-weather (plain-GET JSON through rest::parse_records + the matching RestCollector preset field map) and trafikverket-trainpos (the one POST-body daemon: it builds the XML <REQUEST> with its key from config key / env key_env and POSTs it, then parses /RESPONSE/RESULT/0/TrainPosition — hand-written over the SAME shared reconcile, only its Source is a Post). daemon_registry() now lists 21 under daemon (+ gtfs-stops under gtfs, + osm-cut under osm-daemon). Every registered daemon self-registers into collector-register.redb (register.rs), so a RegisterReader in another process (korp's Collectors panel, or korp-collectors status) sees each one's phase + facet — proven RED→GREEN per daemon in tests/collector_daemons_register.rs. The remaining follow-on (gribnc · rest · mainframe presets) is the same iceberg_daemon! line.


Build order

  1. jera daemon-job kind (§5) — foundation.
  2. Collector traits + CollectorCtx + registry (§1, §4, §6).
  3. osm-cut as the first daemon (§2, §3, §7, §9) + its bencher (§10).
  4. Settings + infra matrix (§8).
  5. Adopt the existing collectors (§11).

(HISTORICAL, 2026-08: the urgent map un-blank landed FIRST and independently. The whole tiled route it fixed has since been deleted — korp map v2, e66f846..1d53b1b — and the map is served from the packed cut this collector produces.)

§14 — the REQUIREMENT declaration (lane CP, 2026-09-02)

A daemon declares what it needs before it can reach ONLINE, through the same crate::requires::Requirements the one-shot ingest Collector trait answers with:

fn requirements(&self) -> crate::requires::Requirements;   // CollectorConfig, defaulted

The default reads the source out of the daemon's own defaults() (url / endpoint / source / base_url) and declares no secret. A daemon that reads CollectorCtx::secret MUST override it and name the variable — a daemon whose key is absent can otherwise only report Failed at the wire, which is the state this declaration exists to replace with NeedsConfig.

SecretString / SecretStore / EnvSecretStore MOVED to crate::requires (unchanged; daemon:: re-exports them, so every existing path still resolves). The seam had to be readable without the daemon feature, because readiness answers for the ingest registry too and a second copy behind a second gate would be two answers to "is this key set?".

requires::SecretPresence is the one-method question readiness asks; the blanket impl<T: SecretStore + ?Sized> SecretPresence for T is the ONLY writer of the answer, so a host that swaps in a vault store changes what the UI calls runnable without readiness learning what a vault is.