sightingdb 0.7.2

A database designed for Sightings, a technique to count items
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
Storage, and running more than one of them
==========================================

Notes towards a clustered SightingDB: a galaxy of servers behind an entry point,
each holding some or all of the namespaces, syncing with each other over the
same HTTP API a client uses.

Some of this is built. Each section says which, because the difference matters:
what is built can be relied on, and what is not is still a choice.

The measurements quoted below come from doc/sharding-experiment.py:

    python3 doc/sharding-experiment.py


Storage today
-------------

Default storage is a simple tree which contains the path + value. The path is
usually the type of value, such as a host, an IP address, a hash etc.

Two properties of it decide everything that follows.

**One file per top-level namespace.** persistence::shard_of() takes
'feeds/misp/ips' to 'feeds', and that is the unit written out, paged in,
evicted and tiered. Namespaces beginning with '_' share one internal shard.

Because the namespace is already the storage unit, a galaxy that divides work
by namespace keeps every namespace-scoped operation local: listing a namespace,
counting it, deleting it, exporting it as STIX. A galaxy that divided by value
would turn each of those into a scatter-gather. That is why namespace mirroring
is the model here.

**A sighting is a set of merge-safe operations.** This is what makes clustering
possible without a consensus protocol, and it is already true:

    count       += 1                     a counter
    first_seen  only ever moves back     a min register
    last_seen   only ever moves forward  a max register
    tags        union, de-duplicated     a grow-only set
    ttl         assignment               a last-write-wins register
    stats       += 1 per hour bucket     a map of counters

Every one of those converges whatever order it is applied in. Two servers can
take writes for the same value independently and merge afterwards without
agreeing on anything first. No Raft, no quorum on the write path.

The exception was `count`, since an increment is not idempotent. That is now
kept per node and summed — see "Counters must be per node", which was the
prerequisite everything else waited on.


What a server is
----------------

    BUILT.

A server has a storage set and a peer set, both optional. "Node" and "load
balancer" are not types — they are what those two settings add up to:

    [storage]
    namespaces = ["/"]              everything: a full mirror (the default)
    namespaces = ["feeds"]          that subtree and nothing else
    namespaces = []                 nothing of its own: a pure router

    [galaxy]
    max_hops = 4
    health_interval = 30
    peers = [
      { url = "https://node-a:9999", key = "..." },
      { url = "https://lb2:9999",    key = "..." },
    ]

    stores something, no peers    node
    stores nothing, has peers     router
    stores something, has peers   both

Any server can be any of them, and a router's peer may itself be a router, so a
router in front of routers is a cascade. /_management/api/info reports which
under `role`, and /_management/api/galaxy reports the peers with their health.

Prefixes match whole path segments, so `feeds` holds 'feeds/misp/ips' and never
'feeds-internal'. The internal namespaces are always held whatever the setting
says — a server cannot keep its own consensus tally otherwise — so listing one
is an error.

A write to a namespace a server does not store answers 421 Misdirected Request.
Not 403, which would say the caller may not; not 404, which would say there is
no such thing. The request arrived somewhere that cannot serve it, and once
forwarding exists that is what a router answers when no peer holds it either.
It has to stay distinguishable from a real 404 to be debuggable.

**The peer key is the capability bound.** Each peer carries the key this server
authenticates to it with, and the peer's own ACL decides what that key may
reach. A key granted `rw:feeds` on the peer cannot write anywhere else, however
this server is configured or compromised. This is the existing ACL doing the
work, not a new authorization model, and it is the reason a galaxy needs no
trust between members beyond what each key allows. Give each one the narrowest
grant that does the job.

Misconfiguration is refused at startup rather than at the first forward: a peer
without a key, a url without a scheme, the same peer twice, max_hops of 0.


Health
------

    BUILT.

Each peer's /health is probed every `health_interval` seconds and the result
kept: online, consecutive failures, last successful probe, round trip, and the
peer's own version — a galaxy running mixed versions is worth seeing before it
starts behaving oddly.

