Routing and manifests
The hard problem in a network without a coordinator is not executing a query. It is deciding who to ask. Gnarl solves it with manifests.
What a manifest is
A manifest is a small, signed document that describes what a node can answer — never what it holds. It is the only thing that crosses the network before a query does.
{
"index": "imagery",
"node": "4f2c9a7e1b83d05c6ea41f9027bd3e58c1740a9f6b2d85e3c09a7f41d62be8b7",
"version": 7,
"doc_count": 48219,
"fields": ["title", "captured_at", "sensor"],
"summary": {
"time_range": ["2019-03-01T00:00:00Z", "2026-07-28T00:00:00Z"],
"bbox": [-124.7, 24.5, -66.9, 49.4],
"sensor": ["landsat-8", "sentinel-2", "planetscope"],
"terms": "bloom:8kb:base64…"
},
"signature": "ed25519:9c41…2f7e"
}
Typical size is a few kilobytes regardless of corpus size — a 50-million-document node advertises no more than a 50-thousand-document one.
Elimination, not lookup
Planning is a process of provable elimination. The planner never asks "who has this document?" It asks "who can I rule out?"
| Predicate in query | Manifest field consulted | Eliminates |
|---|---|---|
captured_at > 2024 | summary.time_range | Nodes whose newest document predates 2024 |
within(bbox) | summary.bbox | Nodes with no overlapping footprint |
sensor = "landsat-8" | summary.sensor | Nodes that have never seen that sensor |
match("glacier") | summary.terms (Bloom filter) | Nodes whose vocabulary lacks the term |
| any | policy | Nodes that would refuse you anyway |
Whatever survives elimination gets asked. False positives cost one wasted request; false negatives are impossible, because every summary is conservative by construction — a Bloom filter may say "maybe" but never a wrongful "no".
Fan-out control
Asking everyone is correct but not always affordable. Queries carry a budget.
The budget is not something a caller sets today. The planner ranks surviving candidates by expected contribution — manifest match strength, recent latency, and recent success rate — asks the best ones, and reports what it got:
"coverage": {
"expected_claims": 4,
"served_claims": 3,
"skipped_claims": ["…"]
},
"partial": true
That is the contract: a partial answer is always visibly partial. When partial
is not acceptable, say so and let the query fail instead —
gnarl search "…" --require-complete, which exits non-zero rather than handing
back less than you asked for.
Manifest propagation
Manifests gossip. A node pushes a new manifest version to a random subset of peers, which forward it onward until the network converges — typically within a few seconds on networks of hundreds of nodes.
Versions are monotonic and signed, so a stale manifest can never overwrite a fresh one, and a peer cannot forge a manifest on someone else's behalf.
Tuning
There is no config file, and most of routing is not a dial you turn. The one exposed knob is how often a node reconciles with its peers:
gnarl start --check-in-interval-secs 5 # 60 on an --edge peer
Shorter means faster convergence and more chatter; longer is why a phone on battery is not the same cost as a rack.