feat(netflow-db): add merge-shards and a cluster launcher for day-sharded reprocessing#106
Note
🤖 Claude Opus 5.5 on behalf of Oliver
ELI5
A full reprocess takes days on one machine. This lets several machines each build a slice of days and then stitches the slices into one database, checking that the result matches a single-machine run.
Why
pipeline is one process with one SQLite writer. A production day takes ~16 min and uses only 4–5 of 24 cores, so a year of data is ~100 h single-process. Local days are independent: rollups max out at 1d, and every rollup bucket is aligned to local midnight. Day ranges can therefore be built separately and merged.
What changes
netflow-db merge-shards --output OUT SHARD...merges day-sharded products built with identical flags and an explicit--end-dateinto a new product. It copies completion markers, so runningpipelineon the merged product is a no-op for the merged days.--consumekeeps peak disk near the output size plus one shard. It moves the first shard into the temporary output and deletes each other shard right after its rows commit.scripts/netflow-db-cluster.shsplits a date range acrosshost:Nslots. It runs each shard detached over plain SSH, copies snapshots back, merges with--consumeby default and runsverify. All hosts must use the same--remote-dirpath.- Compact MAAD tables (#119). This PR is stacked on #119.
address_maad_statsrows are day-owned and merged like other stats.maad_q_gridis shared: every shard must hold identical(ip_version, q_min, q_step, q_count)rows before the merge keeps the first shard's copy. With MAAD disabled, all shards hold an empty grid. The merge checks the table contract withstorage::product_schema()and no longer keeps its own copy. Any table that is neither day-owned nor shared is refused. - Docs: a cluster section in
docs/user/setup-pipeline.md, and "Day-sharded products" indocs/code/pipeline-contract.md.
Review path
Setup: any nfcapd tree with at least three days and a dataset registry entry, e.g. datasets.json.example. Build each shard with identical flags and an explicit --start-date/--end-date.
- Plain merge. Build
a.sqlite(day 1) andb.sqlite(days 2–3), then runnetflow-db merge-shards --output m.sqlite b.sqlite a.sqlite.- Expect a per-table row summary.
netflow-db verify m.sqlite --dataset-id <id> --require-data --require-maad-data --require-processed --require-rollup-parity --require-no-raw-ipprintsOK.- Rerunning
pipelineover days 1–3 onm.sqlitereportsPublished five-minute buckets: 0.
- Refusals. Merge overlapping shards, shards built with a different MAAD setting, or a shard whose
maad_q_gridwas edited (for exampleUPDATE maad_q_grid SET q_step = q_step * 2).- Expect a refusal and no output or temporary file left behind.
- Consuming merge and recovery. Build one shard per day, then make the day-3 shard fail its insert, e.g. with a
BEFORE INSERTtrigger ontraffic_stats. Runmerge-shards --consumewith the shards in the order day 2, day 1, day 3.- The error names the day-1 and day-2 shards as consumed. It keeps a partial
.merge.tmpproduct and prints a resume command. - The partial's
datasets.default_start_dateis day 1. - After removing the trigger, the printed resume command produces a product identical to a single run.
- The error names the day-1 and day-2 shards as consumed. It keeps a partial
- Launcher allocation. Run the launcher with
--hosts nodeA:4,nodeBover 6 days, using real hosts or the fakessh/scpintools/netflow-db/tests/cluster_launcher.rs.- The plan is 5 shards: 2+1+1+1+1 days on nodeA, nodeB, nodeA, nodeA, nodeA.
- Over 3 days the plan is nodeA, nodeB, nodeA.
Decisions and edge cases
Launcher split. The range splits into
min(days, slots)contiguous shards whose lengths differ by at most one day. Slots are ordered round-robin across hosts, so every listed host gets work whenever the range has at least as many days as there are hosts.host:Ngives a host N slots, so leaving:Noff a busier host gives it a lighter share instead of skipping it.default_start_datepolicy. For each dataset:- If every shard stores the same date, it is kept. This covers configured dates.
- Otherwise, every shard must store the date inferred from its own earliest five-minute traffic, or the fallback date if it has no traffic. Any other mismatch is refused.
- The merged product then takes the date of its earliest merged traffic, and the fallback only if the whole product has no traffic. A traffic-less shard's fallback date no longer wins.
- The date is recomputed inside each shard's insert transaction, so a partial product from a failed consuming merge always carries the date of the days it holds, whatever the shard order.
What counts as consumed. A shard counts as consumed the moment its data is inside the temporary output: after the first shard's rename (or cross-filesystem copy and sync), or after a later shard's commit. Any later failure keeps the partial and lists that shard as consumed, so the resume command never re-includes it. This covers a directory sync,
DETACH, or deleting the shard's files. A failed file deletion is reported as its own cleanup error that names the leftover files, which are safe to delete.Failure after publication. If the directory sync fails after the output is renamed into place, the error says the product is published and asks you to run
verify. It does not report the temporary path, which no longer exists.Recovery guarantees, as tested. Tests cover:
- an insert failure midway, with shards in reverse-chronological order;
- a deletion failure right after the first shard's rename;
- a deletion failure right after a later shard's commit.
In each case no data is lost, the partial holds exactly the consumed shards, and resuming produces a product identical to a single run. Durability across a crash or power loss rests on SQLite's rollback journal with
synchronous=FULLand explicit file and directory syncs, and is not tested. The post-publication sync failure is not covered by a test.The nfdump path is part of product identity, so every host installs it under the same absolute
--remote-dir.processed_inputs.file_devicediffers per node on network filesystems, and resume decisions ignore it.A launcher rerun re-copies every shard from the hosts and deletes a stale partial merge first. Remote shards survive an SSH drop or a launcher interrupt, and a rerun reattaches to shards that are still running.
Verification
- Automated:
bun run format,bun run lint,bun run typecheck,bun run test:db, andcargo fmt --all --check.cargo clippy --workspace --all-targets --all-features --locked -- -D warnings.- Merge tests:
- equality with a single run on dual-stack traffic, where every IPv4 and IPv6
addresses/packets/bytesrow stores blob curves and 2 grid rows, plusverifyand resume as a no-op; - refusal when a
maad_q_gridq_min,q_step, orq_countdiffers or a row is missing, and a MAAD-disabled merge with empty grid and MAAD tables; - refusals for identity, overlap, out-of-range rows, CSV, schema, missing zero-fill, and mismatched configured start dates;
- temp cleanup on a write failure, consuming success, the three recovery cases above, and a traffic-less shard not setting the start date.
- equality with a single run on dual-stack traffic, where every IPv4 and IPv6
- Launcher tests with fake
ssh/scp:--consumeby default and off with--keep-shards, plus shard allocation fornodeA:4,nodeBover 6 and 3 days andnodeA,nodeBover 7 days.
- Real data, from before the review fixes: 3 production days sharded across 3 nodes matched a single-process run table by table (row counts and SHA-256), apart from
file_device. A 2-node launcher run built, merged and verifiedOK, and survived a mid-run TERM and rerun. - Remaining manual checks:
- one launcher run on the cluster with an uneven host list, confirming every host receives a shard;
- the first production-scale consuming merge.
Made by Claude Opus 5.5 via Claude Code.
Checks
- SuccessfulPlaywright E2EGitHub
- SuccessfulWeb ChecksGitHub
- SuccessfulRust Pipeline ChecksGitHub
Added 11 commits
- fix(scripts): spread launcher shards across every slot and host0e76fc7
- feat(netflow-db): add a consuming merge mode that bounds peak disk1bb81c5
- feat(scripts): consume local shard copies during the launcher merge248b865
- fix(netflow-db): track consumed shards and reconcile start dates during merges3d8a26a
- fix(netflow-db): keep the merge error when temporary cleanup cannot resolve paths4b2cf88
- refactor(netflow-db): merge shards one attachment at a time and require full marker coverage612b092
- feat(netflow-db): add merge-shards for day-sharded products7d7a151
- fix(scripts): run cluster shards detached and name them by day range9dc43a9
- fix(scripts): deploy cluster files atomically and cap launcher slotse9db5e3
- feat(scripts): add ssh launcher for day-sharded pipeline runsf7e37d8
- fix(netflow-db): merge compact MAAD tables and require matching q gridsfd760d7