/health needs no API key, so a probe carries no credential.

Three details that matter more than they look:

 * "Not probed yet" is reported separately from "offline". A view that conflates
   them says a galaxy is down for the first few seconds of its life.

 * `last_seen` survives failure, so an operator can tell a peer that died a
   minute ago from one that has been gone a week. Latency is cleared, because a
   stale latency is a lie.

 * What is reported is reachability *from this server*. A peer that is running
   but unreachable from here is, for this server's purposes, down, and saying so
   is more useful than saying it is up.

`verify_tls = false` exists for a galaxy of self-signed instances, which is what
--setup produces. It is on by default.


Consensus
---------

    BUILT. A server in front of a galaxy keeps the galaxy-wide tally, read
    back with /r/_all?val=, and rebuilds it from the nodes on a timer.

Consensus means "how many namespaces have seen this value". Each server keeps
its own tally in `_all`, which is a local fact: the namespaces *on this server*
that hold the value.

**The entry point is the right place for the global number**, because it sees
the logical write — (namespace, value) — while mirroring is a physical detail
below it. Its view is de-duplicated by construction, and it needs no polling, so
a peer going away does not make consensus drop for a value that still exists.

A router must therefore keep an `_all` of its own, which is why a storage set of
`[]` still holds the internal namespaces.

And because `_all` is an ordinary readable namespace, the global number needs no
new endpoint:

    GET /r/feeds/ips?val=V      the serving node's local consensus
    GET /r/_all?val=V           on a router: the global one

Leaving /r alone like this keeps a router a thin proxy for reads. The
alternative — rewriting the `consensus` field in every forwarded response —
couples it to the response schema and costs CPU per read.

**No internal namespace is writable from outside.** `_all`, `_shadow/*` and
`_config` answer 403 to every write route including /d. Before that guard a
client could write `_all` directly and give a value a consensus no namespace
supported — the one number this database exists to be trusted about. Reading
them stays open apart from `_config`.

Write responses carry `new`: whether the sighting was the first of that value
in that namespace, which is what gates a consensus increment and which a server
in front cannot work out without holding the namespace. It counts the value once
when the mirror that took the write reports new, however many mirrors took it.

Measured on a router over two *partial* mirrors, where neither node can know the
global number alone:

    1.2.3.4 written into alpha/one, alpha/two (na) and beta/one (nb)
      lb 3   na 2   nb 1          lb is the only one that is right

**And rebuilt, because that tally only ever rises.** Consensus is released when
a namespace is deleted and when the sweeper reclaims an expired value, both on
the node and both invisible from in front of it:

    delete alpha/two on na
      lb 3   na 1   nb 1          truth is 2; lb has drifted
    after a reconciliation pass
      lb 2   na 1   nb 1

A pass surveys the galaxy: for each namespace, walk its values and record which
namespaces hold each one. Consensus is then the size of that set — a set, not a
sum, because the same namespace mirrored three times is still one namespace. A
value the survey never sees at all is set to nothing: it has gone from
everywhere.

It **walks the galaxy**, which is why `reconcile_interval` is a setting of its
own with a long default of an hour. It is a repair, not a steady cost: the
incremental tally answers reads in between, and a pass that finds nothing wrong
says nothing.

**Parallel routers cannot share consensus.** Two entry points each see a
fraction of the writes, and a count cannot be reconciled — you cannot tell a
counter it has already counted something. So root-level redundancy costs either
an active/standby router that rebuilds its tally from the nodes on promotion, or
a router that stores presence pairs (value -> set of namespaces) instead of a
count, because sets union idempotently. Data availability is unaffected either
way; this is only about the global number.


A forwarded write is counted for its origin
------------------------------------------

    BUILT. The subtlety that only appears once fan-out and syncing both work,
    and it cost a round of measurement to find.

