@optimystic/db-p2p 0.14.1 → 0.16.2
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 +616 -609
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +4 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +52 -3
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +129 -6
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +3 -1
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +182 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +429 -69
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/peer-key-binding.d.ts +25 -0
- package/dist/src/cluster/peer-key-binding.d.ts.map +1 -0
- package/dist/src/cluster/peer-key-binding.js +39 -0
- package/dist/src/cluster/peer-key-binding.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +72 -0
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -0
- package/dist/src/cluster/quorum-restore.js +0 -0
- package/dist/src/cluster/quorum-restore.js.map +1 -0
- package/dist/src/cluster/rebalance-monitor.d.ts +23 -1
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +17 -2
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +7 -1
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +7 -0
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +4 -1
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cluster/supermajority-coupling.d.ts +22 -0
- package/dist/src/cluster/supermajority-coupling.d.ts.map +1 -0
- package/dist/src/cluster/supermajority-coupling.js +27 -0
- package/dist/src/cluster/supermajority-coupling.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +46 -4
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-driver.js +62 -7
- package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +7 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-transport.js +9 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts +197 -27
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +643 -87
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/dispute/arbitrator-selection.d.ts +68 -7
- package/dist/src/dispute/arbitrator-selection.d.ts.map +1 -1
- package/dist/src/dispute/arbitrator-selection.js +105 -16
- package/dist/src/dispute/arbitrator-selection.js.map +1 -1
- package/dist/src/dispute/cascade.d.ts +7 -0
- package/dist/src/dispute/cascade.d.ts.map +1 -1
- package/dist/src/dispute/cascade.js +1 -1
- package/dist/src/dispute/cascade.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +3 -2
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +32 -3
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +82 -9
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/index.d.ts +1 -1
- package/dist/src/dispute/index.d.ts.map +1 -1
- package/dist/src/dispute/index.js +1 -1
- package/dist/src/dispute/index.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +25 -0
- package/dist/src/dispute/invalidation.d.ts.map +1 -1
- package/dist/src/dispute/invalidation.js +14 -2
- package/dist/src/dispute/invalidation.js.map +1 -1
- package/dist/src/dispute/service.d.ts.map +1 -1
- package/dist/src/dispute/service.js +7 -1
- package/dist/src/dispute/service.js.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/it-utility.d.ts +1 -1
- package/dist/src/it-utility.d.ts.map +1 -1
- package/dist/src/it-utility.js +4 -13
- package/dist/src/it-utility.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +98 -1
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +272 -17
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +12 -0
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +415 -87
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +2 -0
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +6 -0
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/matchmaking/traffic-validation.js +1 -0
- package/dist/src/matchmaking/traffic-validation.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +2 -2
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +8 -4
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/owned-block-seed.d.ts +27 -0
- package/dist/src/owned-block-seed.d.ts.map +1 -0
- package/dist/src/owned-block-seed.js +37 -0
- package/dist/src/owned-block-seed.js.map +1 -0
- package/dist/src/protocol-client.d.ts +1 -0
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +9 -2
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/protocol-limits.d.ts +32 -0
- package/dist/src/protocol-limits.d.ts.map +1 -0
- package/dist/src/protocol-limits.js +32 -0
- package/dist/src/protocol-limits.js.map +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +3 -0
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +41 -4
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +128 -80
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +38 -1
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +92 -13
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +7 -1
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/peer-reputation.js +1 -1
- package/dist/src/reputation/peer-reputation.js.map +1 -1
- package/dist/src/reputation/types.d.ts +3 -1
- package/dist/src/reputation/types.d.ts.map +1 -1
- package/dist/src/reputation/types.js +3 -0
- package/dist/src/reputation/types.js.map +1 -1
- package/dist/src/rn.d.ts +5 -1
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +7 -1
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/arachnode-fret-adapter.d.ts +30 -1
- package/dist/src/storage/arachnode-fret-adapter.d.ts.map +1 -1
- package/dist/src/storage/arachnode-fret-adapter.js +15 -2
- package/dist/src/storage/arachnode-fret-adapter.js.map +1 -1
- package/dist/src/storage/arachnode-partition.d.ts +49 -0
- package/dist/src/storage/arachnode-partition.d.ts.map +1 -0
- package/dist/src/storage/arachnode-partition.js +61 -0
- package/dist/src/storage/arachnode-partition.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +40 -1
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +150 -57
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +8 -0
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +13 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +45 -0
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/kv-raw-storage.js +113 -0
- package/dist/src/storage/kv-raw-storage.js.map +1 -0
- package/dist/src/storage/memory-storage.d.ts +20 -58
- package/dist/src/storage/memory-storage.d.ts.map +1 -1
- package/dist/src/storage/memory-storage.js +22 -148
- package/dist/src/storage/memory-storage.js.map +1 -1
- package/dist/src/storage/memory-store-driver.d.ts +39 -0
- package/dist/src/storage/memory-store-driver.d.ts.map +1 -0
- package/dist/src/storage/memory-store-driver.js +129 -0
- package/dist/src/storage/memory-store-driver.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +10 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -0
- package/dist/src/storage/raw-store-codec.js +36 -0
- package/dist/src/storage/raw-store-codec.js.map +1 -0
- package/dist/src/storage/raw-store-driver.d.ts +69 -0
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -0
- package/dist/src/storage/raw-store-driver.js +2 -0
- package/dist/src/storage/raw-store-driver.js.map +1 -0
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator-v2.js +1 -1
- package/dist/src/storage/restoration-coordinator-v2.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts +73 -0
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -0
- package/dist/src/storage/restoration-coordinator.js +181 -0
- package/dist/src/storage/restoration-coordinator.js.map +1 -0
- package/dist/src/storage/ring-selector.d.ts +92 -5
- package/dist/src/storage/ring-selector.d.ts.map +1 -1
- package/dist/src/storage/ring-selector.js +153 -51
- package/dist/src/storage/ring-selector.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts +128 -0
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -0
- package/dist/src/storage/ring-shift-coordinator.js +196 -0
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -0
- package/dist/src/storage/storage-monitor.d.ts +28 -0
- package/dist/src/storage/storage-monitor.d.ts.map +1 -1
- package/dist/src/storage/storage-monitor.js +42 -2
- package/dist/src/storage/storage-monitor.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +19 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +172 -22
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/client.d.ts.map +1 -1
- package/dist/src/sync/client.js +5 -1
- package/dist/src/sync/client.js.map +1 -1
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +2 -1
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +28 -4
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +51 -10
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
- package/dist/src/testing/index.d.ts +1 -0
- package/dist/src/testing/index.d.ts.map +1 -1
- package/dist/src/testing/index.js +1 -0
- package/dist/src/testing/index.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +15 -7
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +24 -0
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -0
- package/dist/src/testing/raw-storage-conformance.js +313 -0
- package/dist/src/testing/raw-storage-conformance.js.map +1 -0
- package/package.json +4 -4
- package/src/cluster/block-transfer-service.ts +4 -3
- package/src/cluster/block-transfer.ts +424 -265
- package/src/cluster/client.ts +3 -1
- package/src/cluster/cluster-repo.ts +481 -71
- package/src/cluster/peer-key-binding.ts +37 -0
- package/src/cluster/quorum-restore.ts +0 -0
- package/src/cluster/rebalance-monitor.ts +34 -3
- package/src/cluster/service.ts +7 -1
- package/src/cluster/spread-on-churn.ts +11 -1
- package/src/cluster/supermajority-coupling.ts +37 -0
- package/src/cohort-topic/cohort-gossip-driver.ts +96 -9
- package/src/cohort-topic/cohort-gossip-transport.ts +10 -0
- package/src/cohort-topic/host.ts +831 -105
- package/src/dispute/arbitrator-selection.ts +138 -28
- package/src/dispute/cascade.ts +8 -1
- package/src/dispute/client.ts +3 -2
- package/src/dispute/dispute-service.ts +94 -9
- package/src/dispute/index.ts +7 -1
- package/src/dispute/invalidation.ts +40 -2
- package/src/dispute/service.ts +7 -1
- package/src/index.ts +46 -43
- package/src/it-utility.ts +5 -13
- package/src/libp2p-key-network.ts +281 -18
- package/src/libp2p-node-base.ts +485 -79
- package/src/matchmaking/query-transport.ts +9 -0
- package/src/network/network-manager-service.ts +8 -2
- package/src/owned-block-seed.ts +40 -0
- package/src/protocol-client.ts +10 -3
- package/src/protocol-limits.ts +33 -0
- package/src/repo/client.ts +3 -0
- package/src/repo/cluster-coordinator.ts +145 -82
- package/src/repo/coordinator-repo.ts +96 -16
- package/src/repo/service.ts +7 -1
- package/src/reputation/types.ts +3 -0
- package/src/rn.ts +7 -1
- package/src/storage/arachnode-fret-adapter.ts +33 -2
- package/src/storage/arachnode-partition.ts +74 -0
- package/src/storage/block-storage.ts +169 -61
- package/src/storage/i-block-storage.ts +9 -0
- package/src/storage/i-raw-storage.ts +14 -0
- package/src/storage/kv-raw-storage.ts +135 -0
- package/src/storage/memory-storage.ts +25 -173
- package/src/storage/memory-store-driver.ts +157 -0
- package/src/storage/raw-store-codec.ts +42 -0
- package/src/storage/raw-store-driver.ts +80 -0
- package/src/storage/{restoration-coordinator-v2.ts → restoration-coordinator.ts} +43 -37
- package/src/storage/ring-selector.ts +317 -176
- package/src/storage/ring-shift-coordinator.ts +271 -0
- package/src/storage/storage-monitor.ts +70 -3
- package/src/storage/storage-repo.ts +710 -558
- package/src/sync/client.ts +5 -1
- package/src/sync/service.ts +2 -1
- package/src/testing/cohort-topic-mesh-harness.ts +61 -11
- package/src/testing/index.ts +1 -0
- package/src/testing/mesh-harness.ts +14 -6
- package/src/testing/raw-storage-conformance.ts +397 -0
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { hashPeerId } from 'p2p-fret';
|
|
2
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
3
|
+
import { extractPrefix } from './arachnode-partition.js';
|
|
4
|
+
/** EWMA weight for the demand inputs. Higher = more reactive, lower = smoother. */
|
|
5
|
+
const DEFAULT_SMOOTHING_ALPHA = 0.2;
|
|
6
|
+
/** Hysteresis half-width in rings. 0.5 puts a full ring between the move-out and move-in triggers. */
|
|
7
|
+
const DEFAULT_DEADBAND = 0.5;
|
|
8
|
+
/** Minimum time (ms) at a ring after a move is triggered before another may start. */
|
|
9
|
+
const DEFAULT_MIN_DWELL_MS = 10 * 60 * 1000;
|
|
10
|
+
/** Floor for smoothed coverage so `-log2` never sees 0; the [0,16] clamp bounds the result anyway. */
|
|
11
|
+
const DEPTH_EPSILON = 1e-12;
|
|
2
12
|
/**
|
|
3
13
|
* Determines appropriate ring depth based on storage capacity and network demand.
|
|
4
14
|
*
|
|
@@ -7,18 +17,36 @@ import { hashPeerId } from 'p2p-fret';
|
|
|
7
17
|
* - Ring N: 2^N partitions
|
|
8
18
|
*
|
|
9
19
|
* A node selects its ring based on: available_capacity / estimated_neighborhood_demand
|
|
20
|
+
*
|
|
21
|
+
* The transition decision ({@link shouldTransition}) is damped so it does not thrash near a ring
|
|
22
|
+
* boundary: the demand signal is smoothed with an EWMA, a dead-band keeps a hovering ratio from
|
|
23
|
+
* moving, and a minimum-dwell timer + single-step rule bound how often and how far it moves. See
|
|
24
|
+
* `docs/arachnode-ring-handoff.md` § Part 1.
|
|
10
25
|
*/
|
|
11
26
|
export class RingSelector {
|
|
12
27
|
fretAdapter;
|
|
13
28
|
storageMonitor;
|
|
14
29
|
config;
|
|
30
|
+
/** EWMA of available capacity (bytes). Undefined until the first sample seeds it. */
|
|
31
|
+
smoothedAvailable;
|
|
32
|
+
/** EWMA of estimated total network data (bytes). Undefined until the first sample seeds it. */
|
|
33
|
+
smoothedTotalData;
|
|
34
|
+
/** Clock time (ms) a move was last triggered; undefined = no move yet, so dwell does not block. */
|
|
35
|
+
lastMoveAt;
|
|
36
|
+
now;
|
|
15
37
|
constructor(fretAdapter, storageMonitor, config) {
|
|
16
38
|
this.fretAdapter = fretAdapter;
|
|
17
39
|
this.storageMonitor = storageMonitor;
|
|
18
40
|
this.config = config;
|
|
41
|
+
this.now = config.now ?? (() => Date.now());
|
|
19
42
|
}
|
|
20
43
|
/**
|
|
21
|
-
* Determine appropriate ring depth
|
|
44
|
+
* Determine appropriate ring depth from an instantaneous capacity + demand snapshot.
|
|
45
|
+
*
|
|
46
|
+
* This is the *undamped* reading used for the deterministic first-boot ring and anywhere a
|
|
47
|
+
* one-shot estimate is wanted. The damped transition logic lives in {@link shouldTransition};
|
|
48
|
+
* this method intentionally does not touch the EWMA state so its result is a pure function of
|
|
49
|
+
* the current snapshot.
|
|
22
50
|
*/
|
|
23
51
|
async determineRing() {
|
|
24
52
|
const capacity = await this.storageMonitor.getCapacity();
|
|
@@ -26,14 +54,31 @@ export class RingSelector {
|
|
|
26
54
|
// Not enough capacity for any ring
|
|
27
55
|
return -1;
|
|
28
56
|
}
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
//
|
|
57
|
+
const estimatedTotalData = this.estimateNetworkData(capacity.available);
|
|
58
|
+
// Calculate what fraction of keyspace we can cover
|
|
59
|
+
const coverage = capacity.available / estimatedTotalData;
|
|
60
|
+
// Ring depth: 0 = full keyspace, N = 2^N partitions
|
|
61
|
+
// If coverage = 0.01 (1%), we need ~100 partitions ≈ Ring 7
|
|
62
|
+
// If coverage = 1.0 (100%), we can handle full keyspace = Ring 0
|
|
63
|
+
const ringDepth = Math.max(0, Math.ceil(-Math.log2(Math.max(0.001, coverage))));
|
|
64
|
+
return Math.min(ringDepth, 16); // Cap at Ring 16 (65536 partitions)
|
|
65
|
+
}
|
|
66
|
+
// NOTE: prefix extraction lives in `arachnode-partition.ts` (single source of truth shared with
|
|
67
|
+
// the block-side responsibility derivation in RestorationCoordinator / RingShiftCoordinator).
|
|
68
|
+
/**
|
|
69
|
+
* Estimate total network spare capacity (bytes) by aggregating observed per-ring stats.
|
|
70
|
+
*
|
|
71
|
+
* See docs/arachnode.md §"Capacity Management and Ring Adjustment": ring selection should
|
|
72
|
+
* reflect observed network demand, not a hard-coded constant. We treat Σ (peerCount ×
|
|
73
|
+
* avgCapacity) over known rings as a proxy for total network spare capacity, so `coverage`
|
|
74
|
+
* becomes our share of it. More peers (or richer peers) → smaller share → higher ring depth
|
|
75
|
+
* (more specialization). On bootstrap, when no ring stats are observed yet, we fall back to a
|
|
76
|
+
* conservative fixed estimate so first-boot behavior is deterministic.
|
|
77
|
+
*
|
|
78
|
+
* @param availableForFallback local available bytes, used only for the pathological
|
|
79
|
+
* zero-aggregate case (e.g. all peers report 0 available) so coverage collapses to 1.0 (ring 0).
|
|
80
|
+
*/
|
|
81
|
+
estimateNetworkData(availableForFallback) {
|
|
37
82
|
const ringStats = this.fretAdapter.getRingStats();
|
|
38
83
|
let estimatedTotalData;
|
|
39
84
|
if (ringStats.length > 0) {
|
|
@@ -44,18 +89,34 @@ export class RingSelector {
|
|
|
44
89
|
const estimatedTotalBlocks = 1000; // Conservative estimate
|
|
45
90
|
estimatedTotalData = estimatedTotalBlocks * avgBlockSize;
|
|
46
91
|
}
|
|
47
|
-
// Guard against pathological zero-aggregate (e.g. all peers report 0 available);
|
|
48
|
-
// fall back to local capacity so coverage = 1.0 (ring 0).
|
|
49
92
|
if (estimatedTotalData <= 0) {
|
|
50
|
-
estimatedTotalData =
|
|
93
|
+
estimatedTotalData = availableForFallback;
|
|
51
94
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
95
|
+
return estimatedTotalData;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Fold one sample into the EWMA of each demand input. Seeds from the first real sample rather
|
|
99
|
+
* than from 0 — a 0 seed would drag the smoothed depth for many ticks.
|
|
100
|
+
*/
|
|
101
|
+
updateSmoothing(available, totalData) {
|
|
102
|
+
const alpha = this.config.smoothingAlpha ?? DEFAULT_SMOOTHING_ALPHA;
|
|
103
|
+
this.smoothedAvailable = this.smoothedAvailable === undefined
|
|
104
|
+
? available
|
|
105
|
+
: alpha * available + (1 - alpha) * this.smoothedAvailable;
|
|
106
|
+
this.smoothedTotalData = this.smoothedTotalData === undefined
|
|
107
|
+
? totalData
|
|
108
|
+
: alpha * totalData + (1 - alpha) * this.smoothedTotalData;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Continuous smoothed ring depth `d = clamp(-log2(smoothedCoverage), 0, 16)` from the current
|
|
112
|
+
* EWMA state. Returns 0 (ring 0) if no sample has been folded in yet.
|
|
113
|
+
*/
|
|
114
|
+
smoothedDepth() {
|
|
115
|
+
const available = this.smoothedAvailable ?? 0;
|
|
116
|
+
const totalData = this.smoothedTotalData ?? 0;
|
|
117
|
+
const coverage = totalData > 0 ? available / totalData : 1;
|
|
118
|
+
const depth = -Math.log2(Math.max(DEPTH_EPSILON, coverage));
|
|
119
|
+
return Math.min(16, Math.max(0, depth));
|
|
59
120
|
}
|
|
60
121
|
/**
|
|
61
122
|
* Calculate partition for a given ring depth and peer ID.
|
|
@@ -64,19 +125,27 @@ export class RingSelector {
|
|
|
64
125
|
if (ringDepth === 0) {
|
|
65
126
|
return undefined; // Full keyspace, no partition
|
|
66
127
|
}
|
|
67
|
-
// Hash peer ID to get coordinate
|
|
68
|
-
|
|
69
|
-
//
|
|
128
|
+
// Hash peer ID to get coordinate. hashPeerId reads `peerId.toMultihash().bytes`, so it
|
|
129
|
+
// needs a real PeerId — reconstruct one from the string. Peers must occupy the SAME ring
|
|
130
|
+
// coordinate FRET uses to place them (hashPeerId(peerId)), or restoration's block-prefix
|
|
131
|
+
// vs peer-prefix comparison stops meaning "this peer owns this block's slice".
|
|
132
|
+
const coord = await hashPeerId(peerIdFromString(peerId));
|
|
133
|
+
// Extract prefix bits from coordinate (shared with block-side responsibility derivation).
|
|
70
134
|
const prefixBits = ringDepth;
|
|
71
|
-
const prefixValue =
|
|
135
|
+
const prefixValue = extractPrefix(coord, prefixBits);
|
|
72
136
|
return { prefixBits, prefixValue };
|
|
73
137
|
}
|
|
74
138
|
/**
|
|
75
139
|
* Create Arachnode info for this node.
|
|
140
|
+
*
|
|
141
|
+
* @param ringDepthOverride when supplied (e.g. the single-step target from
|
|
142
|
+
* {@link shouldTransition}), the node advertises exactly this ring instead of a freshly
|
|
143
|
+
* recomputed {@link determineRing}. The transition site passes it so the advertised ring can
|
|
144
|
+
* never disagree with the ring the transition decision just chose.
|
|
76
145
|
*/
|
|
77
|
-
async createArachnodeInfo(peerId) {
|
|
146
|
+
async createArachnodeInfo(peerId, ringDepthOverride) {
|
|
78
147
|
const capacity = await this.storageMonitor.getCapacity();
|
|
79
|
-
const ringDepth = await this.determineRing();
|
|
148
|
+
const ringDepth = ringDepthOverride ?? await this.determineRing();
|
|
80
149
|
const partition = ringDepth >= 0
|
|
81
150
|
? await this.calculatePartition(ringDepth, peerId)
|
|
82
151
|
: undefined;
|
|
@@ -92,45 +161,78 @@ export class RingSelector {
|
|
|
92
161
|
};
|
|
93
162
|
}
|
|
94
163
|
/**
|
|
95
|
-
*
|
|
164
|
+
* Decide whether a damped ring transition should start.
|
|
165
|
+
*
|
|
166
|
+
* Damping (see `docs/arachnode-ring-handoff.md` § Part 1):
|
|
167
|
+
* - **Smoothing** — every call folds the current sample into an EWMA of the demand inputs and
|
|
168
|
+
* works off the smoothed continuous depth `d`, so a single noisy sample cannot trigger a move.
|
|
169
|
+
* - **Hysteresis** — anchored on the node's *currently-advertised* ring `R` (not a freshly
|
|
170
|
+
* recomputed one). A move only fires when `d` is past the boundary by the dead-band `h`, so a
|
|
171
|
+
* ratio hovering around an integer produces no move.
|
|
172
|
+
* - **Dwell + single-step** — no new move within `minDwellMs` of the last, no move while a shift
|
|
173
|
+
* is already in flight (`status === 'moving'`), and each move steps by exactly ±1 ring even
|
|
174
|
+
* when `d` implies a larger jump.
|
|
175
|
+
*
|
|
176
|
+
* Return shape (`{ shouldMove, direction, newRingDepth }`) is the *trigger* the handoff protocol
|
|
177
|
+
* consumes; it is kept stable so that consumer is unaffected.
|
|
96
178
|
*/
|
|
97
179
|
async shouldTransition() {
|
|
98
180
|
const capacity = await this.storageMonitor.getCapacity();
|
|
99
|
-
const usedPercent = capacity.used / capacity.total;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
181
|
+
const usedPercent = capacity.total > 0 ? capacity.used / capacity.total : 0;
|
|
182
|
+
// Smoothing updates every tick, regardless of whether we act, so the EWMA keeps tracking
|
|
183
|
+
// demand even while a candidate move is dwell-blocked or a shift is already in flight.
|
|
184
|
+
const rawTotalData = this.estimateNetworkData(capacity.available);
|
|
185
|
+
this.updateSmoothing(capacity.available, rawTotalData);
|
|
186
|
+
const d = this.smoothedDepth();
|
|
187
|
+
const info = this.fretAdapter.getMyArachnodeInfo();
|
|
188
|
+
// Hysteresis anchor: the node's currently-advertised ring. Fall back to the smoothed depth
|
|
189
|
+
// only in the pathological case where nothing has been advertised yet (production always
|
|
190
|
+
// advertises at bootstrap before the first tick).
|
|
191
|
+
const advertisedRing = info?.ringDepth ?? Math.min(16, Math.max(0, Math.round(d)));
|
|
192
|
+
// No re-entrant shift: a move already in flight (advertise→confirm→release) owns the ring.
|
|
193
|
+
if (info?.status === 'moving') {
|
|
194
|
+
return { shouldMove: false };
|
|
195
|
+
}
|
|
196
|
+
// Minimum dwell: rate-limit so signal noise cannot drive rapid flips. `lastMoveAt` undefined
|
|
197
|
+
// means no prior move, so the first genuine move is never blocked. The stamp here (at *trigger*)
|
|
198
|
+
// keeps a move from immediately re-triggering before `status` flips to `moving`; the driver then
|
|
199
|
+
// calls {@link recordShiftSettled} once the advertise→confirm→release handoff SETTLES, so the
|
|
200
|
+
// effective dwell is measured from the *completed* shift (spec §1.3), not merely the trigger.
|
|
201
|
+
const minDwellMs = this.config.minDwellMs ?? DEFAULT_MIN_DWELL_MS;
|
|
202
|
+
if (this.lastMoveAt !== undefined && (this.now() - this.lastMoveAt) < minDwellMs) {
|
|
203
|
+
return { shouldMove: false };
|
|
204
|
+
}
|
|
205
|
+
const h = this.config.deadband ?? DEFAULT_DEADBAND;
|
|
206
|
+
// Move OUT (R → R+1): smoothed depth solidly past the outer boundary AND storage pressure high.
|
|
207
|
+
if (d >= advertisedRing + 1 - h && usedPercent > this.config.thresholds.moveOut) {
|
|
208
|
+
this.lastMoveAt = this.now();
|
|
103
209
|
return {
|
|
104
210
|
shouldMove: true,
|
|
105
211
|
direction: 'out',
|
|
106
|
-
newRingDepth:
|
|
212
|
+
newRingDepth: advertisedRing + 1 // single step, even if d implies a larger jump
|
|
107
213
|
};
|
|
108
214
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
};
|
|
118
|
-
}
|
|
215
|
+
// Move IN (R → R-1): smoothed depth solidly past the inner boundary AND storage slack AND R>0.
|
|
216
|
+
if (advertisedRing > 0 && d <= advertisedRing - 1 + h && usedPercent < this.config.thresholds.moveIn) {
|
|
217
|
+
this.lastMoveAt = this.now();
|
|
218
|
+
return {
|
|
219
|
+
shouldMove: true,
|
|
220
|
+
direction: 'in',
|
|
221
|
+
newRingDepth: advertisedRing - 1 // single step
|
|
222
|
+
};
|
|
119
223
|
}
|
|
120
224
|
return { shouldMove: false };
|
|
121
225
|
}
|
|
122
226
|
/**
|
|
123
|
-
*
|
|
227
|
+
* Re-stamp the dwell timer at the moment a ring shift *settles* — the driver
|
|
228
|
+
* (`RingShiftCoordinator` via `libp2p-node-base`) calls this once `executeShift` resolves, for a
|
|
229
|
+
* completed OR a rolled-back move. So the minimum dwell is measured from the settled shift rather
|
|
230
|
+
* than only from the trigger (`shouldTransition`): a completed shift dwells `minDwellMs` before the
|
|
231
|
+
* next move (spec §1.3, "after a *completed* shift"), and a shift that keeps failing is rate-limited
|
|
232
|
+
* rather than retried every tick. No-op-safe to call when no shift ran.
|
|
124
233
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for (let i = 0; i < bits; i++) {
|
|
128
|
-
const byteIndex = Math.floor(i / 8);
|
|
129
|
-
const bitIndex = 7 - (i % 8);
|
|
130
|
-
const bit = (bytes[byteIndex] >> bitIndex) & 1;
|
|
131
|
-
value = (value << 1) | bit;
|
|
132
|
-
}
|
|
133
|
-
return value;
|
|
234
|
+
recordShiftSettled() {
|
|
235
|
+
this.lastMoveAt = this.now();
|
|
134
236
|
}
|
|
135
237
|
}
|
|
136
238
|
//# sourceMappingURL=ring-selector.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ring-selector.js","sourceRoot":"","sources":["../../../src/storage/ring-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"ring-selector.js","sourceRoot":"","sources":["../../../src/storage/ring-selector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { FretService } from 'p2p-fret';
|
|
2
|
+
import type { PartitionDetector } from '../cluster/partition-detector.js';
|
|
3
|
+
import type { ArachnodeFretAdapter } from './arachnode-fret-adapter.js';
|
|
4
|
+
import type { RingSelector } from './ring-selector.js';
|
|
5
|
+
/**
|
|
6
|
+
* The confirm primitive the handoff gates release on. Satisfied by `BlockTransferCoordinator`; kept
|
|
7
|
+
* as a narrow structural interface so the coordinator is unit-testable with a stub.
|
|
8
|
+
*/
|
|
9
|
+
export interface ReplicationConfirmer {
|
|
10
|
+
confirmReplicated(blockIds: string[], owners: Map<string, string[]>, floor: number): Promise<{
|
|
11
|
+
confirmed: string[];
|
|
12
|
+
unconfirmed: string[];
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
export interface RingShiftDeps {
|
|
16
|
+
fretAdapter: ArachnodeFretAdapter;
|
|
17
|
+
ringSelector: RingSelector;
|
|
18
|
+
fret: FretService;
|
|
19
|
+
partitionDetector: PartitionDetector;
|
|
20
|
+
/** Confirms a shed block replicated to ≥ N qualifying holders (Phase B). */
|
|
21
|
+
confirmer: ReplicationConfirmer;
|
|
22
|
+
/** The blocks this node physically holds — the candidate set the shed range is drawn from. */
|
|
23
|
+
ownedBlocks: Set<string>;
|
|
24
|
+
/** This node's own peer id string (excluded from the qualifying-holder count). */
|
|
25
|
+
selfPeerId: string;
|
|
26
|
+
/** The replication floor `N` (e.g. `RebalanceMonitor.getCohortSize`), read fresh per shift. */
|
|
27
|
+
getFloor: () => number;
|
|
28
|
+
/**
|
|
29
|
+
* Called with the shed block ids the instant Phase C releases them. The wiring stops serving +
|
|
30
|
+
* spreading them (untrack) and marks their local bytes GC-eligible — nothing may reclaim a shed
|
|
31
|
+
* range before this fires.
|
|
32
|
+
*/
|
|
33
|
+
onRelease: (blockIds: string[]) => void;
|
|
34
|
+
}
|
|
35
|
+
export interface RingShiftConfig {
|
|
36
|
+
/**
|
|
37
|
+
* How many extra candidate holders beyond the floor to request from FRET per shed block, giving
|
|
38
|
+
* headroom to exclude self and same-range movers while still finding `N` qualifying holders.
|
|
39
|
+
* Default 2.
|
|
40
|
+
*/
|
|
41
|
+
candidateMargin?: number;
|
|
42
|
+
}
|
|
43
|
+
/** The result of a single `executeShift` — what happened and why, for logging/tests. */
|
|
44
|
+
export type ShiftOutcome = {
|
|
45
|
+
status: 'moved-out';
|
|
46
|
+
from: number;
|
|
47
|
+
to: number;
|
|
48
|
+
released: string[];
|
|
49
|
+
} | {
|
|
50
|
+
status: 'moved-in';
|
|
51
|
+
from: number;
|
|
52
|
+
to: number;
|
|
53
|
+
} | {
|
|
54
|
+
status: 'rolled-back';
|
|
55
|
+
ring: number;
|
|
56
|
+
reason: string;
|
|
57
|
+
} | {
|
|
58
|
+
status: 'skipped';
|
|
59
|
+
reason: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Drives a single damped ring transition through the **advertise → confirm-replication → release**
|
|
63
|
+
* handoff (`docs/arachnode-ring-handoff.md` § Part 2), so a ring shift never drops a key below its
|
|
64
|
+
* replication floor `N`.
|
|
65
|
+
*
|
|
66
|
+
* - **Move-out** (`R → R+1`, sheds half its keyspace) runs all three phases: advertise the target
|
|
67
|
+
* ring while still serving the old range (Phase A), confirm every shed block is replicated to ≥ N
|
|
68
|
+
* qualifying post-move holders (Phase B), then release the shed range (Phase C). Any Phase-B
|
|
69
|
+
* failure — partition, unreachable holders, floor unmet — rolls back to `active` at the old ring,
|
|
70
|
+
* keeping the range. No shed block is released unless EVERY shed block confirmed.
|
|
71
|
+
* - **Move-in** (`R → R-1`, gains keyspace, sheds nothing) is Phase A only: it advertises the inner
|
|
72
|
+
* ring so peers observe the membership change, then pulls the gained half via the restoration /
|
|
73
|
+
* rebalance path. The floor is never at risk from a mover that only gains, so there is no
|
|
74
|
+
* confirm/release.
|
|
75
|
+
*
|
|
76
|
+
* The trigger is the (damped) `RingSelector.shouldTransition()` decision; this class is the state
|
|
77
|
+
* machine that decision drives.
|
|
78
|
+
*/
|
|
79
|
+
export declare class RingShiftCoordinator {
|
|
80
|
+
private readonly deps;
|
|
81
|
+
private readonly candidateMargin;
|
|
82
|
+
/** Guards against a re-entrant shift if the driving interval overlaps a long confirm. */
|
|
83
|
+
private inFlight;
|
|
84
|
+
constructor(deps: RingShiftDeps, config?: RingShiftConfig);
|
|
85
|
+
/**
|
|
86
|
+
* Execute one single-step ring transition. No-op (`skipped`) if a shift is already in flight or the
|
|
87
|
+
* node is already `moving`. `direction`/`newRingDepth` come straight from the damped
|
|
88
|
+
* `shouldTransition()` trigger.
|
|
89
|
+
*/
|
|
90
|
+
executeShift(transition: {
|
|
91
|
+
direction: 'in' | 'out';
|
|
92
|
+
newRingDepth: number;
|
|
93
|
+
}): Promise<ShiftOutcome>;
|
|
94
|
+
/**
|
|
95
|
+
* Reconcile a stale `status='moving'` advertisement left by a crash between advertise (Phase A) and
|
|
96
|
+
* release (Phase C). The crashed node never ran Phase C, so it never released the shed range and is
|
|
97
|
+
* still responsible for its OLD range: restore that range and refresh status to `active`. A node
|
|
98
|
+
* whose advertised status is not `moving` needs no reconciliation. Called once at startup. See
|
|
99
|
+
* `docs/arachnode-ring-handoff.md` § Part 3 (crash mid-handoff).
|
|
100
|
+
*/
|
|
101
|
+
reconcileOnStart(): {
|
|
102
|
+
reconciled: boolean;
|
|
103
|
+
};
|
|
104
|
+
private moveOut;
|
|
105
|
+
/**
|
|
106
|
+
* Phase B. Succeeds only when EVERY shed block is confirmed replicated to ≥ `floor` qualifying
|
|
107
|
+
* holders. Aborts (no partial release) on a detected partition or any unconfirmed block.
|
|
108
|
+
*/
|
|
109
|
+
private confirmShedRange;
|
|
110
|
+
/**
|
|
111
|
+
* The shed range: blocks this node holds that its OLD partition covers but its NEW (target)
|
|
112
|
+
* partition does not — i.e. the half of its slice it stops covering by moving out.
|
|
113
|
+
*/
|
|
114
|
+
private computeShedBlocks;
|
|
115
|
+
/**
|
|
116
|
+
* For each shed block, the qualifying post-move holders to confirm against: the FRET cohort around
|
|
117
|
+
* the block minus self and minus any peer that does not still cover the block under its OWN
|
|
118
|
+
* advertised (target) partition — which excludes same-range movers ({@link qualifiesForFloor}). A
|
|
119
|
+
* candidate with no advertised Arachnode info is excluded (its coverage cannot be verified).
|
|
120
|
+
*/
|
|
121
|
+
private buildOwnersMap;
|
|
122
|
+
private moveIn;
|
|
123
|
+
/** Restore the pre-move advertisement (old ring/partition, `active`, no `moveFrom`). */
|
|
124
|
+
private rollback;
|
|
125
|
+
/** Strip the transient `moveFrom` field so an `active` advertisement never carries move state. */
|
|
126
|
+
private clearMove;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=ring-shift-coordinator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ring-shift-coordinator.d.ts","sourceRoot":"","sources":["../../../src/storage/ring-shift-coordinator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,KAAK,EAAE,oBAAoB,EAAiB,MAAM,6BAA6B,CAAC;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAOvD;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACpC,iBAAiB,CAChB,QAAQ,EAAE,MAAM,EAAE,EAClB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAC7B,KAAK,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,aAAa;IAC7B,WAAW,EAAE,oBAAoB,CAAC;IAClC,YAAY,EAAE,YAAY,CAAC;IAC3B,IAAI,EAAE,WAAW,CAAC;IAClB,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,4EAA4E;IAC5E,SAAS,EAAE,oBAAoB,CAAC;IAChC,8FAA8F;IAC9F,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB,+FAA+F;IAC/F,QAAQ,EAAE,MAAM,MAAM,CAAC;IACvB;;;;OAIG;IACH,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wFAAwF;AACxF,MAAM,MAAM,YAAY,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GACrE;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,oBAAoB;IAM/B,OAAO,CAAC,QAAQ,CAAC,IAAI;IALtB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,yFAAyF;IACzF,OAAO,CAAC,QAAQ,CAAS;gBAGP,IAAI,EAAE,aAAa,EACpC,MAAM,GAAE,eAAoB;IAK7B;;;;OAIG;IACG,YAAY,CAAC,UAAU,EAAE;QAAE,SAAS,EAAE,IAAI,GAAG,KAAK,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IAmBxG;;;;;;OAMG;IACH,gBAAgB,IAAI;QAAE,UAAU,EAAE,OAAO,CAAA;KAAE;YAiB7B,OAAO;IAgCrB;;;OAGG;YACW,gBAAgB;IAiB9B;;;OAGG;YACW,iBAAiB;IAc/B;;;;;OAKG;YACW,cAAc;YAkBd,MAAM;IAapB,wFAAwF;IACxF,OAAO,CAAC,QAAQ;IAkBhB,kGAAkG;IAClG,OAAO,CAAC,SAAS;CAIjB"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { hashKey } from 'p2p-fret';
|
|
2
|
+
import { partitionCovers, qualifiesForFloor } from './arachnode-partition.js';
|
|
3
|
+
import { createLogger } from '../logger.js';
|
|
4
|
+
const log = createLogger('ring-shift');
|
|
5
|
+
const textEncoder = new TextEncoder();
|
|
6
|
+
/**
|
|
7
|
+
* Drives a single damped ring transition through the **advertise → confirm-replication → release**
|
|
8
|
+
* handoff (`docs/arachnode-ring-handoff.md` § Part 2), so a ring shift never drops a key below its
|
|
9
|
+
* replication floor `N`.
|
|
10
|
+
*
|
|
11
|
+
* - **Move-out** (`R → R+1`, sheds half its keyspace) runs all three phases: advertise the target
|
|
12
|
+
* ring while still serving the old range (Phase A), confirm every shed block is replicated to ≥ N
|
|
13
|
+
* qualifying post-move holders (Phase B), then release the shed range (Phase C). Any Phase-B
|
|
14
|
+
* failure — partition, unreachable holders, floor unmet — rolls back to `active` at the old ring,
|
|
15
|
+
* keeping the range. No shed block is released unless EVERY shed block confirmed.
|
|
16
|
+
* - **Move-in** (`R → R-1`, gains keyspace, sheds nothing) is Phase A only: it advertises the inner
|
|
17
|
+
* ring so peers observe the membership change, then pulls the gained half via the restoration /
|
|
18
|
+
* rebalance path. The floor is never at risk from a mover that only gains, so there is no
|
|
19
|
+
* confirm/release.
|
|
20
|
+
*
|
|
21
|
+
* The trigger is the (damped) `RingSelector.shouldTransition()` decision; this class is the state
|
|
22
|
+
* machine that decision drives.
|
|
23
|
+
*/
|
|
24
|
+
export class RingShiftCoordinator {
|
|
25
|
+
deps;
|
|
26
|
+
candidateMargin;
|
|
27
|
+
/** Guards against a re-entrant shift if the driving interval overlaps a long confirm. */
|
|
28
|
+
inFlight = false;
|
|
29
|
+
constructor(deps, config = {}) {
|
|
30
|
+
this.deps = deps;
|
|
31
|
+
this.candidateMargin = config.candidateMargin ?? 2;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Execute one single-step ring transition. No-op (`skipped`) if a shift is already in flight or the
|
|
35
|
+
* node is already `moving`. `direction`/`newRingDepth` come straight from the damped
|
|
36
|
+
* `shouldTransition()` trigger.
|
|
37
|
+
*/
|
|
38
|
+
async executeShift(transition) {
|
|
39
|
+
if (this.inFlight) {
|
|
40
|
+
return { status: 'skipped', reason: 'shift-in-flight' };
|
|
41
|
+
}
|
|
42
|
+
const current = this.deps.fretAdapter.getMyArachnodeInfo();
|
|
43
|
+
if (current?.status === 'moving') {
|
|
44
|
+
return { status: 'skipped', reason: 'already-moving' };
|
|
45
|
+
}
|
|
46
|
+
this.inFlight = true;
|
|
47
|
+
try {
|
|
48
|
+
return transition.direction === 'out'
|
|
49
|
+
? await this.moveOut(transition.newRingDepth, current)
|
|
50
|
+
: await this.moveIn(transition.newRingDepth, current);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
this.inFlight = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reconcile a stale `status='moving'` advertisement left by a crash between advertise (Phase A) and
|
|
58
|
+
* release (Phase C). The crashed node never ran Phase C, so it never released the shed range and is
|
|
59
|
+
* still responsible for its OLD range: restore that range and refresh status to `active`. A node
|
|
60
|
+
* whose advertised status is not `moving` needs no reconciliation. Called once at startup. See
|
|
61
|
+
* `docs/arachnode-ring-handoff.md` § Part 3 (crash mid-handoff).
|
|
62
|
+
*/
|
|
63
|
+
reconcileOnStart() {
|
|
64
|
+
const info = this.deps.fretAdapter.getMyArachnodeInfo();
|
|
65
|
+
if (!info || info.status !== 'moving') {
|
|
66
|
+
return { reconciled: false };
|
|
67
|
+
}
|
|
68
|
+
// Never released → still responsible for the old range. Resume serving it, active at the old ring.
|
|
69
|
+
this.deps.fretAdapter.setArachnodeInfo(info.moveFrom
|
|
70
|
+
? this.clearMove({ ...info, ringDepth: info.moveFrom.ringDepth, partition: info.moveFrom.partition, status: 'active' })
|
|
71
|
+
: this.clearMove({ ...info, status: 'active' }));
|
|
72
|
+
log('reconcile:resumed-old-range ring=%d', info.moveFrom?.ringDepth ?? info.ringDepth);
|
|
73
|
+
return { reconciled: true };
|
|
74
|
+
}
|
|
75
|
+
// --- Move-out: advertise → confirm → release ---
|
|
76
|
+
async moveOut(newRingDepth, oldInfo) {
|
|
77
|
+
const oldRing = oldInfo?.ringDepth ?? 0;
|
|
78
|
+
// Phase A — advertise the target ring; KEEP serving the old range (moveFrom retains it).
|
|
79
|
+
const target = await this.deps.ringSelector.createArachnodeInfo(this.deps.selfPeerId, newRingDepth);
|
|
80
|
+
const movingInfo = {
|
|
81
|
+
...target,
|
|
82
|
+
status: 'moving',
|
|
83
|
+
moveFrom: { ringDepth: oldRing, partition: oldInfo?.partition }
|
|
84
|
+
};
|
|
85
|
+
this.deps.fretAdapter.setArachnodeInfo(movingInfo);
|
|
86
|
+
log('phaseA:advertise from=%d to=%d', oldRing, newRingDepth);
|
|
87
|
+
// Phase B — confirm the shed range replicated to ≥ N qualifying holders.
|
|
88
|
+
const shed = await this.computeShedBlocks(oldInfo?.partition, target.partition);
|
|
89
|
+
const floor = Math.max(1, this.deps.getFloor());
|
|
90
|
+
const confirm = await this.confirmShedRange(shed, floor);
|
|
91
|
+
if (!confirm.ok) {
|
|
92
|
+
this.rollback(oldInfo);
|
|
93
|
+
log('phaseB:abort reason=%s → rolled-back to ring=%d', confirm.reason, oldRing);
|
|
94
|
+
return { status: 'rolled-back', ring: oldRing, reason: confirm.reason };
|
|
95
|
+
}
|
|
96
|
+
// Phase C — release: active at the new ring (moveFrom cleared), stop serving/spreading + GC-eligible.
|
|
97
|
+
this.deps.fretAdapter.setArachnodeInfo(this.clearMove({ ...target, status: 'active' }));
|
|
98
|
+
if (shed.length > 0) {
|
|
99
|
+
this.deps.onRelease(shed);
|
|
100
|
+
}
|
|
101
|
+
log('phaseC:release from=%d to=%d shed=%d', oldRing, newRingDepth, shed.length);
|
|
102
|
+
return { status: 'moved-out', from: oldRing, to: newRingDepth, released: shed };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Phase B. Succeeds only when EVERY shed block is confirmed replicated to ≥ `floor` qualifying
|
|
106
|
+
* holders. Aborts (no partial release) on a detected partition or any unconfirmed block.
|
|
107
|
+
*/
|
|
108
|
+
async confirmShedRange(shed, floor) {
|
|
109
|
+
if (shed.length === 0) {
|
|
110
|
+
return { ok: true }; // nothing to shed → trivially safe
|
|
111
|
+
}
|
|
112
|
+
if (this.deps.partitionDetector.detectPartition()) {
|
|
113
|
+
return { ok: false, reason: 'partition' };
|
|
114
|
+
}
|
|
115
|
+
const owners = await this.buildOwnersMap(shed, floor);
|
|
116
|
+
const { confirmed, unconfirmed } = await this.deps.confirmer.confirmReplicated(shed, owners, floor);
|
|
117
|
+
if (unconfirmed.length > 0) {
|
|
118
|
+
return { ok: false, reason: `unconfirmed:${unconfirmed.length}` };
|
|
119
|
+
}
|
|
120
|
+
// Belt-and-suspenders: every shed block must be in `confirmed` before any is released.
|
|
121
|
+
return confirmed.length === shed.length ? { ok: true } : { ok: false, reason: 'incomplete' };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The shed range: blocks this node holds that its OLD partition covers but its NEW (target)
|
|
125
|
+
* partition does not — i.e. the half of its slice it stops covering by moving out.
|
|
126
|
+
*/
|
|
127
|
+
async computeShedBlocks(oldPartition, newPartition) {
|
|
128
|
+
const shed = [];
|
|
129
|
+
for (const blockId of this.deps.ownedBlocks) {
|
|
130
|
+
const coord = await hashKey(textEncoder.encode(blockId));
|
|
131
|
+
if (partitionCovers(oldPartition, coord) && !partitionCovers(newPartition, coord)) {
|
|
132
|
+
shed.push(blockId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return shed;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* For each shed block, the qualifying post-move holders to confirm against: the FRET cohort around
|
|
139
|
+
* the block minus self and minus any peer that does not still cover the block under its OWN
|
|
140
|
+
* advertised (target) partition — which excludes same-range movers ({@link qualifiesForFloor}). A
|
|
141
|
+
* candidate with no advertised Arachnode info is excluded (its coverage cannot be verified).
|
|
142
|
+
*/
|
|
143
|
+
async buildOwnersMap(shed, floor) {
|
|
144
|
+
const owners = new Map();
|
|
145
|
+
const want = floor + 1 + this.candidateMargin; // headroom for self + excluded movers
|
|
146
|
+
for (const blockId of shed) {
|
|
147
|
+
const coord = await hashKey(textEncoder.encode(blockId));
|
|
148
|
+
const cohort = this.deps.fret.assembleCohort(coord, want);
|
|
149
|
+
const qualifying = cohort.filter(peerId => {
|
|
150
|
+
if (peerId === this.deps.selfPeerId)
|
|
151
|
+
return false;
|
|
152
|
+
const info = this.deps.fretAdapter.getArachnodeInfo(peerId);
|
|
153
|
+
return info !== undefined && qualifiesForFloor(info, coord);
|
|
154
|
+
});
|
|
155
|
+
owners.set(blockId, qualifying);
|
|
156
|
+
}
|
|
157
|
+
return owners;
|
|
158
|
+
}
|
|
159
|
+
// --- Move-in: advertise only (sheds nothing) ---
|
|
160
|
+
async moveIn(newRingDepth, oldInfo) {
|
|
161
|
+
const oldRing = oldInfo?.ringDepth ?? 0;
|
|
162
|
+
// Sheds nothing: advertise the (broader) inner ring directly at `active`. The gained half is
|
|
163
|
+
// pulled by the restoration / rebalance path; the old holders keep serving until THEY confirm
|
|
164
|
+
// their own release, so the floor is never at risk from this mover.
|
|
165
|
+
const target = await this.deps.ringSelector.createArachnodeInfo(this.deps.selfPeerId, newRingDepth);
|
|
166
|
+
this.deps.fretAdapter.setArachnodeInfo(this.clearMove({ ...target, status: 'active' }));
|
|
167
|
+
log('moveIn:advertise from=%d to=%d', oldRing, newRingDepth);
|
|
168
|
+
return { status: 'moved-in', from: oldRing, to: newRingDepth };
|
|
169
|
+
}
|
|
170
|
+
// --- Shared helpers ---
|
|
171
|
+
/** Restore the pre-move advertisement (old ring/partition, `active`, no `moveFrom`). */
|
|
172
|
+
rollback(oldInfo) {
|
|
173
|
+
if (oldInfo) {
|
|
174
|
+
this.deps.fretAdapter.setArachnodeInfo(this.clearMove({ ...oldInfo, status: 'active' }));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// No prior advertisement: the node's "old range" is the whole keyspace (ring 0). Phase A has
|
|
178
|
+
// already replaced self with the `moving` target, so a bare `setStatus('active')` would leave
|
|
179
|
+
// the aborted NARROWER target ring/partition (and a stray `moveFrom`) advertised as active —
|
|
180
|
+
// dropping coverage of the shed range with nothing confirmed elsewhere, the exact floor
|
|
181
|
+
// violation this handoff exists to prevent. Restore a CLEAN active ring-0 advertisement instead.
|
|
182
|
+
const cur = this.deps.fretAdapter.getMyArachnodeInfo();
|
|
183
|
+
if (cur) {
|
|
184
|
+
this.deps.fretAdapter.setArachnodeInfo(this.clearMove({ ...cur, ringDepth: 0, partition: undefined, status: 'active' }));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
this.deps.fretAdapter.setStatus('active');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Strip the transient `moveFrom` field so an `active` advertisement never carries move state. */
|
|
191
|
+
clearMove(info) {
|
|
192
|
+
const { moveFrom: _drop, ...rest } = info;
|
|
193
|
+
return rest;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=ring-shift-coordinator.js.map
|