@serfab/quereus-plugin-sereus 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +367 -360
- package/dist/cached-storage.d.ts +66 -0
- package/dist/cached-storage.js +103 -0
- package/dist/cached-storage.js.map +1 -0
- package/dist/cluster-size.d.ts +231 -0
- package/dist/cluster-size.js +240 -0
- package/dist/cluster-size.js.map +1 -0
- package/dist/compose-strand.d.ts +8 -2
- package/dist/compose-strand.js +23 -23
- package/dist/compose-strand.js.map +1 -1
- package/dist/connect-browser.js +2 -1
- package/dist/connect-browser.js.map +1 -1
- package/dist/connect.js +2 -1
- package/dist/connect.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parse-config.js +23 -11
- package/dist/parse-config.js.map +1 -1
- package/dist/plugin-browser.js +76684 -75082
- package/dist/plugin-browser.js.map +4 -4
- package/dist/strand-schema.d.ts +1 -1
- package/dist/strand-schema.js +398 -35
- package/dist/strand-schema.js.map +1 -1
- package/dist/types.d.ts +46 -10
- package/package.json +8 -8
- package/src/cached-storage.ts +107 -0
- package/src/cluster-size.ts +248 -0
- package/src/compose-strand.ts +33 -24
- package/src/connect-browser.ts +57 -56
- package/src/connect.ts +38 -37
- package/src/index.ts +10 -1
- package/src/parse-config.ts +70 -55
- package/src/plugin-browser.ts +29 -29
- package/src/plugin.ts +39 -39
- package/src/strand-schema.ts +398 -35
- package/src/types.ts +106 -69
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap an embedder-supplied `IRawStorage` in `@optimystic/db-p2p`'s write-through
|
|
3
|
+
* raw-storage cache (`CachedRawStorage`) before handing it to a node or transactor.
|
|
4
|
+
*
|
|
5
|
+
* Why: bringing a control database or strand up is raw-storage-operation bound —
|
|
6
|
+
* uncached, a cold start re-reads the same handful of blocks ~2000 times (see the
|
|
7
|
+
* budget history in cadre-core's `test/control-start-storage-op-budget.spec.ts` and
|
|
8
|
+
* the sereus tickets `complete/optimystic-block-read-amplification-on-control-start`
|
|
9
|
+
* + `complete/optimystic-schema-catalog-reread-per-write-blows-storage-budgets`).
|
|
10
|
+
* Upstream ships the remedy as an opt-in wrapper — adoption is deliberately the
|
|
11
|
+
* consumer's choice — and this module is where Sereus opts in. It lives in this
|
|
12
|
+
* package (not cadre-core) for the same reason `cluster-size.ts` does: both the SQL
|
|
13
|
+
* plugin's own connectors (`composeStrand`, including the browser/IndexedDB path,
|
|
14
|
+
* where per-operation latency is highest) and cadre-core's control/strand seams
|
|
15
|
+
* need the identical wrap, and two copies could disagree.
|
|
16
|
+
*
|
|
17
|
+
* Soundness rests on the cache's single-process-owner invariant (upstream
|
|
18
|
+
* docs/storage.md §6): every write to the backend must funnel through the wrapped
|
|
19
|
+
* instance. Two rules uphold that:
|
|
20
|
+
*
|
|
21
|
+
* - **One wrap per inner instance** (WeakMap memo): the same `IRawStorage` reaches
|
|
22
|
+
* this function from several seams at once — cadre-core's node wiring and the
|
|
23
|
+
* strand backfill share the instance the node was built with, and the SQL
|
|
24
|
+
* plugin's `composeStrand` re-wraps whatever it is handed. Two live caches over
|
|
25
|
+
* one backend would each miss the other's writes; the memo makes the wrap
|
|
26
|
+
* idempotent instead — a caller wrapping an already-wrapped or already-seen
|
|
27
|
+
* instance gets the same object back.
|
|
28
|
+
* - **A Sereus process owns its data stores** — no second process writes the same
|
|
29
|
+
* backend, which is the invariant's other half.
|
|
30
|
+
*
|
|
31
|
+
* `MemoryRawStorage` is returned unwrapped on upstream's own guidance: the backend
|
|
32
|
+
* is already in-memory, so the cache adds bookkeeping with nothing to save.
|
|
33
|
+
*
|
|
34
|
+
* The memo dedupes *concurrent* seams; it is not what keeps a cache alive across a
|
|
35
|
+
* runtime rebuild. That is cadre-core's job, and it does it by OWNERSHIP: one
|
|
36
|
+
* resolved store per scope per runtime lifetime — a strand's store for the life of
|
|
37
|
+
* its instance (a hibernation quiesce → resume reuses the store already resolved,
|
|
38
|
+
* so the wake lands on a warm cache), the control store for the life of one
|
|
39
|
+
* `CadreNode.start()`. Because the store outlives the rebuild, so does its wrap,
|
|
40
|
+
* whether or not the embedder's provider returns a stable instance.
|
|
41
|
+
*
|
|
42
|
+
* When a scope's runtime stops for good, cadre-core calls {@link disposeStorageCache}
|
|
43
|
+
* — the release half of that ownership. Without it the retired wrapper's
|
|
44
|
+
* `CacheStoreHandle` would stay in the shared pool's registry for the process
|
|
45
|
+
* lifetime (`unregisterStore` is the only removal), one orphan per stop.
|
|
46
|
+
*/
|
|
47
|
+
import { type IRawStorage } from '@optimystic/db-p2p/rn';
|
|
48
|
+
/**
|
|
49
|
+
* The cached view of `storage`, memoized per instance. `label` names the store in
|
|
50
|
+
* the shared pool's `stats()` (e.g. `control`, or the strand id).
|
|
51
|
+
*/
|
|
52
|
+
export declare function wrapStorageWithCache(storage: IRawStorage, label: string): IRawStorage;
|
|
53
|
+
/**
|
|
54
|
+
* Release the cache wrapper {@link wrapStorageWithCache} returned: unregister it from
|
|
55
|
+
* the shared pool and forget the memo, so a later re-wrap of the same inner instance
|
|
56
|
+
* gets a live cache rather than this retired one. A disposed wrapper must never be
|
|
57
|
+
* handed out again — its pool store id is retired and never reused — and providers
|
|
58
|
+
* that return a stable instance per scope (the web reference app's `strand-storage.ts`,
|
|
59
|
+
* the integration harness's `captureRawStorage`) would get exactly that back on a
|
|
60
|
+
* relaunch if the memo were left in place.
|
|
61
|
+
*
|
|
62
|
+
* No-op for anything this module returned unwrapped (e.g. `MemoryRawStorage`), so
|
|
63
|
+
* callers need no `instanceof` test. Only the wrapper is released: the inner store the
|
|
64
|
+
* embedder returned is never closed — that handle stays the embedder's to manage.
|
|
65
|
+
*/
|
|
66
|
+
export declare function disposeStorageCache(storage: IRawStorage): Promise<void>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap an embedder-supplied `IRawStorage` in `@optimystic/db-p2p`'s write-through
|
|
3
|
+
* raw-storage cache (`CachedRawStorage`) before handing it to a node or transactor.
|
|
4
|
+
*
|
|
5
|
+
* Why: bringing a control database or strand up is raw-storage-operation bound —
|
|
6
|
+
* uncached, a cold start re-reads the same handful of blocks ~2000 times (see the
|
|
7
|
+
* budget history in cadre-core's `test/control-start-storage-op-budget.spec.ts` and
|
|
8
|
+
* the sereus tickets `complete/optimystic-block-read-amplification-on-control-start`
|
|
9
|
+
* + `complete/optimystic-schema-catalog-reread-per-write-blows-storage-budgets`).
|
|
10
|
+
* Upstream ships the remedy as an opt-in wrapper — adoption is deliberately the
|
|
11
|
+
* consumer's choice — and this module is where Sereus opts in. It lives in this
|
|
12
|
+
* package (not cadre-core) for the same reason `cluster-size.ts` does: both the SQL
|
|
13
|
+
* plugin's own connectors (`composeStrand`, including the browser/IndexedDB path,
|
|
14
|
+
* where per-operation latency is highest) and cadre-core's control/strand seams
|
|
15
|
+
* need the identical wrap, and two copies could disagree.
|
|
16
|
+
*
|
|
17
|
+
* Soundness rests on the cache's single-process-owner invariant (upstream
|
|
18
|
+
* docs/storage.md §6): every write to the backend must funnel through the wrapped
|
|
19
|
+
* instance. Two rules uphold that:
|
|
20
|
+
*
|
|
21
|
+
* - **One wrap per inner instance** (WeakMap memo): the same `IRawStorage` reaches
|
|
22
|
+
* this function from several seams at once — cadre-core's node wiring and the
|
|
23
|
+
* strand backfill share the instance the node was built with, and the SQL
|
|
24
|
+
* plugin's `composeStrand` re-wraps whatever it is handed. Two live caches over
|
|
25
|
+
* one backend would each miss the other's writes; the memo makes the wrap
|
|
26
|
+
* idempotent instead — a caller wrapping an already-wrapped or already-seen
|
|
27
|
+
* instance gets the same object back.
|
|
28
|
+
* - **A Sereus process owns its data stores** — no second process writes the same
|
|
29
|
+
* backend, which is the invariant's other half.
|
|
30
|
+
*
|
|
31
|
+
* `MemoryRawStorage` is returned unwrapped on upstream's own guidance: the backend
|
|
32
|
+
* is already in-memory, so the cache adds bookkeeping with nothing to save.
|
|
33
|
+
*
|
|
34
|
+
* The memo dedupes *concurrent* seams; it is not what keeps a cache alive across a
|
|
35
|
+
* runtime rebuild. That is cadre-core's job, and it does it by OWNERSHIP: one
|
|
36
|
+
* resolved store per scope per runtime lifetime — a strand's store for the life of
|
|
37
|
+
* its instance (a hibernation quiesce → resume reuses the store already resolved,
|
|
38
|
+
* so the wake lands on a warm cache), the control store for the life of one
|
|
39
|
+
* `CadreNode.start()`. Because the store outlives the rebuild, so does its wrap,
|
|
40
|
+
* whether or not the embedder's provider returns a stable instance.
|
|
41
|
+
*
|
|
42
|
+
* When a scope's runtime stops for good, cadre-core calls {@link disposeStorageCache}
|
|
43
|
+
* — the release half of that ownership. Without it the retired wrapper's
|
|
44
|
+
* `CacheStoreHandle` would stay in the shared pool's registry for the process
|
|
45
|
+
* lifetime (`unregisterStore` is the only removal), one orphan per stop.
|
|
46
|
+
*/
|
|
47
|
+
// Imported from the `/rn` entry — the platform-neutral surface (no Node-only
|
|
48
|
+
// transports) — because `composeStrand` reaches this module on the browser path
|
|
49
|
+
// too. Both db-p2p entrypoints re-export the same storage modules, so class
|
|
50
|
+
// identity (the `instanceof` checks below) is unaffected on Node.
|
|
51
|
+
import { CachedRawStorage, MemoryRawStorage } from '@optimystic/db-p2p/rn';
|
|
52
|
+
const wraps = new WeakMap();
|
|
53
|
+
/**
|
|
54
|
+
* Reverse of `wraps`, so {@link disposeStorageCache} can evict the memo entry it
|
|
55
|
+
* cannot otherwise find — `wraps` is keyed by the inner instance, and dispose is
|
|
56
|
+
* handed the wrapper.
|
|
57
|
+
*/
|
|
58
|
+
const inners = new WeakMap();
|
|
59
|
+
/**
|
|
60
|
+
* The cached view of `storage`, memoized per instance. `label` names the store in
|
|
61
|
+
* the shared pool's `stats()` (e.g. `control`, or the strand id).
|
|
62
|
+
*/
|
|
63
|
+
export function wrapStorageWithCache(storage, label) {
|
|
64
|
+
if (storage instanceof MemoryRawStorage || storage instanceof CachedRawStorage) {
|
|
65
|
+
return storage;
|
|
66
|
+
}
|
|
67
|
+
let wrapped = wraps.get(storage);
|
|
68
|
+
if (!wrapped) {
|
|
69
|
+
wrapped = new CachedRawStorage(storage, undefined, label);
|
|
70
|
+
wraps.set(storage, wrapped);
|
|
71
|
+
inners.set(wrapped, storage);
|
|
72
|
+
}
|
|
73
|
+
return wrapped;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Release the cache wrapper {@link wrapStorageWithCache} returned: unregister it from
|
|
77
|
+
* the shared pool and forget the memo, so a later re-wrap of the same inner instance
|
|
78
|
+
* gets a live cache rather than this retired one. A disposed wrapper must never be
|
|
79
|
+
* handed out again — its pool store id is retired and never reused — and providers
|
|
80
|
+
* that return a stable instance per scope (the web reference app's `strand-storage.ts`,
|
|
81
|
+
* the integration harness's `captureRawStorage`) would get exactly that back on a
|
|
82
|
+
* relaunch if the memo were left in place.
|
|
83
|
+
*
|
|
84
|
+
* No-op for anything this module returned unwrapped (e.g. `MemoryRawStorage`), so
|
|
85
|
+
* callers need no `instanceof` test. Only the wrapper is released: the inner store the
|
|
86
|
+
* embedder returned is never closed — that handle stays the embedder's to manage.
|
|
87
|
+
*/
|
|
88
|
+
export async function disposeStorageCache(storage) {
|
|
89
|
+
if (!(storage instanceof CachedRawStorage)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const inner = inners.get(storage);
|
|
93
|
+
if (inner) {
|
|
94
|
+
inners.delete(storage);
|
|
95
|
+
// Only evict the memo if it still points at THIS wrapper: a re-wrap after an
|
|
96
|
+
// earlier dispose has already installed a live successor we must not drop.
|
|
97
|
+
if (wraps.get(inner) === storage) {
|
|
98
|
+
wraps.delete(inner);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
await storage.dispose();
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=cached-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cached-storage.js","sourceRoot":"","sources":["../src/cached-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,6EAA6E;AAC7E,gFAAgF;AAChF,4EAA4E;AAC5E,kEAAkE;AAClE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAoB,MAAM,uBAAuB,CAAC;AAE7F,MAAM,KAAK,GAAG,IAAI,OAAO,EAAiC,CAAC;AAE3D;;;;GAIG;AACH,MAAM,MAAM,GAAG,IAAI,OAAO,EAAiC,CAAC;AAE5D;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAoB,EAAE,KAAa;IACvE,IAAI,OAAO,YAAY,gBAAgB,IAAI,OAAO,YAAY,gBAAgB,EAAE,CAAC;QAChF,OAAO,OAAO,CAAC;IAChB,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,OAAO,GAAG,IAAI,gBAAgB,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QAC1D,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC5B,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAAoB;IAC7D,IAAI,CAAC,CAAC,OAAO,YAAY,gBAAgB,CAAC,EAAE,CAAC;QAC5C,OAAO;IACR,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,KAAK,EAAE,CAAC;QACX,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACvB,6EAA6E;QAC7E,2EAA2E;QAC3E,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,OAAO,EAAE,CAAC;YAClC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACF,CAAC;IACD,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smallest cluster size Optimystic will honour — its own `minAbsoluteClusterSize`,
|
|
3
|
+
* and the smallest value that reaches the cluster path at all (a lone node writes to
|
|
4
|
+
* local storage without forming a cluster). A validation floor, not a default.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MIN_CLUSTER_SIZE = 2;
|
|
7
|
+
/**
|
|
8
|
+
* How many nodes each block of the **control** (cadre membership) database is replicated
|
|
9
|
+
* to. Chosen to exceed any party's node count, so in practice every control block lands
|
|
10
|
+
* on every member of the party.
|
|
11
|
+
*
|
|
12
|
+
* **Why full replication here.** Every control node reads the whole control database —
|
|
13
|
+
* membership, peer addresses, the strand list — so a member left out of a block's cohort is
|
|
14
|
+
* a member that may never learn the fact, and at a cohort of two the *read repair* that is
|
|
15
|
+
* supposed to make that safe cannot converge (it can ask exactly one peer, and that peer may
|
|
16
|
+
* be the member that also missed the write). Measured: 4 failures in 10 runs of the
|
|
17
|
+
* control-DB replication scenario at size 2, 0 in 20 at size 3 and 0 in 10 at size 8.
|
|
18
|
+
*
|
|
19
|
+
* **Why this number, and why a constant.** Optimystic caps a cohort at the peers that
|
|
20
|
+
* actually serve the network and downsizes a cohort it cannot fill (`allowDownsize: true`,
|
|
21
|
+
* which Cadre passes), so any value at or above the party's node count yields the same
|
|
22
|
+
* behaviour: cohort = whole party. 16 is roughly twice the largest deployment the product
|
|
23
|
+
* documents (`docs/architecture.md` → "Enterprise (Multi-Node Mixed)", 7 nodes) — headroom
|
|
24
|
+
* without pretending to support arbitrarily large parties; a party genuinely running more
|
|
25
|
+
* than 16 nodes is back to partial replication for its control database and should raise
|
|
26
|
+
* this. It is a constant rather than the live member count because Optimystic freezes the
|
|
27
|
+
* value at libp2p-node construction, before the `ControlDatabase` holding the `CadrePeer`
|
|
28
|
+
* rows exists, and because a per-node derivation lets two members disagree.
|
|
29
|
+
*
|
|
30
|
+
* A cadre is one party by construction, so the owner-blind cohort-selection question that
|
|
31
|
+
* applies to strand networks (see {@link DEFAULT_STRAND_CLUSTER_SIZE} below) is not a question
|
|
32
|
+
* here: "replicate to the whole party" already is the whole intent, and there is no party
|
|
33
|
+
* diversity for this breadth to buy or fail to buy.
|
|
34
|
+
*
|
|
35
|
+
* Canonical explanation of all of the above — the read-repair mechanics, what whole-party
|
|
36
|
+
* breadth does and does not remove, and what the wider cohort costs in write availability —
|
|
37
|
+
* is `docs/architecture.md` → "Replication cluster size". Keep it there, not here.
|
|
38
|
+
*
|
|
39
|
+
* **Deliberately not `clusterPolicy.assumedClusterSize`.** That option means "the smallest
|
|
40
|
+
* cohort this deployment can genuinely field", and it feeds both the membership admission
|
|
41
|
+
* gate's low-confidence path *and* the read-repair/reconcile corroboration floor. A Cadre
|
|
42
|
+
* party legitimately runs one or two nodes, so Cadre leaves it at Optimystic's default of 2;
|
|
43
|
+
* asserting 16 there would make the admission gate demand `ceil(0.75 x 16) = 12` declared
|
|
44
|
+
* peers and refuse every real party's writes.
|
|
45
|
+
*
|
|
46
|
+
* NOTE: cohort selection overfetches proportionally to this number —
|
|
47
|
+
* `Libp2pKeyPeerNetwork.membershipOverfetch` asks FRET for
|
|
48
|
+
* `max(clusterSize * 4, clusterSize + 16)` candidates and does a peerStore protocol lookup
|
|
49
|
+
* per candidate, so 16 requests a 64-peer proximity band. Free today, because the result is
|
|
50
|
+
* bounded by the peers FRET actually knows (2 in a 3-node party). If control-network cohort
|
|
51
|
+
* selection ever shows up as slow on a large mesh, look here first.
|
|
52
|
+
*/
|
|
53
|
+
export declare const CONTROL_REPLICATION_BREADTH = 16;
|
|
54
|
+
/**
|
|
55
|
+
* Cluster consensus policy every CONTROL-network libp2p node runs — production
|
|
56
|
+
* (`CadreNode.buildControlNodeOptions`) and the integration harness alike. One definition so
|
|
57
|
+
* the two cannot drift: the object literal used to be hand-copied at both sites, and the copy
|
|
58
|
+
* at the harness had picked up a `superMajorityThreshold` override production never had.
|
|
59
|
+
*
|
|
60
|
+
* `superMajorityThreshold` is deliberately ABSENT. Omitting it makes both the cluster member
|
|
61
|
+
* and the coordinator fall back to Optimystic's `DEFAULT_SUPER_MAJORITY_THRESHOLD` (0.75) —
|
|
62
|
+
* the bar a real party commits control writes against. Setting it here, or at any one
|
|
63
|
+
* consumer, reintroduces exactly the divergence this constant exists to prevent. The absence
|
|
64
|
+
* is a researched decision, not an inherited default: no threshold both relaxes the
|
|
65
|
+
* three-node unanimity bar and satisfies Optimystic's partition-safety condition at the
|
|
66
|
+
* shipped admission fraction — see `docs/architecture.md` → "Replication cluster size".
|
|
67
|
+
*
|
|
68
|
+
* `sizeTolerance` only makes sense alongside `allowDownsize`: a fixed
|
|
69
|
+
* {@link CONTROL_REPLICATION_BREADTH}-wide target is unsatisfiable by any real (2-7 node)
|
|
70
|
+
* party, so the cohort must be allowed to shrink to the party that actually exists.
|
|
71
|
+
*
|
|
72
|
+
* NOT for strand networks: {@link STRAND_CLUSTER_POLICY} is a structurally identical object
|
|
73
|
+
* for a different network with different reasoning. The shape match is a coincidence; keep
|
|
74
|
+
* them separate.
|
|
75
|
+
*
|
|
76
|
+
* `assumedClusterSize` is REQUIRED here, and 2 is not a guess — it is the smallest party we
|
|
77
|
+
* support. The field feeds two Optimystic consumers with opposite failure modes, and only one
|
|
78
|
+
* of them defaults to something workable for us:
|
|
79
|
+
*
|
|
80
|
+
* - The membership admission gate already defaults to 2 (`minAbsoluteClusterSize`), so naming
|
|
81
|
+
* it changes nothing there.
|
|
82
|
+
* - The read-repair / reconcile corroboration floor does NOT. Absent this field it falls back
|
|
83
|
+
* to `clusterSize` — which for the control network is {@link CONTROL_REPLICATION_BREADTH},
|
|
84
|
+
* 16. `corroboratorCapacity(peers, 16)` is then `max(peers, 15)`, so the floor of two
|
|
85
|
+
* distinct non-self corroborators binds. A two-node party fields exactly one, can never
|
|
86
|
+
* reach two, and so can never repair a block: `cluster-fetch:no-quorum` forever.
|
|
87
|
+
*
|
|
88
|
+
* Declaring 2 does NOT lower the replication factor — that stays at
|
|
89
|
+
* {@link CONTROL_REPLICATION_BREADTH}. It states the cohort size we can genuinely field, which
|
|
90
|
+
* is what the corroboration floor is asking. See Optimystic's `cluster/cluster-policy.ts`
|
|
91
|
+
* ("Why two size yardsticks, not one") and `corroboratorCapacity` in `cluster/quorum-restore.ts`.
|
|
92
|
+
*
|
|
93
|
+
* The `satisfies` is load-bearing: without it a mistyped or obsolete key would compile at both
|
|
94
|
+
* this definition and every consumer, because TypeScript only excess-property-checks fresh
|
|
95
|
+
* object literals, not a shared constant handed to `clusterPolicy`.
|
|
96
|
+
*/
|
|
97
|
+
export declare const CONTROL_CLUSTER_POLICY: Readonly<{
|
|
98
|
+
allowDownsize: true;
|
|
99
|
+
sizeTolerance: number;
|
|
100
|
+
assumedClusterSize: number;
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* Default number of nodes a **strand** network is told its replication cluster should have.
|
|
104
|
+
*
|
|
105
|
+
* Strand data is application data, where partial replication is a legitimate choice: only
|
|
106
|
+
* the control database's every-member-reads-all-of-it character forces full replication
|
|
107
|
+
* ({@link CONTROL_REPLICATION_BREADTH}). So the question here is not "everyone" but "how
|
|
108
|
+
* few is too few", and the answer is four.
|
|
109
|
+
*
|
|
110
|
+
* **Why four.** A write commits when a super-majority of the cohort approves, and that bar is
|
|
111
|
+
* Optimystic's `DEFAULT_SUPER_MAJORITY_THRESHOLD` of 0.75 — which Cadre selects by naming no
|
|
112
|
+
* threshold at all (see {@link CONTROL_CLUSTER_POLICY}). Approvals needed is
|
|
113
|
+
* `ceil(cohort x 0.75)`, so:
|
|
114
|
+
*
|
|
115
|
+
* | copies | approvals needed | holders that may be offline |
|
|
116
|
+
* |--------|------------------|-----------------------------|
|
|
117
|
+
* | 2 | 2 | 0 |
|
|
118
|
+
* | 3 | 3 | 0 |
|
|
119
|
+
* | 4 | 3 | 1 |
|
|
120
|
+
* | 5 | 4 | 1 |
|
|
121
|
+
* | 6 | 5 | 1 |
|
|
122
|
+
*
|
|
123
|
+
* Four is the smallest breadth that lets a write commit while one holder is away. At two or
|
|
124
|
+
* three every holder must be awake for every write, which for a workspace shared between
|
|
125
|
+
* phones and laptops is the ordinary case, not the rare one. Six buys no more fault tolerance
|
|
126
|
+
* than four and costs more overfetch (see the NOTE below).
|
|
127
|
+
*
|
|
128
|
+
* **It is also a correctness floor, not only a durability one — but the floor is measured in
|
|
129
|
+
* machines, not in this number.** A node that has fallen behind and can ask exactly one peer
|
|
130
|
+
* whether it is current has Optimystic accept that single answer as the cluster's truth
|
|
131
|
+
* (`corroboratorCapacity` in `db-p2p/src/cluster/quorum-restore.ts` lowers the corroboration
|
|
132
|
+
* floor to one when the cohort cannot hold a second voter). If that peer is also behind it
|
|
133
|
+
* honestly reports the stale revision, the reader concludes it is current, and re-arms its
|
|
134
|
+
* repair window forever. Measured on the control-DB replication scenario: 4 failures in 10 runs
|
|
135
|
+
* at breadth 2, 0 in 20 at breadth 3, 0 in 10 at breadth 8. What lifts the floor off a single
|
|
136
|
+
* voter is a *third machine*, not a wider target: `corroboratorCapacity` takes the cohort peers
|
|
137
|
+
* actually present against {@link STRAND_CLUSTER_POLICY}'s declared `assumedClusterSize`, so a
|
|
138
|
+
* three-machine strand needs two corroborators whatever this number says, and a two-machine one
|
|
139
|
+
* cannot supply them however wide the target is. The underlying Optimystic behaviour is unfixed
|
|
140
|
+
* — `backlog/debt-read-repair-single-voter-corroboration` — so a two-machine strand still takes
|
|
141
|
+
* the exposure. Raising this number does not buy the way out; adding a machine does.
|
|
142
|
+
*
|
|
143
|
+
* **Why not derived from the party or member count.** The strand's `Member` rows live *in* the
|
|
144
|
+
* strand database, which runs on the strand libp2p node, whose cluster size is frozen at
|
|
145
|
+
* construction — reading the list to configure the node needs the node to exist. Open strands
|
|
146
|
+
* have no member list at all. The count also changes in the unsafe direction: a node that
|
|
147
|
+
* restarted after someone joined derives a wider expected cohort than one that did not, and the
|
|
148
|
+
* membership admission gate's confident path is what rejects the *wider* view. And a `Member` is
|
|
149
|
+
* a party, not a machine; cohort width consumes machines. Full reasoning in
|
|
150
|
+
* `docs/architecture.md` → "Replication cluster size".
|
|
151
|
+
*
|
|
152
|
+
* **Four machines, not four parties.** This number counts machines, and today that is not the
|
|
153
|
+
* same as counting independent holders. Cohort selection upstream is owner-blind — it
|
|
154
|
+
* picks nodes by hash proximity with no concept of which peer belongs to which party
|
|
155
|
+
* (`optimystic/tickets/backlog/feat-cohort-selection-owner-aware-placement`) — and on the
|
|
156
|
+
* cadre-driven path, the one that resolves a seed for itself, a strand's libp2p mesh is seeded
|
|
157
|
+
* from exactly one party's own control-database peer rows (`CadreNode.resolveCohortSeed`), so
|
|
158
|
+
* the cohort actually formed is that one party's machines. Four copies are, on that path, four
|
|
159
|
+
* machines belonging to one party — one failure domain, one witness. A cross-party mesh is not
|
|
160
|
+
* forbidden, only undiscoverable: it takes hand-supplied addresses
|
|
161
|
+
* (`StrandConnectionOptions.bootstrapNodes`) or a hand-written dial (see the doc). Tracked:
|
|
162
|
+
* `backlog/feat-strand-party-identity`. Full reasoning: `docs/architecture.md` →
|
|
163
|
+
* "Replication cluster size".
|
|
164
|
+
*
|
|
165
|
+
* Raising or lowering it per strand is still the embedder's call
|
|
166
|
+
* ({@link resolveStrandClusterSize}, `CadreNodeConfig.strandClusterSize`,
|
|
167
|
+
* `StrandConnectionOptions.clusterSize`) — but every node on one strand must agree, so a change
|
|
168
|
+
* means restarting all of them. There is no smaller-than-{@link MIN_CLUSTER_SIZE} option.
|
|
169
|
+
*
|
|
170
|
+
* NOTE: cohort selection overfetches proportionally — `Libp2pKeyPeerNetwork.membershipOverfetch`
|
|
171
|
+
* asks FRET for `max(clusterSize * 4, clusterSize + 16)` candidates and does one peerStore
|
|
172
|
+
* protocol lookup per candidate, so 4 requests a 20-peer band (was 18 at breadth 2). Negligible,
|
|
173
|
+
* and bounded by the peers FRET actually knows. If strand cohort selection ever shows up as slow,
|
|
174
|
+
* look here first.
|
|
175
|
+
*/
|
|
176
|
+
export declare const DEFAULT_STRAND_CLUSTER_SIZE = 4;
|
|
177
|
+
/**
|
|
178
|
+
* Cluster consensus policy every STRAND-network libp2p node runs — production
|
|
179
|
+
* (`StrandInstanceManager.buildStrandRuntime`) and the plugin's own networked e2e mesh alike.
|
|
180
|
+
* One definition so the two cannot drift; the literal used to be hand-copied at both sites.
|
|
181
|
+
*
|
|
182
|
+
* Structurally identical to {@link CONTROL_CLUSTER_POLICY} and deliberately separate from it:
|
|
183
|
+
* that one is the control database's, whose breadth is fixed at
|
|
184
|
+
* {@link CONTROL_REPLICATION_BREADTH} for reasons that do not apply to application data.
|
|
185
|
+
*
|
|
186
|
+
* `superMajorityThreshold` is ABSENT for the same reason it is absent there: omitting it is how
|
|
187
|
+
* both the coordinator and the cluster member select Optimystic's
|
|
188
|
+
* `DEFAULT_SUPER_MAJORITY_THRESHOLD` (0.75), the bar {@link DEFAULT_STRAND_CLUSTER_SIZE}'s
|
|
189
|
+
* copies/approvals table is computed against.
|
|
190
|
+
*
|
|
191
|
+
* `sizeTolerance` only makes sense alongside `allowDownsize`: a {@link
|
|
192
|
+
* DEFAULT_STRAND_CLUSTER_SIZE}-wide target is unsatisfiable by a strand of one, two or three
|
|
193
|
+
* machines, so the cohort must be allowed to shrink to the mesh that actually exists.
|
|
194
|
+
*
|
|
195
|
+
* `assumedClusterSize` is REQUIRED, and {@link MIN_CLUSTER_SIZE} is not a guess — it is the
|
|
196
|
+
* smallest strand that reaches the cluster path at all. Absent this field the read-repair /
|
|
197
|
+
* reconcile corroboration floor falls back to `clusterSize`, so
|
|
198
|
+
* `corroboratorCapacity(peers, 4)` is `max(peers, 3)` and two distinct non-self corroborators
|
|
199
|
+
* become mandatory. A two-machine strand fields exactly one peer, can never reach two, and so
|
|
200
|
+
* can never repair a block — `cluster-fetch:no-quorum` forever, which is a read that never
|
|
201
|
+
* catches up and a `create table` that dies with `Missing block` on the joining node. Declaring
|
|
202
|
+
* the honest floor does NOT lower the replication factor (that stays at
|
|
203
|
+
* {@link DEFAULT_STRAND_CLUSTER_SIZE}) and does not relax anything a strand with three or more
|
|
204
|
+
* machines does: `corroboratorCapacity` takes the MAX of the cohort actually present and this
|
|
205
|
+
* number, so the relaxed branch is reachable only by a cohort that is genuinely that small. See
|
|
206
|
+
* Optimystic's `cluster/cluster-policy.ts` ("Why two size yardsticks, not one").
|
|
207
|
+
*
|
|
208
|
+
* The `satisfies` is load-bearing for the same reason as {@link CONTROL_CLUSTER_POLICY}'s.
|
|
209
|
+
*/
|
|
210
|
+
export declare const STRAND_CLUSTER_POLICY: Readonly<{
|
|
211
|
+
allowDownsize: true;
|
|
212
|
+
sizeTolerance: number;
|
|
213
|
+
assumedClusterSize: number;
|
|
214
|
+
}>;
|
|
215
|
+
/**
|
|
216
|
+
* Resolve the cluster size to hand `createLibp2pNode` for a **strand** network, applying
|
|
217
|
+
* {@link DEFAULT_STRAND_CLUSTER_SIZE} and rejecting values Optimystic cannot honour. The
|
|
218
|
+
* control network does not route through here — it uses the fixed
|
|
219
|
+
* {@link CONTROL_REPLICATION_BREADTH}.
|
|
220
|
+
*
|
|
221
|
+
* Leaving the value unset is NOT the same as passing the default: Optimystic's own fallback
|
|
222
|
+
* is 10, so every strand node-creating path must route through here.
|
|
223
|
+
*
|
|
224
|
+
* Every node on the same strand should resolve to the same value: the membership admission
|
|
225
|
+
* gate's *confident* path compares a coordinator's declared set against the member's own
|
|
226
|
+
* cohort view, and that view is bounded by this number, so a member configured much higher
|
|
227
|
+
* can reject a smaller declared set as a downsize. Divergence is a live hazard whenever FRET
|
|
228
|
+
* has a confident network-size estimate — not the unconditional refusal earlier revisions of
|
|
229
|
+
* this comment described. See `docs/architecture.md` → "Replication cluster size".
|
|
230
|
+
*/
|
|
231
|
+
export declare function resolveStrandClusterSize(configured?: number): number;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smallest cluster size Optimystic will honour — its own `minAbsoluteClusterSize`,
|
|
3
|
+
* and the smallest value that reaches the cluster path at all (a lone node writes to
|
|
4
|
+
* local storage without forming a cluster). A validation floor, not a default.
|
|
5
|
+
*/
|
|
6
|
+
export const MIN_CLUSTER_SIZE = 2;
|
|
7
|
+
/**
|
|
8
|
+
* How many nodes each block of the **control** (cadre membership) database is replicated
|
|
9
|
+
* to. Chosen to exceed any party's node count, so in practice every control block lands
|
|
10
|
+
* on every member of the party.
|
|
11
|
+
*
|
|
12
|
+
* **Why full replication here.** Every control node reads the whole control database —
|
|
13
|
+
* membership, peer addresses, the strand list — so a member left out of a block's cohort is
|
|
14
|
+
* a member that may never learn the fact, and at a cohort of two the *read repair* that is
|
|
15
|
+
* supposed to make that safe cannot converge (it can ask exactly one peer, and that peer may
|
|
16
|
+
* be the member that also missed the write). Measured: 4 failures in 10 runs of the
|
|
17
|
+
* control-DB replication scenario at size 2, 0 in 20 at size 3 and 0 in 10 at size 8.
|
|
18
|
+
*
|
|
19
|
+
* **Why this number, and why a constant.** Optimystic caps a cohort at the peers that
|
|
20
|
+
* actually serve the network and downsizes a cohort it cannot fill (`allowDownsize: true`,
|
|
21
|
+
* which Cadre passes), so any value at or above the party's node count yields the same
|
|
22
|
+
* behaviour: cohort = whole party. 16 is roughly twice the largest deployment the product
|
|
23
|
+
* documents (`docs/architecture.md` → "Enterprise (Multi-Node Mixed)", 7 nodes) — headroom
|
|
24
|
+
* without pretending to support arbitrarily large parties; a party genuinely running more
|
|
25
|
+
* than 16 nodes is back to partial replication for its control database and should raise
|
|
26
|
+
* this. It is a constant rather than the live member count because Optimystic freezes the
|
|
27
|
+
* value at libp2p-node construction, before the `ControlDatabase` holding the `CadrePeer`
|
|
28
|
+
* rows exists, and because a per-node derivation lets two members disagree.
|
|
29
|
+
*
|
|
30
|
+
* A cadre is one party by construction, so the owner-blind cohort-selection question that
|
|
31
|
+
* applies to strand networks (see {@link DEFAULT_STRAND_CLUSTER_SIZE} below) is not a question
|
|
32
|
+
* here: "replicate to the whole party" already is the whole intent, and there is no party
|
|
33
|
+
* diversity for this breadth to buy or fail to buy.
|
|
34
|
+
*
|
|
35
|
+
* Canonical explanation of all of the above — the read-repair mechanics, what whole-party
|
|
36
|
+
* breadth does and does not remove, and what the wider cohort costs in write availability —
|
|
37
|
+
* is `docs/architecture.md` → "Replication cluster size". Keep it there, not here.
|
|
38
|
+
*
|
|
39
|
+
* **Deliberately not `clusterPolicy.assumedClusterSize`.** That option means "the smallest
|
|
40
|
+
* cohort this deployment can genuinely field", and it feeds both the membership admission
|
|
41
|
+
* gate's low-confidence path *and* the read-repair/reconcile corroboration floor. A Cadre
|
|
42
|
+
* party legitimately runs one or two nodes, so Cadre leaves it at Optimystic's default of 2;
|
|
43
|
+
* asserting 16 there would make the admission gate demand `ceil(0.75 x 16) = 12` declared
|
|
44
|
+
* peers and refuse every real party's writes.
|
|
45
|
+
*
|
|
46
|
+
* NOTE: cohort selection overfetches proportionally to this number —
|
|
47
|
+
* `Libp2pKeyPeerNetwork.membershipOverfetch` asks FRET for
|
|
48
|
+
* `max(clusterSize * 4, clusterSize + 16)` candidates and does a peerStore protocol lookup
|
|
49
|
+
* per candidate, so 16 requests a 64-peer proximity band. Free today, because the result is
|
|
50
|
+
* bounded by the peers FRET actually knows (2 in a 3-node party). If control-network cohort
|
|
51
|
+
* selection ever shows up as slow on a large mesh, look here first.
|
|
52
|
+
*/
|
|
53
|
+
export const CONTROL_REPLICATION_BREADTH = 16;
|
|
54
|
+
/**
|
|
55
|
+
* Cluster consensus policy every CONTROL-network libp2p node runs — production
|
|
56
|
+
* (`CadreNode.buildControlNodeOptions`) and the integration harness alike. One definition so
|
|
57
|
+
* the two cannot drift: the object literal used to be hand-copied at both sites, and the copy
|
|
58
|
+
* at the harness had picked up a `superMajorityThreshold` override production never had.
|
|
59
|
+
*
|
|
60
|
+
* `superMajorityThreshold` is deliberately ABSENT. Omitting it makes both the cluster member
|
|
61
|
+
* and the coordinator fall back to Optimystic's `DEFAULT_SUPER_MAJORITY_THRESHOLD` (0.75) —
|
|
62
|
+
* the bar a real party commits control writes against. Setting it here, or at any one
|
|
63
|
+
* consumer, reintroduces exactly the divergence this constant exists to prevent. The absence
|
|
64
|
+
* is a researched decision, not an inherited default: no threshold both relaxes the
|
|
65
|
+
* three-node unanimity bar and satisfies Optimystic's partition-safety condition at the
|
|
66
|
+
* shipped admission fraction — see `docs/architecture.md` → "Replication cluster size".
|
|
67
|
+
*
|
|
68
|
+
* `sizeTolerance` only makes sense alongside `allowDownsize`: a fixed
|
|
69
|
+
* {@link CONTROL_REPLICATION_BREADTH}-wide target is unsatisfiable by any real (2-7 node)
|
|
70
|
+
* party, so the cohort must be allowed to shrink to the party that actually exists.
|
|
71
|
+
*
|
|
72
|
+
* NOT for strand networks: {@link STRAND_CLUSTER_POLICY} is a structurally identical object
|
|
73
|
+
* for a different network with different reasoning. The shape match is a coincidence; keep
|
|
74
|
+
* them separate.
|
|
75
|
+
*
|
|
76
|
+
* `assumedClusterSize` is REQUIRED here, and 2 is not a guess — it is the smallest party we
|
|
77
|
+
* support. The field feeds two Optimystic consumers with opposite failure modes, and only one
|
|
78
|
+
* of them defaults to something workable for us:
|
|
79
|
+
*
|
|
80
|
+
* - The membership admission gate already defaults to 2 (`minAbsoluteClusterSize`), so naming
|
|
81
|
+
* it changes nothing there.
|
|
82
|
+
* - The read-repair / reconcile corroboration floor does NOT. Absent this field it falls back
|
|
83
|
+
* to `clusterSize` — which for the control network is {@link CONTROL_REPLICATION_BREADTH},
|
|
84
|
+
* 16. `corroboratorCapacity(peers, 16)` is then `max(peers, 15)`, so the floor of two
|
|
85
|
+
* distinct non-self corroborators binds. A two-node party fields exactly one, can never
|
|
86
|
+
* reach two, and so can never repair a block: `cluster-fetch:no-quorum` forever.
|
|
87
|
+
*
|
|
88
|
+
* Declaring 2 does NOT lower the replication factor — that stays at
|
|
89
|
+
* {@link CONTROL_REPLICATION_BREADTH}. It states the cohort size we can genuinely field, which
|
|
90
|
+
* is what the corroboration floor is asking. See Optimystic's `cluster/cluster-policy.ts`
|
|
91
|
+
* ("Why two size yardsticks, not one") and `corroboratorCapacity` in `cluster/quorum-restore.ts`.
|
|
92
|
+
*
|
|
93
|
+
* The `satisfies` is load-bearing: without it a mistyped or obsolete key would compile at both
|
|
94
|
+
* this definition and every consumer, because TypeScript only excess-property-checks fresh
|
|
95
|
+
* object literals, not a shared constant handed to `clusterPolicy`.
|
|
96
|
+
*/
|
|
97
|
+
export const CONTROL_CLUSTER_POLICY = Object.freeze({
|
|
98
|
+
allowDownsize: true,
|
|
99
|
+
sizeTolerance: 0.5,
|
|
100
|
+
assumedClusterSize: 2,
|
|
101
|
+
});
|
|
102
|
+
/**
|
|
103
|
+
* Default number of nodes a **strand** network is told its replication cluster should have.
|
|
104
|
+
*
|
|
105
|
+
* Strand data is application data, where partial replication is a legitimate choice: only
|
|
106
|
+
* the control database's every-member-reads-all-of-it character forces full replication
|
|
107
|
+
* ({@link CONTROL_REPLICATION_BREADTH}). So the question here is not "everyone" but "how
|
|
108
|
+
* few is too few", and the answer is four.
|
|
109
|
+
*
|
|
110
|
+
* **Why four.** A write commits when a super-majority of the cohort approves, and that bar is
|
|
111
|
+
* Optimystic's `DEFAULT_SUPER_MAJORITY_THRESHOLD` of 0.75 — which Cadre selects by naming no
|
|
112
|
+
* threshold at all (see {@link CONTROL_CLUSTER_POLICY}). Approvals needed is
|
|
113
|
+
* `ceil(cohort x 0.75)`, so:
|
|
114
|
+
*
|
|
115
|
+
* | copies | approvals needed | holders that may be offline |
|
|
116
|
+
* |--------|------------------|-----------------------------|
|
|
117
|
+
* | 2 | 2 | 0 |
|
|
118
|
+
* | 3 | 3 | 0 |
|
|
119
|
+
* | 4 | 3 | 1 |
|
|
120
|
+
* | 5 | 4 | 1 |
|
|
121
|
+
* | 6 | 5 | 1 |
|
|
122
|
+
*
|
|
123
|
+
* Four is the smallest breadth that lets a write commit while one holder is away. At two or
|
|
124
|
+
* three every holder must be awake for every write, which for a workspace shared between
|
|
125
|
+
* phones and laptops is the ordinary case, not the rare one. Six buys no more fault tolerance
|
|
126
|
+
* than four and costs more overfetch (see the NOTE below).
|
|
127
|
+
*
|
|
128
|
+
* **It is also a correctness floor, not only a durability one — but the floor is measured in
|
|
129
|
+
* machines, not in this number.** A node that has fallen behind and can ask exactly one peer
|
|
130
|
+
* whether it is current has Optimystic accept that single answer as the cluster's truth
|
|
131
|
+
* (`corroboratorCapacity` in `db-p2p/src/cluster/quorum-restore.ts` lowers the corroboration
|
|
132
|
+
* floor to one when the cohort cannot hold a second voter). If that peer is also behind it
|
|
133
|
+
* honestly reports the stale revision, the reader concludes it is current, and re-arms its
|
|
134
|
+
* repair window forever. Measured on the control-DB replication scenario: 4 failures in 10 runs
|
|
135
|
+
* at breadth 2, 0 in 20 at breadth 3, 0 in 10 at breadth 8. What lifts the floor off a single
|
|
136
|
+
* voter is a *third machine*, not a wider target: `corroboratorCapacity` takes the cohort peers
|
|
137
|
+
* actually present against {@link STRAND_CLUSTER_POLICY}'s declared `assumedClusterSize`, so a
|
|
138
|
+
* three-machine strand needs two corroborators whatever this number says, and a two-machine one
|
|
139
|
+
* cannot supply them however wide the target is. The underlying Optimystic behaviour is unfixed
|
|
140
|
+
* — `backlog/debt-read-repair-single-voter-corroboration` — so a two-machine strand still takes
|
|
141
|
+
* the exposure. Raising this number does not buy the way out; adding a machine does.
|
|
142
|
+
*
|
|
143
|
+
* **Why not derived from the party or member count.** The strand's `Member` rows live *in* the
|
|
144
|
+
* strand database, which runs on the strand libp2p node, whose cluster size is frozen at
|
|
145
|
+
* construction — reading the list to configure the node needs the node to exist. Open strands
|
|
146
|
+
* have no member list at all. The count also changes in the unsafe direction: a node that
|
|
147
|
+
* restarted after someone joined derives a wider expected cohort than one that did not, and the
|
|
148
|
+
* membership admission gate's confident path is what rejects the *wider* view. And a `Member` is
|
|
149
|
+
* a party, not a machine; cohort width consumes machines. Full reasoning in
|
|
150
|
+
* `docs/architecture.md` → "Replication cluster size".
|
|
151
|
+
*
|
|
152
|
+
* **Four machines, not four parties.** This number counts machines, and today that is not the
|
|
153
|
+
* same as counting independent holders. Cohort selection upstream is owner-blind — it
|
|
154
|
+
* picks nodes by hash proximity with no concept of which peer belongs to which party
|
|
155
|
+
* (`optimystic/tickets/backlog/feat-cohort-selection-owner-aware-placement`) — and on the
|
|
156
|
+
* cadre-driven path, the one that resolves a seed for itself, a strand's libp2p mesh is seeded
|
|
157
|
+
* from exactly one party's own control-database peer rows (`CadreNode.resolveCohortSeed`), so
|
|
158
|
+
* the cohort actually formed is that one party's machines. Four copies are, on that path, four
|
|
159
|
+
* machines belonging to one party — one failure domain, one witness. A cross-party mesh is not
|
|
160
|
+
* forbidden, only undiscoverable: it takes hand-supplied addresses
|
|
161
|
+
* (`StrandConnectionOptions.bootstrapNodes`) or a hand-written dial (see the doc). Tracked:
|
|
162
|
+
* `backlog/feat-strand-party-identity`. Full reasoning: `docs/architecture.md` →
|
|
163
|
+
* "Replication cluster size".
|
|
164
|
+
*
|
|
165
|
+
* Raising or lowering it per strand is still the embedder's call
|
|
166
|
+
* ({@link resolveStrandClusterSize}, `CadreNodeConfig.strandClusterSize`,
|
|
167
|
+
* `StrandConnectionOptions.clusterSize`) — but every node on one strand must agree, so a change
|
|
168
|
+
* means restarting all of them. There is no smaller-than-{@link MIN_CLUSTER_SIZE} option.
|
|
169
|
+
*
|
|
170
|
+
* NOTE: cohort selection overfetches proportionally — `Libp2pKeyPeerNetwork.membershipOverfetch`
|
|
171
|
+
* asks FRET for `max(clusterSize * 4, clusterSize + 16)` candidates and does one peerStore
|
|
172
|
+
* protocol lookup per candidate, so 4 requests a 20-peer band (was 18 at breadth 2). Negligible,
|
|
173
|
+
* and bounded by the peers FRET actually knows. If strand cohort selection ever shows up as slow,
|
|
174
|
+
* look here first.
|
|
175
|
+
*/
|
|
176
|
+
export const DEFAULT_STRAND_CLUSTER_SIZE = 4;
|
|
177
|
+
/**
|
|
178
|
+
* Cluster consensus policy every STRAND-network libp2p node runs — production
|
|
179
|
+
* (`StrandInstanceManager.buildStrandRuntime`) and the plugin's own networked e2e mesh alike.
|
|
180
|
+
* One definition so the two cannot drift; the literal used to be hand-copied at both sites.
|
|
181
|
+
*
|
|
182
|
+
* Structurally identical to {@link CONTROL_CLUSTER_POLICY} and deliberately separate from it:
|
|
183
|
+
* that one is the control database's, whose breadth is fixed at
|
|
184
|
+
* {@link CONTROL_REPLICATION_BREADTH} for reasons that do not apply to application data.
|
|
185
|
+
*
|
|
186
|
+
* `superMajorityThreshold` is ABSENT for the same reason it is absent there: omitting it is how
|
|
187
|
+
* both the coordinator and the cluster member select Optimystic's
|
|
188
|
+
* `DEFAULT_SUPER_MAJORITY_THRESHOLD` (0.75), the bar {@link DEFAULT_STRAND_CLUSTER_SIZE}'s
|
|
189
|
+
* copies/approvals table is computed against.
|
|
190
|
+
*
|
|
191
|
+
* `sizeTolerance` only makes sense alongside `allowDownsize`: a {@link
|
|
192
|
+
* DEFAULT_STRAND_CLUSTER_SIZE}-wide target is unsatisfiable by a strand of one, two or three
|
|
193
|
+
* machines, so the cohort must be allowed to shrink to the mesh that actually exists.
|
|
194
|
+
*
|
|
195
|
+
* `assumedClusterSize` is REQUIRED, and {@link MIN_CLUSTER_SIZE} is not a guess — it is the
|
|
196
|
+
* smallest strand that reaches the cluster path at all. Absent this field the read-repair /
|
|
197
|
+
* reconcile corroboration floor falls back to `clusterSize`, so
|
|
198
|
+
* `corroboratorCapacity(peers, 4)` is `max(peers, 3)` and two distinct non-self corroborators
|
|
199
|
+
* become mandatory. A two-machine strand fields exactly one peer, can never reach two, and so
|
|
200
|
+
* can never repair a block — `cluster-fetch:no-quorum` forever, which is a read that never
|
|
201
|
+
* catches up and a `create table` that dies with `Missing block` on the joining node. Declaring
|
|
202
|
+
* the honest floor does NOT lower the replication factor (that stays at
|
|
203
|
+
* {@link DEFAULT_STRAND_CLUSTER_SIZE}) and does not relax anything a strand with three or more
|
|
204
|
+
* machines does: `corroboratorCapacity` takes the MAX of the cohort actually present and this
|
|
205
|
+
* number, so the relaxed branch is reachable only by a cohort that is genuinely that small. See
|
|
206
|
+
* Optimystic's `cluster/cluster-policy.ts` ("Why two size yardsticks, not one").
|
|
207
|
+
*
|
|
208
|
+
* The `satisfies` is load-bearing for the same reason as {@link CONTROL_CLUSTER_POLICY}'s.
|
|
209
|
+
*/
|
|
210
|
+
export const STRAND_CLUSTER_POLICY = Object.freeze({
|
|
211
|
+
allowDownsize: true,
|
|
212
|
+
sizeTolerance: 0.5,
|
|
213
|
+
assumedClusterSize: MIN_CLUSTER_SIZE,
|
|
214
|
+
});
|
|
215
|
+
/**
|
|
216
|
+
* Resolve the cluster size to hand `createLibp2pNode` for a **strand** network, applying
|
|
217
|
+
* {@link DEFAULT_STRAND_CLUSTER_SIZE} and rejecting values Optimystic cannot honour. The
|
|
218
|
+
* control network does not route through here — it uses the fixed
|
|
219
|
+
* {@link CONTROL_REPLICATION_BREADTH}.
|
|
220
|
+
*
|
|
221
|
+
* Leaving the value unset is NOT the same as passing the default: Optimystic's own fallback
|
|
222
|
+
* is 10, so every strand node-creating path must route through here.
|
|
223
|
+
*
|
|
224
|
+
* Every node on the same strand should resolve to the same value: the membership admission
|
|
225
|
+
* gate's *confident* path compares a coordinator's declared set against the member's own
|
|
226
|
+
* cohort view, and that view is bounded by this number, so a member configured much higher
|
|
227
|
+
* can reject a smaller declared set as a downsize. Divergence is a live hazard whenever FRET
|
|
228
|
+
* has a confident network-size estimate — not the unconditional refusal earlier revisions of
|
|
229
|
+
* this comment described. See `docs/architecture.md` → "Replication cluster size".
|
|
230
|
+
*/
|
|
231
|
+
export function resolveStrandClusterSize(configured) {
|
|
232
|
+
if (configured === undefined) {
|
|
233
|
+
return DEFAULT_STRAND_CLUSTER_SIZE;
|
|
234
|
+
}
|
|
235
|
+
if (!Number.isInteger(configured) || configured < MIN_CLUSTER_SIZE) {
|
|
236
|
+
throw new Error(`clusterSize must be an integer >= ${MIN_CLUSTER_SIZE} (Optimystic's minimum cluster size); got ${configured}`);
|
|
237
|
+
}
|
|
238
|
+
return configured;
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=cluster-size.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cluster-size.js","sourceRoot":"","sources":["../src/cluster-size.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC,MAAM,CAAC;IACnD,aAAa,EAAE,IAAI;IACnB,aAAa,EAAE,GAAG;IAClB,kBAAkB,EAAE,CAAC;CAC+B,CAAC,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,CAAC;IAClD,aAAa,EAAE,IAAI;IACnB,aAAa,EAAE,GAAG;IAClB,kBAAkB,EAAE,gBAAgB;CACgB,CAAC,CAAC;AAEvD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,wBAAwB,CAAC,UAAmB;IAC3D,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,2BAA2B,CAAC;IACpC,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,gBAAgB,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CACd,qCAAqC,gBAAgB,6CAA6C,UAAU,EAAE,CAC9G,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAC;AACnB,CAAC"}
|