Fanning one write out to two mirrors has each mirror count it — and with
per-node counters, each counts it under *its own* name. The two then disagree
about nothing and yet merging them adds the counts together. Measured on a real
cascade: three writes through a load balancer became six once the mirrors
synced.

So the origin travels with the write, in X-SightingDB-Origin, and every mirror
counts it under the same name. Merging mirrors that already agree is then the
no-op it should be:

    client writes 3 times to lb1 (lb1 -> lb2 -> two mirrors)
      node-a {lb1: 3}   node-b {lb1: 3}
    mirrors sync with each other, three times over
      node-a 3          node-b 3

In a cascade the header is passed along unchanged, so the attribution is the
entry point the client actually talked to — lb1 above, not lb2.

Honoured only on a request that also carries a hop count, which is to say only
on a forwarded one: the origin is part of the forwarding protocol, not the
client API. A client setting it anyway would mislabel its own writes without
changing any total, since one write is still one increment.


Counters must be per node
-------------------------

    BUILT. `count` and the hourly buckets are kept per node, the snapshot
    format is version 2, and version 1 is migrated on load. What is still
    missing is the merge verb below.

`count` is an increment, and increments are not idempotent. Over HTTP, where
/w is the only write verb and it means "add one", any sync that reads a peer and
writes what it finds compounds. Measured against two real instances:

    client wrote 3 times.   A=3  B=3     correct
    A syncs from B:         A=6  B=3     A doubled
    B syncs back from A:    A=6  B=9

Three sightings, two sync rounds, and the cluster claims nine. It is unbounded.

The fix, now in place, is a map from node id to count, reported as the sum:

    counts:     node id -> count                 reported as the sum
    node_stats: node id -> hour -> count         reported merged

Each server only ever writes its own entry, so replication becomes "send my
entries", which is idempotent and order-independent. Releasing consensus takes
from the releasing server's own entry, so a server gives back only what it put
in and never spends a peer's contribution.

`node_id` in `[daemon]` names the server, defaulting to "local" so a standalone
one sums to exactly what it always did.

The verb that goes with it is POST /_api/merge, which combines rather than
increments, and GET /r/<ns>?val=V&for_merge, which answers in the shape that
route takes. Both are built. The read side matters as much as the write side:
offering a *total* rather than per-node counts would make the receiver
attribute every server's sightings to the sender, and two servers exchanging
totals inflate each other without bound. That was found by writing the sync and
watching it do exactly that.

Reading a version 1 snapshot folds its totals into the reading server's entry,
which is the only thing they can be. check_version was changed to accept older
versions rather than refuse them: a build that would not open the existing
database is indistinguishable from total data loss, and the next save would
make it real. A *newer* version is still refused.


A node that was down, and comes back
------------------------------------

    BUILT. A worker on a timer, and the state that withholds reads while it
    runs. A galaxy heals itself rather than needing someone with curl.

Measured on a real galaxy. node-b is killed, writes continue through the
router, node-b is restarted and nobody touches it:

    nb down      1.2.3.4=3/-  9.9.9.9=4/-  7.7.7.7=2/-
    +3s          1.2.3.4=3/3  9.9.9.9=4/4  7.7.7.7=2/2
    +40s         1.2.3.4=3/3  9.9.9.9=4/4  7.7.7.7=2/2

    nb's log: Caught up: 2 namespace(s) checked, 2 value(s) taken from peers

7.7.7.7 is in a namespace node-b had never heard of, created while it was
down — found through /_api/namespaces, which exists for exactly that. Writes
kept succeeding on the surviving mirror throughout, so the outage cost
availability on one server and nothing else. Forty seconds of five-second
passes after convergence moved nothing.

