@optimystic/db-p2p 0.14.0 → 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
|
@@ -112,6 +112,8 @@ export interface MatchmakingQueryServeDeps {
|
|
|
112
112
|
readonly gate?: (from: PeerId, topicId: Uint8Array) => boolean;
|
|
113
113
|
/** Per-frame ceiling for encode/decode; default {@link DEFAULT_STREAM_MAX_BYTES}. */
|
|
114
114
|
readonly maxBytes?: number;
|
|
115
|
+
/** Wall clock (unix ms) stamped on the query-accounting bump; default {@link Date.now}. Injectable for tests. */
|
|
116
|
+
readonly clock?: () => number;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
/**
|
|
@@ -151,6 +153,13 @@ export function createMatchmakingQueryHandler(
|
|
|
151
153
|
return undefined;
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
// Query accounting (matchmaking-query-accounting-seam): a served query bumps `queriesPerMin` for the
|
|
157
|
+
// topic on the served coord's TrafficCounters; surfaces in a later reply's snapshot (lags one round).
|
|
158
|
+
// Sits AFTER the gate + no-engine guards (a dropped / unserved query never inflates the barometer) and
|
|
159
|
+
// BEFORE the reply build (a query that resolved a serving engine is counted on serve, even if signing
|
|
160
|
+
// later fails transiently). A decode failure threw before `topicId` resolved, so it never counts.
|
|
161
|
+
engine.recordQuery(topicId, (deps.clock ?? Date.now)());
|
|
162
|
+
|
|
154
163
|
// Build the reply from a single synchronous read (records + traffic + epoch) so a concurrent gossip
|
|
155
164
|
// round cannot tear the snapshot between read and sign. `handleMatchmakingQuery` forwards each record's
|
|
156
165
|
// fields verbatim through the pure `evaluateQuery` (capability filter + limit truncation + entry
|
|
@@ -72,6 +72,7 @@ export class NetworkManagerService implements Startable {
|
|
|
72
72
|
initRebalanceMonitor(
|
|
73
73
|
partitionDetector: PartitionDetector,
|
|
74
74
|
fretAdapter: ArachnodeFretAdapter,
|
|
75
|
+
trackedBlocks: Set<string>,
|
|
75
76
|
config?: RebalanceMonitorConfig
|
|
76
77
|
): RebalanceMonitor {
|
|
77
78
|
const libp2p = this.getLibp2p()
|
|
@@ -79,8 +80,10 @@ export class NetworkManagerService implements Startable {
|
|
|
79
80
|
if (!libp2p || !fret) {
|
|
80
81
|
throw new Error('Cannot init RebalanceMonitor: libp2p or FRET not available')
|
|
81
82
|
}
|
|
83
|
+
// trackedBlocks is the shared owned-block set: the node passes the SAME Set instance into
|
|
84
|
+
// both monitor inits so spread and rebalance act on one source of truth and never drift.
|
|
82
85
|
this.rebalanceMonitor = new RebalanceMonitor(
|
|
83
|
-
{ libp2p, fret, partitionDetector, fretAdapter },
|
|
86
|
+
{ libp2p, fret, partitionDetector, fretAdapter, trackedBlocks },
|
|
84
87
|
config
|
|
85
88
|
)
|
|
86
89
|
return this.rebalanceMonitor
|
|
@@ -102,6 +105,7 @@ export class NetworkManagerService implements Startable {
|
|
|
102
105
|
peerNetwork: SpreadOnChurnDeps['peerNetwork'],
|
|
103
106
|
clusterSize: number,
|
|
104
107
|
protocolPrefix: string,
|
|
108
|
+
trackedBlocks: Set<string>,
|
|
105
109
|
config?: Partial<SpreadOnChurnConfig>
|
|
106
110
|
): SpreadOnChurnMonitor {
|
|
107
111
|
const libp2p = this.getLibp2p()
|
|
@@ -113,8 +117,10 @@ export class NetworkManagerService implements Startable {
|
|
|
113
117
|
// BlockTransferClient with it, and the receiving node registers the block-transfer
|
|
114
118
|
// handler under /optimystic/<networkName>. A missing/empty prefix dials the wrong
|
|
115
119
|
// protocol and every churn push fails to connect.
|
|
120
|
+
// trackedBlocks is the shared owned-block set: the node passes the SAME Set instance into
|
|
121
|
+
// both monitor inits so spread and rebalance act on one source of truth and never drift.
|
|
116
122
|
this.spreadOnChurnMonitor = new SpreadOnChurnMonitor(
|
|
117
|
-
{ libp2p, fret, partitionDetector, repo, peerNetwork, clusterSize, protocolPrefix },
|
|
123
|
+
{ libp2p, fret, partitionDetector, repo, peerNetwork, clusterSize, protocolPrefix, trackedBlocks },
|
|
118
124
|
config
|
|
119
125
|
)
|
|
120
126
|
return this.spreadOnChurnMonitor
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { IRawStorage } from './storage/i-raw-storage.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Seed the resilience monitors' shared owned-block set from blocks already
|
|
5
|
+
* durable in raw storage from a previous process run.
|
|
6
|
+
*
|
|
7
|
+
* The live owned-block feed (`storageRepo.onAnyCollectionChange`, wired in
|
|
8
|
+
* `createLibp2pNodeBase`) only fires on NEW commits and RECEIVED replicas, so a
|
|
9
|
+
* block that was already durable on disk from a previous run is not tracked
|
|
10
|
+
* after a restart until it happens to be committed or replicated again — a
|
|
11
|
+
* freshly restarted node would under-protect exactly the data it already holds.
|
|
12
|
+
* This scan closes that gap by enumerating the metadata store (one id per block
|
|
13
|
+
* with a committed revision or persisted replica — the same population the live
|
|
14
|
+
* feed tracks) and adding each id to `ownedBlocks`.
|
|
15
|
+
*
|
|
16
|
+
* Called AFTER the live feed is already subscribed, so a block committed/replicated
|
|
17
|
+
* mid-scan is independently caught by the feed; `Set.add` is idempotent, so the
|
|
18
|
+
* overlap is harmless. `isStopping()` is checked each iteration so a scan over a
|
|
19
|
+
* huge store aborts promptly when the node is stopping (the `for await` then calls
|
|
20
|
+
* the iterator's `return()` to release the backend cursor). A cooperative yield
|
|
21
|
+
* every `yieldEvery` ids keeps a tight add-loop from monopolizing an event-loop tick.
|
|
22
|
+
*
|
|
23
|
+
* Backends that omit `listBlockIds` (or an in-memory backend with nothing durable
|
|
24
|
+
* across a restart) yield no seed — the monitors still populate over time via the
|
|
25
|
+
* live feed.
|
|
26
|
+
*/
|
|
27
|
+
export async function seedOwnedBlocksFromStorage(
|
|
28
|
+
rawStorage: Pick<IRawStorage, 'listBlockIds'>,
|
|
29
|
+
ownedBlocks: Set<string>,
|
|
30
|
+
isStopping: () => boolean,
|
|
31
|
+
yieldEvery = 1000,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
if (typeof rawStorage.listBlockIds !== 'function') return;
|
|
34
|
+
let n = 0;
|
|
35
|
+
for await (const blockId of rawStorage.listBlockIds()) {
|
|
36
|
+
if (isStopping()) break;
|
|
37
|
+
ownedBlocks.add(blockId);
|
|
38
|
+
if (++n % yieldEvery === 0) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/protocol-client.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Stream as Libp2pStream } from '@libp2p/interface';
|
|
|
4
4
|
import type { PeerId, IPeerNetwork } from '@optimystic/db-core';
|
|
5
5
|
import { first } from './it-utility.js';
|
|
6
6
|
import { createLogger } from './logger.js';
|
|
7
|
+
import { MAX_BLOCK_MESSAGE_BYTES } from './protocol-limits.js';
|
|
7
8
|
|
|
8
9
|
const log = createLogger('protocol-client');
|
|
9
10
|
|
|
@@ -50,7 +51,7 @@ export class ProtocolClient {
|
|
|
50
51
|
protected async processMessage<T>(
|
|
51
52
|
message: unknown,
|
|
52
53
|
protocol: string,
|
|
53
|
-
options?: { signal?: AbortSignal; correlationId?: string; dialTimeoutMs?: number; responseTimeoutMs?: number }
|
|
54
|
+
options?: { signal?: AbortSignal; correlationId?: string; dialTimeoutMs?: number; responseTimeoutMs?: number; maxDataLength?: number }
|
|
54
55
|
): Promise<T> {
|
|
55
56
|
const peer = this.peerId.toString();
|
|
56
57
|
const cid = options?.correlationId;
|
|
@@ -144,11 +145,17 @@ export class ProtocolClient {
|
|
|
144
145
|
stream.send(chunk);
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
// Read the response from the stream (which is now directly AsyncIterable)
|
|
148
|
+
// Read the response from the stream (which is now directly AsyncIterable).
|
|
149
|
+
// Cap the response frame size so a peer can't flood the client with an
|
|
150
|
+
// oversized reply; each caller passes the cap matching its response shape
|
|
151
|
+
// (control vs block), defaulting to the block cap so no existing caller
|
|
152
|
+
// regresses. An oversized frame rejects at the length-prefix (before
|
|
153
|
+
// allocation) and surfaces through the read's catch, tearing the stream down.
|
|
154
|
+
const maxDataLength = options?.maxDataLength ?? MAX_BLOCK_MESSAGE_BYTES;
|
|
148
155
|
let firstByte = true;
|
|
149
156
|
const source = pipe(
|
|
150
157
|
stream,
|
|
151
|
-
lpDecode,
|
|
158
|
+
(source) => lpDecode(source, { maxDataLength }),
|
|
152
159
|
async function* (source) {
|
|
153
160
|
for await (const data of source) {
|
|
154
161
|
if (firstByte) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound-message size caps for the db-p2p consensus protocols.
|
|
3
|
+
*
|
|
4
|
+
* Every inbound stream handler frames its wire data with length-prefixed
|
|
5
|
+
* encoding (`it-length-prefixed`) and `JSON.parse`s each frame. Passing one of
|
|
6
|
+
* these constants as the decoder's `maxDataLength` makes an oversized frame
|
|
7
|
+
* reject at the length-prefix (before any allocation against the declared size)
|
|
8
|
+
* instead of buffering up to the library default (4 MiB) of attacker-controlled
|
|
9
|
+
* JSON per frame. Mirrors the existing precedent
|
|
10
|
+
* `DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024` in
|
|
11
|
+
* `packages/db-core/src/cohort-topic/wire/codec.ts`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Max size (bytes) of a single inbound framed message on a *control-plane*
|
|
16
|
+
* protocol — cluster records, dispute votes, sync requests. These carry a peer
|
|
17
|
+
* set + signatures + small metadata, never bulk block data.
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_CONTROL_MESSAGE_BYTES = 1 * 1024 * 1024; // 1 MiB
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Max size (bytes) of a single inbound framed message on a *block-carrying*
|
|
23
|
+
* protocol — repo operations (pend/commit transforms + block bodies),
|
|
24
|
+
* block-transfer push payloads (base64-inflated block data, multiple per
|
|
25
|
+
* request), and the block-bearing responses those protocols return.
|
|
26
|
+
*
|
|
27
|
+
* NOTE: the repo enforces no hard per-block byte ceiling today, so this is a
|
|
28
|
+
* heuristic. If a legitimate block/transform can exceed this, the transfer will
|
|
29
|
+
* be rejected — raise this constant (or thread a config override) rather than
|
|
30
|
+
* removing the cap. If the repo later grows a real max-block constant, derive
|
|
31
|
+
* this cap from it.
|
|
32
|
+
*/
|
|
33
|
+
export const MAX_BLOCK_MESSAGE_BYTES = 8 * 1024 * 1024; // 8 MiB
|
package/src/repo/client.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
import type { RepoMessage } from "@optimystic/db-core";
|
|
6
6
|
import { blockIdsForTransforms, blockIdToBytes } from "@optimystic/db-core";
|
|
7
7
|
import { ProtocolClient } from "../protocol-client.js";
|
|
8
|
+
import { MAX_BLOCK_MESSAGE_BYTES } from "../protocol-limits.js";
|
|
8
9
|
import { peerIdFromString } from "@libp2p/peer-id";
|
|
9
10
|
|
|
10
11
|
export class RepoClient extends ProtocolClient implements IRepo {
|
|
@@ -94,6 +95,8 @@ export class RepoClient extends ProtocolClient implements IRepo {
|
|
|
94
95
|
signal: combinedSignal,
|
|
95
96
|
correlationId,
|
|
96
97
|
dialTimeoutMs: options?.dialTimeoutMs,
|
|
98
|
+
// A get response carries block data → block cap (not control).
|
|
99
|
+
maxDataLength: MAX_BLOCK_MESSAGE_BYTES,
|
|
97
100
|
})
|
|
98
101
|
} finally {
|
|
99
102
|
clearTimeout(timer)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { peerIdFromString } from "@libp2p/peer-id";
|
|
2
2
|
import type { ClusterRecord, IKeyNetwork, RepoMessage, BlockId, ClusterPeers, MessageOptions, ClusterConsensusConfig } from "@optimystic/db-core";
|
|
3
|
-
import {
|
|
4
|
-
import { sha256 } from "multiformats/hashes/sha2";
|
|
3
|
+
import { CURRENT_MEMBERSHIP_VERSION, computeClusterMessageHash, membershipDigest } from "@optimystic/db-core";
|
|
5
4
|
import { ClusterClient } from "../cluster/client.js";
|
|
6
5
|
import { Pending } from "@optimystic/db-core";
|
|
7
6
|
import type { PeerId } from "@libp2p/interface";
|
|
@@ -14,6 +13,34 @@ import type { ITransactionStateStore } from "../cluster/i-transaction-state-stor
|
|
|
14
13
|
|
|
15
14
|
const log = createLogger('cluster')
|
|
16
15
|
|
|
16
|
+
/** Cancel handle for an injected timer; cancels a not-yet-fired timer (safe no-op after fire/cancel). */
|
|
17
|
+
export type TimerCancel = () => void;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Production timer binding: a one-shot `setTimeout` whose handle is **unref'd** so a pending
|
|
21
|
+
* commit-retry (or the deferred transaction cleanup) never keeps an otherwise-idle process alive.
|
|
22
|
+
* The returned handle clears the timeout (idempotent). Mirrors the reactivity rotation
|
|
23
|
+
* re-registration scheduler's `defaultSetTimer` (see reactivity/rotation-rereg-scheduler.ts).
|
|
24
|
+
*/
|
|
25
|
+
function defaultSetTimer(fn: () => void, delayMs: number): TimerCancel {
|
|
26
|
+
const handle = setTimeout(fn, delayMs);
|
|
27
|
+
// An idle retry/cleanup timer must not pin a process (mirror rotation re-registration + push-state gossip).
|
|
28
|
+
(handle as { unref?: () => void }).unref?.();
|
|
29
|
+
return (): void => clearTimeout(handle);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Optional injection seam for deterministic time. Production leaves both undefined and gets
|
|
34
|
+
* `Date.now` + an unref'd `setTimeout`; tests inject a fake clock + timer queue so scheduled
|
|
35
|
+
* commit-retries fire in virtual (not wall-clock) time.
|
|
36
|
+
*/
|
|
37
|
+
export interface ClusterCoordinatorClock {
|
|
38
|
+
/** Clock (Unix ms). Defaults to `Date.now`. */
|
|
39
|
+
now?: () => number;
|
|
40
|
+
/** Schedule a one-shot timer, returning a cancel handle. Defaults to an unref'd `setTimeout`. */
|
|
41
|
+
setTimer?: (fn: () => void, delayMs: number) => TimerCancel;
|
|
42
|
+
}
|
|
43
|
+
|
|
17
44
|
/**
|
|
18
45
|
* Manages the state of cluster transactions for a specific block ID
|
|
19
46
|
*/
|
|
@@ -21,7 +48,7 @@ interface CommitRetryState {
|
|
|
21
48
|
pendingPeers: Set<string>;
|
|
22
49
|
attempt: number;
|
|
23
50
|
intervalMs: number;
|
|
24
|
-
|
|
51
|
+
cancel?: TimerCancel;
|
|
25
52
|
}
|
|
26
53
|
|
|
27
54
|
interface ClusterTransactionState {
|
|
@@ -42,6 +69,10 @@ export class ClusterCoordinator {
|
|
|
42
69
|
private readonly retryMaxIntervalMs: number;
|
|
43
70
|
private readonly retryMaxAttempts: number;
|
|
44
71
|
private readonly commitBroadcastImmediateRetries: number;
|
|
72
|
+
private readonly promiseImmediateRetries: number;
|
|
73
|
+
/** Injected clock/timer seam; production defaults to `Date.now` + unref'd `setTimeout`. */
|
|
74
|
+
private readonly now: () => number;
|
|
75
|
+
private readonly setTimer: (fn: () => void, delayMs: number) => TimerCancel;
|
|
45
76
|
|
|
46
77
|
constructor(
|
|
47
78
|
private readonly keyNetwork: IKeyNetwork,
|
|
@@ -54,31 +85,71 @@ export class ClusterCoordinator {
|
|
|
54
85
|
},
|
|
55
86
|
private readonly fretService?: FretService,
|
|
56
87
|
private readonly reputation?: IPeerReputation,
|
|
57
|
-
private readonly stateStore?: ITransactionStateStore
|
|
88
|
+
private readonly stateStore?: ITransactionStateStore,
|
|
89
|
+
clock?: ClusterCoordinatorClock
|
|
58
90
|
) {
|
|
59
91
|
this.retryInitialIntervalMs = cfg.commitBroadcastRetryInitialMs ?? 250;
|
|
60
92
|
this.retryBackoffFactor = cfg.commitBroadcastRetryBackoffFactor ?? 2;
|
|
61
93
|
this.retryMaxIntervalMs = cfg.commitBroadcastRetryMaxIntervalMs ?? 8000;
|
|
62
94
|
this.retryMaxAttempts = cfg.commitBroadcastRetryMaxAttempts ?? 5;
|
|
63
95
|
this.commitBroadcastImmediateRetries = cfg.commitBroadcastImmediateRetries ?? 1;
|
|
96
|
+
this.promiseImmediateRetries = cfg.promiseImmediateRetries ?? 1;
|
|
97
|
+
this.now = clock?.now ?? ((): number => Date.now());
|
|
98
|
+
this.setTimer = clock?.setTimer ?? defaultSetTimer;
|
|
64
99
|
}
|
|
65
100
|
|
|
66
101
|
/**
|
|
67
|
-
*
|
|
102
|
+
* Invoke one cluster member's `update`, retrying transient REMOTE failures up to
|
|
103
|
+
* `immediateRetries` times before surfacing the error. The local cluster is invoked
|
|
104
|
+
* exactly once — a local throw is a real fault (validation / merge / consensus), not a
|
|
105
|
+
* transient transport blip. A remote call rides a libp2p stream that a circuit-relay
|
|
106
|
+
* ("limited") connection can reset once a per-circuit cap or reservation lapses, which
|
|
107
|
+
* surfaces as a StreamResetError; an immediate retry on the (usually still-warm)
|
|
108
|
+
* connection recovers most of those without escalating the peer to a failure. Shared by
|
|
109
|
+
* the promise-collection, commit-collection, and commit-broadcast phases so all three
|
|
110
|
+
* react to a relayed reset the same way.
|
|
68
111
|
*/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
112
|
+
private async updateMember(peerIdStr: string, record: ClusterRecord, immediateRetries: number, phase: string): Promise<ClusterRecord> {
|
|
113
|
+
const isLocal = this.localCluster && peerIdStr === this.localCluster.peerId.toString();
|
|
114
|
+
if (isLocal) {
|
|
115
|
+
return await this.localCluster!.update(record);
|
|
116
|
+
}
|
|
117
|
+
const maxAttempts = 1 + Math.max(0, immediateRetries);
|
|
118
|
+
let lastError: unknown;
|
|
119
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
120
|
+
try {
|
|
121
|
+
return await this.createClusterClient(peerIdFromString(peerIdStr)).update(record);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
lastError = err;
|
|
124
|
+
if (attempt < maxAttempts) {
|
|
125
|
+
log('cluster-tx:member-update-retry', {
|
|
126
|
+
messageHash: record.messageHash,
|
|
127
|
+
peerId: peerIdStr,
|
|
128
|
+
phase,
|
|
129
|
+
attempt,
|
|
130
|
+
error: err instanceof Error ? err.message : String(err)
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
throw lastError;
|
|
76
136
|
}
|
|
77
137
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Creates a base58btc string hash uniquely identifying a transaction. For a v2 record the caller
|
|
140
|
+
* threads in the {@link membershipDigest} of the peer set so the responsible membership is bound into
|
|
141
|
+
* the identity (two different peer sets ⇒ two different hashes). Omitting `membershipDigestValue`
|
|
142
|
+
* reproduces the legacy v1 hash byte-for-byte.
|
|
143
|
+
*
|
|
144
|
+
* NOTE: the whole `message` is hashed (canonicalJson), so a transaction's advisory aged priority —
|
|
145
|
+
* which rides inside the pend operation as `pend.transaction.priority` (multi-collection) or
|
|
146
|
+
* `pend.priority` (single-collection) — is automatically covered here and by the derived
|
|
147
|
+
* promise/commit hashes. That is what makes priority integrity-protected in transit: a relaying peer
|
|
148
|
+
* cannot strip or inflate it without invalidating the message hash the members verify. No separate
|
|
149
|
+
* priority-hashing step is needed.
|
|
150
|
+
*/
|
|
151
|
+
private async createMessageHash(message: RepoMessage, membershipDigestValue?: string): Promise<string> {
|
|
152
|
+
return computeClusterMessageHash(message, membershipDigestValue);
|
|
82
153
|
}
|
|
83
154
|
|
|
84
155
|
/**
|
|
@@ -97,11 +168,15 @@ export class ClusterCoordinator {
|
|
|
97
168
|
}
|
|
98
169
|
}
|
|
99
170
|
|
|
100
|
-
private makeRecord(peers: ClusterPeers, messageHash: string, message: RepoMessage): ClusterRecord {
|
|
171
|
+
private makeRecord(peers: ClusterPeers, messageHash: string, message: RepoMessage, membershipDigestValue: string): ClusterRecord {
|
|
101
172
|
const peerCount = Object.keys(peers ?? {}).length;
|
|
102
173
|
const record: ClusterRecord = {
|
|
103
174
|
messageHash,
|
|
104
175
|
peers,
|
|
176
|
+
// v2: bind the responsible membership into the signed identity. messageHash was computed over
|
|
177
|
+
// this same digest, so a different peer set would have produced a different messageHash.
|
|
178
|
+
membershipVersion: CURRENT_MEMBERSHIP_VERSION,
|
|
179
|
+
membershipDigest: membershipDigestValue,
|
|
105
180
|
message,
|
|
106
181
|
coordinatingBlockIds: message.coordinatingBlockIds,
|
|
107
182
|
promises: {},
|
|
@@ -137,11 +212,16 @@ export class ClusterCoordinator {
|
|
|
137
212
|
// Get the cluster peers for this block
|
|
138
213
|
const peers = await this.getClusterForBlock(blockId);
|
|
139
214
|
|
|
140
|
-
//
|
|
141
|
-
|
|
215
|
+
// Bind the responsible membership into the transaction identity (v2): the digest is folded into
|
|
216
|
+
// the messageHash below, so two different peer sets produce two different messageHashes rather
|
|
217
|
+
// than one hash with a silent internal disagreement about who is responsible.
|
|
218
|
+
const membershipDigestValue = await membershipDigest(peers);
|
|
219
|
+
|
|
220
|
+
// Create a unique hash for this transaction (over message + membership digest)
|
|
221
|
+
const messageHash = await this.createMessageHash(message, membershipDigestValue);
|
|
142
222
|
|
|
143
223
|
// Create a cluster record for this transaction
|
|
144
|
-
const record = this.makeRecord(peers, messageHash, message);
|
|
224
|
+
const record = this.makeRecord(peers, messageHash, message, membershipDigestValue);
|
|
145
225
|
log('cluster-tx:start', {
|
|
146
226
|
messageHash,
|
|
147
227
|
blockId,
|
|
@@ -161,7 +241,7 @@ export class ClusterCoordinator {
|
|
|
161
241
|
messageHash,
|
|
162
242
|
record,
|
|
163
243
|
pending,
|
|
164
|
-
lastUpdate:
|
|
244
|
+
lastUpdate: this.now()
|
|
165
245
|
};
|
|
166
246
|
this.transactions.set(messageHash, state);
|
|
167
247
|
this.persistCoordinatorState(messageHash, record, 'promising');
|
|
@@ -192,7 +272,7 @@ export class ClusterCoordinator {
|
|
|
192
272
|
// Let the retry completion or abort handle cleanup
|
|
193
273
|
if (!stored?.retry) {
|
|
194
274
|
// Wait a bit before cleanup to allow any in-flight responses to arrive
|
|
195
|
-
|
|
275
|
+
this.setTimer(() => {
|
|
196
276
|
this.transactions.delete(messageHash);
|
|
197
277
|
this.deleteCoordinatorState(messageHash);
|
|
198
278
|
log('cluster-tx:transaction-remove', {
|
|
@@ -290,6 +370,11 @@ export class ClusterCoordinator {
|
|
|
290
370
|
rejectionCount,
|
|
291
371
|
peerCount
|
|
292
372
|
});
|
|
373
|
+
// [dispute-subsystem-dormant] Evidence is computed and persisted but initiateDispute() is
|
|
374
|
+
// intentionally NOT called here. Dispute origination stays dormant pending arbitrator-set
|
|
375
|
+
// anchoring — without it a forged synthetic cohort passes resolution.
|
|
376
|
+
// Gate: tickets/backlog/hardening/invalidation-live-wiring-requires-arbitrator-set-anchoring
|
|
377
|
+
// Wiring plan: tickets/backlog/feat-dispute-subsystem-live-activation
|
|
293
378
|
}
|
|
294
379
|
|
|
295
380
|
this.persistCoordinatorState(promised.record.messageHash, promised.record, 'committing');
|
|
@@ -331,13 +416,18 @@ export class ClusterCoordinator {
|
|
|
331
416
|
}
|
|
332
417
|
}
|
|
333
418
|
|
|
334
|
-
// Fallback:
|
|
335
|
-
//
|
|
336
|
-
|
|
419
|
+
// Fallback: with no confident network-size estimate, fail CLOSED by default.
|
|
420
|
+
// An undersized cluster with no way to justify its size is unsafe (a lone/
|
|
421
|
+
// near-lone node could rubber-stamp its own writes), so reject unless the
|
|
422
|
+
// operator has explicitly opted in via allowUnvalidatedSmallCluster (e.g.
|
|
423
|
+
// single-node / local dev knowingly running below the floor).
|
|
424
|
+
const admit = this.cfg.allowUnvalidatedSmallCluster ?? false;
|
|
425
|
+
log('cluster-tx:small-cluster-no-confident-estimate', {
|
|
337
426
|
localSize,
|
|
338
|
-
reason: 'no-confident-network-size-estimate'
|
|
427
|
+
reason: 'no-confident-network-size-estimate',
|
|
428
|
+
admit
|
|
339
429
|
});
|
|
340
|
-
return
|
|
430
|
+
return admit;
|
|
341
431
|
}
|
|
342
432
|
|
|
343
433
|
/**
|
|
@@ -353,14 +443,15 @@ export class ClusterCoordinator {
|
|
|
353
443
|
}));
|
|
354
444
|
log('cluster-tx:promise-peers', { messageHash: record.messageHash, peers: peerDetail });
|
|
355
445
|
}
|
|
356
|
-
// For each peer, create a client and request a promise
|
|
446
|
+
// For each peer, create a client and request a promise. A remote promise rides
|
|
447
|
+
// a libp2p stream that a relayed (limited) connection can reset transiently, so
|
|
448
|
+
// each remote request gets `promiseImmediateRetries` in-line re-attempts before
|
|
449
|
+
// it counts as a failure — without this a single relayed reset drops the peer and
|
|
450
|
+
// sinks super-majority (the commit broadcast already has the same guard).
|
|
357
451
|
const promiseRequests = peerIds.map(peerIdStr => {
|
|
358
452
|
const isLocal = this.localCluster && peerIdStr === this.localCluster.peerId.toString();
|
|
359
453
|
log('cluster-tx:promise-request', { messageHash: record.messageHash, peerId: peerIdStr, isLocal });
|
|
360
|
-
|
|
361
|
-
? this.localCluster!.update(record)
|
|
362
|
-
: this.createClusterClient(peerIdFromString(peerIdStr)).update(record);
|
|
363
|
-
return new Pending(promise);
|
|
454
|
+
return new Pending(this.updateMember(peerIdStr, record, this.promiseImmediateRetries, 'promise'));
|
|
364
455
|
});
|
|
365
456
|
|
|
366
457
|
// Wait for all promises to complete
|
|
@@ -455,6 +546,10 @@ export class ClusterCoordinator {
|
|
|
455
546
|
const commitPayload = {
|
|
456
547
|
...record
|
|
457
548
|
};
|
|
549
|
+
// No per-peer immediate retry here: a commit-collection failure is recovered
|
|
550
|
+
// downstream by broadcastMergedRecord's in-line retry and the scheduled
|
|
551
|
+
// commit-retry timer. (The promise phase has no such backstop, which is why
|
|
552
|
+
// collectPromises gets the immediate retry instead.)
|
|
458
553
|
const commitRequests = peerIds.map(peerIdStr => {
|
|
459
554
|
const isLocal = this.localCluster && peerIdStr === this.localCluster.peerId.toString();
|
|
460
555
|
log('cluster-tx:commit-request', { messageHash: record.messageHash, peerId: peerIdStr, isLocal });
|
|
@@ -560,46 +655,18 @@ export class ClusterCoordinator {
|
|
|
560
655
|
* Local cluster is invoked exactly once — local failures are fatal, not transient.
|
|
561
656
|
*/
|
|
562
657
|
private async broadcastMergedRecord(record: ClusterRecord, peerIds: string[]): Promise<{ failures: string[] }> {
|
|
563
|
-
const maxAttempts = 1 + Math.max(0, this.commitBroadcastImmediateRetries);
|
|
564
658
|
const results = await Promise.all(peerIds.map(async peerIdStr => {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
});
|
|
576
|
-
return { peerId: peerIdStr, success: false as const };
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
let lastError: unknown;
|
|
580
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
581
|
-
try {
|
|
582
|
-
await this.createClusterClient(peerIdFromString(peerIdStr)).update(record);
|
|
583
|
-
return { peerId: peerIdStr, success: true as const };
|
|
584
|
-
} catch (err) {
|
|
585
|
-
lastError = err;
|
|
586
|
-
if (attempt < maxAttempts) {
|
|
587
|
-
log('cluster-tx:consensus-broadcast-retry', {
|
|
588
|
-
messageHash: record.messageHash,
|
|
589
|
-
peerId: peerIdStr,
|
|
590
|
-
attempt,
|
|
591
|
-
error: (err as Error).message
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
}
|
|
659
|
+
try {
|
|
660
|
+
await this.updateMember(peerIdStr, record, this.commitBroadcastImmediateRetries, 'commit-broadcast');
|
|
661
|
+
return { peerId: peerIdStr, success: true as const };
|
|
662
|
+
} catch (err) {
|
|
663
|
+
log('cluster-tx:consensus-broadcast-error', {
|
|
664
|
+
messageHash: record.messageHash,
|
|
665
|
+
peerId: peerIdStr,
|
|
666
|
+
error: err instanceof Error ? err.message : String(err)
|
|
667
|
+
});
|
|
668
|
+
return { peerId: peerIdStr, success: false as const };
|
|
595
669
|
}
|
|
596
|
-
log('cluster-tx:consensus-broadcast-error', {
|
|
597
|
-
messageHash: record.messageHash,
|
|
598
|
-
peerId: peerIdStr,
|
|
599
|
-
attempts: maxAttempts,
|
|
600
|
-
error: lastError instanceof Error ? lastError.message : String(lastError)
|
|
601
|
-
});
|
|
602
|
-
return { peerId: peerIdStr, success: false as const };
|
|
603
670
|
}));
|
|
604
671
|
const failures = results.filter(r => !r.success).map(r => r.peerId);
|
|
605
672
|
return { failures };
|
|
@@ -612,7 +679,7 @@ export class ClusterCoordinator {
|
|
|
612
679
|
return;
|
|
613
680
|
}
|
|
614
681
|
state.record = { ...record };
|
|
615
|
-
state.lastUpdate =
|
|
682
|
+
state.lastUpdate = this.now();
|
|
616
683
|
log('cluster-tx:transaction-update', {
|
|
617
684
|
messageHash: record.messageHash,
|
|
618
685
|
stage,
|
|
@@ -638,17 +705,15 @@ export class ClusterCoordinator {
|
|
|
638
705
|
}
|
|
639
706
|
const pendingPeers = new Set(missingPeers);
|
|
640
707
|
const baseInterval = existing ? Math.min(existing.intervalMs * this.retryBackoffFactor, this.retryMaxIntervalMs) : this.retryInitialIntervalMs;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
const timer = setTimeout(() => {
|
|
708
|
+
existing?.cancel?.();
|
|
709
|
+
const cancel = this.setTimer(() => {
|
|
645
710
|
void this.retryCommits(messageHash);
|
|
646
711
|
}, baseInterval);
|
|
647
712
|
state.retry = {
|
|
648
713
|
pendingPeers,
|
|
649
714
|
attempt: nextAttempt,
|
|
650
715
|
intervalMs: baseInterval,
|
|
651
|
-
|
|
716
|
+
cancel
|
|
652
717
|
};
|
|
653
718
|
this.persistCoordinatorState(messageHash, state.record, 'broadcasting', {
|
|
654
719
|
pendingPeers: Array.from(pendingPeers),
|
|
@@ -713,12 +778,10 @@ export class ClusterCoordinator {
|
|
|
713
778
|
if (!state?.retry) {
|
|
714
779
|
return;
|
|
715
780
|
}
|
|
716
|
-
|
|
717
|
-
clearTimeout(state.retry.timer);
|
|
718
|
-
}
|
|
781
|
+
state.retry.cancel?.();
|
|
719
782
|
state.retry = undefined;
|
|
720
783
|
// Clean up the transaction after retry is complete
|
|
721
|
-
|
|
784
|
+
this.setTimer(() => {
|
|
722
785
|
this.transactions.delete(messageHash);
|
|
723
786
|
this.deleteCoordinatorState(messageHash);
|
|
724
787
|
log('cluster-tx:transaction-remove', {
|
|
@@ -739,7 +802,7 @@ export class ClusterCoordinator {
|
|
|
739
802
|
this.stateStore.saveCoordinatorState(messageHash, {
|
|
740
803
|
messageHash,
|
|
741
804
|
record,
|
|
742
|
-
lastUpdate:
|
|
805
|
+
lastUpdate: this.now(),
|
|
743
806
|
phase,
|
|
744
807
|
retryState
|
|
745
808
|
}).catch(err => log('cluster-tx:persist-error', { messageHash, error: (err as Error).message }));
|
|
@@ -762,7 +825,7 @@ export class ClusterCoordinator {
|
|
|
762
825
|
for (const state of states) {
|
|
763
826
|
const { messageHash } = state;
|
|
764
827
|
// Expired — clean up
|
|
765
|
-
if (state.record.message.expiration && state.record.message.expiration <
|
|
828
|
+
if (state.record.message.expiration && state.record.message.expiration < this.now()) {
|
|
766
829
|
log('cluster-tx:recovery-expired', { messageHash });
|
|
767
830
|
await this.stateStore.deleteCoordinatorState(messageHash);
|
|
768
831
|
continue;
|