**How a pass works.** For each peer: ask what namespaces it holds, keep the
ones this server stores, and for each walk /r/<ns>?for_merge in pages,
applying each page in-process. The first pass after startup is a full one,
because a server that has just started has no idea what it missed. Later
passes use ?count as a trigger and skip a namespace whose peer has no more
values than we do — O(1) on both sides, which is what makes it usable on a
timer. That is a heuristic, not a proof: equal counts do not mean equal
contents. The full pass on startup is what stops the heuristic being
load-bearing.

**The Joining state.** `catching_up` is reported on /health and set from
startup until the first full pass finishes. A router reads it and sends reads
to a mirror that is current instead, because a mirror still catching up would
under-report. Writes go to it throughout — they land directly and the pass
fills in the history behind them. If *every* mirror is catching up one answers
anyway: an under-reported count beats no answer, and refusing would make a
whole galaxy unreadable for as long as it took to start.

`sync_interval` in [galaxy] sets the period; 0 switches catching up off.

The obvious design is for the entry point to buffer what a dead node missed and
push it on recovery. That is the wrong mechanism, for two reasons:

 * The buffer is unbounded. A node down for an hour at ten thousand writes a
   second is thirty-six million entries.

 * There is no durable log anywhere in this system — snapshots only, no WAL. If
   the router restarts while a node is away, the record of what was missed is
   gone, and that node is permanently behind with nothing to say so.

**The surviving mirror is the log.** Catch-up is not replay, it is "pull the
delta from a peer that holds this namespace". Nothing unbounded, survives a
router restart, and works even when a different router saw the writes. This is
also the peer-to-peer sync a galaxy wants anyway.

**Write to a recovering node; withhold reads from it.** Stopping writes while it
catches up means the target keeps moving: under continuous load the sync has no
point at which it provably finishes. Writing to it during catch-up is both
correct and terminating, because new writes land directly while sync backfills
the history behind them, and per-node counters make the two unable to conflict.

What must be withheld is reads, because its data is genuinely incomplete and a
read would under-report:

    state       writes      reads
    Joining     accepted    not served
    Live        accepted    served
    Down        -           -

This is the opposite way round from the intuitive version, and it is the only
version that terminates.

**Never acknowledge a write that reached no holder.** If the entry point acks
when every mirror of a namespace is down, the write exists nowhere and no later
sync can recover it. This is the durability contract, and it is what makes
"the mirror is the log" sound rather than hopeful.

**Knowing how far behind.** There is no sequence number anywhere, so a
recovering node either always syncs on startup or compares itself against a
peer. /r/<namespace>?count is the cheap probe — it is O(1), and its `exact`
flag already says when TTLs make the comparison approximate.

Equal counts do *not* prove convergence: two servers can hold a hundred values
each that the other lacks and report the same total. Count is a good progress
indicator and is what a sync percentage in the interface should use. "Done"
needs a digest or a Merkle root over values. Those are different questions and a
sync that declares itself finished on count equality is quietly wrong.

**Expiry during an outage looks after itself.** A node returning after a week
holds values whose TTL expired while it was away, and may sync them to a peer
that already swept them. It does not matter: expiry is derived from
last_seen + ttl, so a resurrected value is immediately invisible and goes on the
next sweep. Keep catch-up pull-only and this needs no special handling.


Cascading and forwarding
------------------------

    BUILT. A client talks to the entry point; writes fan out to every mirror
    that holds the namespace, reads come from one, and a cycle dies on the hop
    limit.

A router's peer may be a router. Three rules a cascade needs that a single
level does not:

 * **A hop limit, which is not optional.** A diamond or a miswired cycle makes
   writes circulate and counts inflate without bound. max_hops travels in
   X-SightingDB-Hops, is spent on each hop and refused at zero, answering 508.
   Measured on two routers pointing at each other: the write collapses in
   0.05s rather than circulating.

 * **Writes fan out; reads pick one path.** Fanning reads multiplies cost for
   nothing. The exception is namespace-scoped reads — listing, ?count, STIX
   export — which must gather from every peer holding the namespace. Keep that
   list short and deliberate.

 * **Fan-out multiplies with depth.** A root to three routers to three nodes
   each is nine physical writes per client write. Two levels is usually plenty.

**Reads should be sticky per value.** Mirrors diverge transiently under async
sync, so two consecutive reads served by different mirrors can show a count
going *down*. Picking the mirror by rendezvous hashing over the mirror set for
that value fixes it: same value, same mirror, monotonic. This is the one place
rendezvous hashing belongs here — choosing among the servers that already hold a
namespace, not deciding where data lives.

Two things the measurements say about that choice, both in
doc/sharding-experiment.py:

 * Select by rendezvous hashing, not by modulo. mod-n renumbers nearly
   everything when the mirror set changes: 88.9% of keys move going from eight
   mirrors to nine, against the 11.1% minimum. Rendezvous moves the minimum.

 * The hash needs real avalanche. FNV-1a spreads values perfectly well but
   makes a poor rendezvous weight — appending ':n0' / ':n1' perturbs it in a
   structured way and the comparison between servers becomes correlated, moving
   50% where the minimum is 11%. Use SHA-256, already a dependency. Never
   std's DefaultHasher: it is not stable across Rust releases, and a toolchain
   upgrade would silently repoint every read.


Shadow sightings belong at the entry point
------------------------------------------

    BUILT.

Every read records a shadow sighting, so there is no such thing as a read-only
replica and serving more clients roughly doubles write load.

A router is the right place for them, because it is where the client actually
is. It records the shadow locally and forwards the read with `noshadow`, so a
search is counted once, at the entry point, instead of once on whichever mirror
happened to serve it. Nodes stop writing on read entirely, which is what keeps
serving more clients from multiplying write load across a galaxy.

Measured: five reads through a router, with the value held on both mirrors.

    shadow on lb (entry point)  5
    shadow on node-a            0
    shadow on node-b            0

A miss is recorded too, as it already was on a single server: the shadow is
written before the read is forwarded, because a search happened either way.

Under a router that stores only internal namespaces this costs nothing extra:
`_shadow/*` is already held.


Keys are gossiped, not shared
-----------------------------

    BUILT, with one limit worth knowing — see the warning below.

acl_file is rewritten by whichever server served the management request, so
saving a key on one used to leave the others ignorant of it. It is now passed
on to the peers, through each peer's own management interface — which means
**the peer's ACL decides whether the change is allowed.**

That is what makes gossip safe here rather than needing a shared file and the
trust that goes with one. A server can only administer a peer it holds an
`admin` key for; a server holding a narrow `rw:feeds` key there cannot change
anything, and gossip therefore flows from whoever holds the credentials towards
the servers they administer. A narrowly-scoped peer cannot push back.

Measured. A router with an admin key on node-a and only `rw` on node-b:

    save 'analyst' on the router
      router  [analyst, changeme]
      node-a  [analyst, changeme, lb-admin]     took it
      node-b  [changeme, lb-narrow]             not ours to administer

    the key then works on node-a exactly as granted
      /r with 'analyst'  404   accepted, value absent
      /w with 'analyst'  403   read-only, as granted
      node-b             403   never heard of it

Safe to repeat, which is what lets it be retried and re-offered: saving a key
*sets* its grants rather than adding to them, so the same change arriving twice
is the same as once. Unlike a sighting, where exactly that was the difficulty.

A change is pushed as it happens, and this server's keys are offered again
every `gossip_interval` so a peer that was down for a change picks it up:

    node-a down, 'added-while-down' saved on the router
      node-a  down
    node-a back, nobody touches it
      +12s  node-a  [added-while-down, changeme, lb-admin]

A change that arrived *as* gossip is passed along with one hop less, so a
cascade propagates and a cycle dies on the same budget a forwarded write does.

**The limit, which is security relevant.** The periodic offer is additive: it
never deletes a key a peer has and this server does not, because this server is
not necessarily the only place keys are managed and a timer that quietly revoked
a key somebody added elsewhere would be far worse than one that failed to
propagate a deletion. The consequence, confirmed by running it:

    node-a down, 'added-while-down' revoked on the router
      router  [changeme]
      node-a  [added-while-down, changeme, lb-admin]   still live, 18s later

**A revocation made while a server was offline does not reach it.** Repeat the
revocation once it is back, or check that server's own key list. Making this
authoritative instead — peers mirroring one server's ACL exactly — would need a
server to be declared the owner of the galaxy's keys, and that is a decision for
whoever runs it rather than one to assume.

Tiers travel the same way and under the same rule. A tier has no "unset", only
a different value, so unlike a key it has no deletion to miss: offering what
this server holds leaves a peer with exactly that.

**One place to manage keys, if a galaxy wants one.** The additive offer cannot
carry a revocation to a server that was offline for it. `acl_authority` on the
owner makes its offer the whole list, to be held exactly, which does remove —
and `acl_replaceable` on each server that accepts one is what lets it. Both ends
must agree, so arriving here takes two deliberate settings rather than one, and
a server that has not opted in keeps its own keys with the offer falling back to
additive. Setting both on one server is refused at startup: they would take
turns overwriting each other.

A replace must contain an admin key and the key making the request, or it locks
out either the interface or the server doing the replacing.


Still unsolved
--------------

**Durability.** Snapshots every snapshot_interval, 300s by default, and no WAL,
so a crash loses up to five minutes. Replication can stand in for a log, but
only if a write reaches a peer before it is acknowledged; otherwise every server
independently keeps the same five-minute hole. For sighting counts, replicating
asynchronously and accepting the window is defensible — but it should be a
choice rather than an accident.

**Anti-entropy.** Something has to find and repair divergence between mirrors
that are both up, as opposed to catching up one that was down. The per-shard
snapshot files are a natural unit; a version vector per namespace, or Merkle
trees over values once shards get large.

**Membership.** Peers are configured, not discovered. A galaxy that gains and
loses members by hand is fine to start with, and sticky read routing is only as
consistent as the mirror list every server computes against — so two servers
disagreeing about membership will disagree about which mirror serves a value.


Order of work
-------------

    DONE  1. Roles: storage set, peer set, 421 for what a server does not hold.
    DONE  2. Internal namespaces unwritable, so `_all` cannot be forged.
    DONE  3. Peer health, and the galaxy endpoint a topology view reads.
    DONE  4. Per-node counters and the snapshot format bump.
    DONE  5. The merge verb and its read side, and `new` on write responses.
    DONE  6. Forwarding: writes fan out, reads pick one, the hop limit is
             enforced, and a forwarded write is counted for its origin.
    DONE  7. The galaxy view: one request walks the cascade, and the
             management interface draws it.

    DONE  8. Catch-up on a timer, and the Joining state that withholds reads
             while it runs.

    DONE  9. Shadow sightings at the entry point.
    DONE 10. The consensus tally in front of a galaxy, and reconciling it
             against the nodes.
    DONE 11. Keys gossiped to the peers a server administers.

    DONE 12. Tiers gossiped the same way.
    DONE 13. Authoritative ACL, for a galaxy that wants one server to own its
             keys — which is what makes a revocation reach one that was
             offline.

Steps 4 and 5 were refactors with no distributed systems in them, and they were
where the difficulty lived. What remains is plumbing by comparison — with the
exception of step 7, whose protocol is written down above.


A note on whether to divide the data at all
-------------------------------------------

Dividing namespaces between servers scales data volume. Serving many more
*clients* is a connection and throughput problem, and full mirrors behind an
entry point solve that with no placement logic, no rebalancing and no
scatter-gather — every server answers everything, and clients change nothing.

The merge properties at the top of this document are what make that safe, and it
is a great deal less work than the rest.

So: mirror everything until the data outgrows one server, then narrow each
server's storage set. The measured snapshot limits are what say which ceiling is
close.