@optimystic/db-p2p 0.24.2 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{readme.md → README.md} +688 -668
- package/dist/src/cluster/block-transfer-service.d.ts +133 -10
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +155 -9
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +67 -7
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +127 -24
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/certified-claims.d.ts +170 -0
- package/dist/src/cluster/certified-claims.d.ts.map +1 -0
- package/dist/src/cluster/certified-claims.js +200 -0
- package/dist/src/cluster/certified-claims.js.map +1 -0
- package/dist/src/cluster/client-signature-verifier.d.ts +23 -0
- package/dist/src/cluster/client-signature-verifier.d.ts.map +1 -0
- package/dist/src/cluster/client-signature-verifier.js +33 -0
- package/dist/src/cluster/client-signature-verifier.js.map +1 -0
- package/dist/src/cluster/cluster-policy.d.ts +9 -3
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js +111 -27
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +140 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +485 -29
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.d.ts +134 -0
- package/dist/src/cluster/commit-proof.d.ts.map +1 -0
- package/dist/src/cluster/commit-proof.js +263 -0
- package/dist/src/cluster/commit-proof.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +95 -8
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +113 -14
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +84 -0
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +207 -5
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts +27 -7
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +179 -38
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/service.d.ts +13 -4
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +17 -18
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +8 -2
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +7 -7
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -7
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +7 -6
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +14 -41
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/cascade.d.ts +7 -8
- package/dist/src/dispute/cascade.d.ts.map +1 -1
- package/dist/src/dispute/cascade.js +24 -7
- package/dist/src/dispute/cascade.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +5 -0
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +13 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +33 -15
- package/dist/src/dispute/invalidation.d.ts.map +1 -1
- package/dist/src/dispute/invalidation.js +127 -34
- 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 +2 -1
- package/dist/src/dispute/service.js.map +1 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +14 -10
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +28 -58
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +38 -7
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +148 -63
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/network/open-protocol-stream.d.ts +56 -0
- package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
- package/dist/src/network/open-protocol-stream.js +60 -0
- package/dist/src/network/open-protocol-stream.js.map +1 -0
- package/dist/src/network/register-protocol-handler.d.ts +66 -0
- package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
- package/dist/src/network/register-protocol-handler.js +51 -0
- package/dist/src/network/register-protocol-handler.js.map +1 -0
- package/dist/src/peer-address-book.d.ts +60 -2
- package/dist/src/peer-address-book.d.ts.map +1 -1
- package/dist/src/peer-address-book.js +89 -1
- package/dist/src/peer-address-book.js.map +1 -1
- package/dist/src/pend-validation.d.ts +65 -0
- package/dist/src/pend-validation.d.ts.map +1 -0
- package/dist/src/pend-validation.js +69 -0
- package/dist/src/pend-validation.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
- package/dist/src/reactivity/notify-transport.js +2 -1
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -1
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +32 -6
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +44 -10
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +181 -25
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +650 -62
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/served-repo-proxy.d.ts +30 -0
- package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
- package/dist/src/repo/served-repo-proxy.js +50 -0
- package/dist/src/repo/served-repo-proxy.js.map +1 -0
- package/dist/src/repo/service.d.ts +16 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +18 -16
- package/dist/src/repo/service.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 -1
- package/dist/src/reputation/types.js.map +1 -1
- package/dist/src/rn.d.ts +24 -2
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +33 -4
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-archive.d.ts +153 -0
- package/dist/src/storage/block-archive.d.ts.map +1 -0
- package/dist/src/storage/block-archive.js +194 -0
- package/dist/src/storage/block-archive.js.map +1 -0
- package/dist/src/storage/block-latch.d.ts +92 -0
- package/dist/src/storage/block-latch.d.ts.map +1 -0
- package/dist/src/storage/block-latch.js +132 -0
- package/dist/src/storage/block-latch.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +169 -12
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +415 -152
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +5 -1
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/cached-raw-storage.js +16 -1
- package/dist/src/storage/cached-raw-storage.js.map +1 -1
- package/dist/src/storage/cached-store-driver.d.ts +14 -4
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
- package/dist/src/storage/cached-store-driver.js +39 -5
- package/dist/src/storage/cached-store-driver.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +70 -19
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js +21 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +38 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +14 -8
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.js +29 -9
- package/dist/src/storage/kv-raw-storage.js.map +1 -1
- package/dist/src/storage/memory-store-driver.d.ts +4 -1
- package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
- package/dist/src/storage/memory-store-driver.js +22 -2
- package/dist/src/storage/memory-store-driver.js.map +1 -1
- package/dist/src/storage/raw-store-codec.js +2 -2
- package/dist/src/storage/raw-store-driver.d.ts +44 -3
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +60 -3
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.js +68 -4
- package/dist/src/storage/shared-cache-pool.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +204 -26
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +422 -119
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/store-identity.d.ts +21 -0
- package/dist/src/storage/store-identity.d.ts.map +1 -0
- package/dist/src/storage/store-identity.js +33 -0
- package/dist/src/storage/store-identity.js.map +1 -0
- package/dist/src/storage/struct.d.ts +15 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/storage/with-read-cache.d.ts +92 -0
- package/dist/src/storage/with-read-cache.d.ts.map +1 -0
- package/dist/src/storage/with-read-cache.js +132 -0
- package/dist/src/storage/with-read-cache.js.map +1 -0
- package/dist/src/sync/service.d.ts +13 -3
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +7 -30
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +75 -2
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +193 -81
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +97 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +5 -6
- package/src/cluster/block-transfer-service.ts +528 -312
- package/src/cluster/block-transfer.ts +547 -424
- package/src/cluster/certified-claims.ts +319 -0
- package/src/cluster/client-signature-verifier.ts +34 -0
- package/src/cluster/cluster-policy.ts +294 -203
- package/src/cluster/cluster-repo.ts +2501 -2034
- package/src/cluster/commit-proof.ts +344 -0
- package/src/cluster/quorum-restore.ts +348 -223
- package/src/cluster/rebalance-monitor.ts +539 -254
- package/src/cluster/reconcile-block.ts +404 -203
- package/src/cluster/service.ts +23 -20
- package/src/cluster/spread-on-churn.ts +352 -346
- package/src/cohort-topic/host.ts +8 -7
- package/src/cohort-topic/stream-util.ts +14 -43
- package/src/dispute/cascade.ts +31 -15
- package/src/dispute/dispute-service.ts +706 -696
- package/src/dispute/invalidation.ts +162 -52
- package/src/dispute/service.ts +126 -125
- package/src/index.ts +7 -0
- package/src/libp2p-key-network.ts +1204 -1235
- package/src/libp2p-node-base.ts +185 -70
- package/src/network/open-protocol-stream.ts +87 -0
- package/src/network/register-protocol-handler.ts +85 -0
- package/src/peer-address-book.ts +96 -2
- package/src/pend-validation.ts +89 -0
- package/src/reactivity/notify-transport.ts +2 -1
- package/src/reactivity/push-state-gossip.ts +2 -1
- package/src/repo/cluster-coordinator.ts +65 -11
- package/src/repo/coordinator-repo.ts +729 -71
- package/src/repo/served-repo-proxy.ts +55 -0
- package/src/repo/service.ts +25 -16
- package/src/reputation/types.ts +3 -1
- package/src/rn.ts +34 -4
- package/src/storage/block-archive.ts +242 -0
- package/src/storage/block-latch.ts +144 -0
- package/src/storage/block-storage.ts +787 -498
- package/src/storage/cached-raw-storage.ts +21 -1
- package/src/storage/cached-store-driver.ts +898 -859
- package/src/storage/i-block-storage.ts +172 -116
- package/src/storage/i-raw-storage.ts +40 -0
- package/src/storage/kv-raw-storage.ts +34 -9
- package/src/storage/memory-store-driver.ts +26 -2
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +45 -3
- package/src/storage/shared-cache-pool.ts +516 -452
- package/src/storage/storage-repo.ts +1421 -1014
- package/src/storage/store-identity.ts +41 -0
- package/src/storage/struct.ts +52 -34
- package/src/storage/with-read-cache.ts +182 -0
- package/src/sync/service.ts +178 -198
- package/src/testing/mesh-harness.ts +524 -344
- package/src/testing/raw-storage-conformance.ts +114 -2
|
@@ -1,254 +1,539 @@
|
|
|
1
|
-
import type { Startable, Libp2p } from '@libp2p/interface'
|
|
2
|
-
import { hashKey } from 'p2p-fret'
|
|
3
|
-
import type { FretService } from 'p2p-fret'
|
|
4
|
-
import type { PartitionDetector } from './partition-detector.js'
|
|
5
|
-
import type { ArachnodeFretAdapter, ArachnodeInfo } from '../storage/arachnode-fret-adapter.js'
|
|
6
|
-
import { createLogger } from '../logger.js'
|
|
7
|
-
|
|
8
|
-
const log = createLogger('rebalance-monitor')
|
|
9
|
-
const textEncoder = new TextEncoder()
|
|
10
|
-
|
|
11
|
-
export interface RebalanceEvent {
|
|
12
|
-
/** Block IDs this node has gained responsibility for */
|
|
13
|
-
gained: string[]
|
|
14
|
-
/** Block IDs this node has lost responsibility for */
|
|
15
|
-
lost: string[]
|
|
16
|
-
/** Peers that are now closer for the lost blocks: blockId → peerId[] */
|
|
17
|
-
newOwners: Map<string, string[]>
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
private
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
this.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this.
|
|
253
|
-
}
|
|
254
|
-
|
|
1
|
+
import type { Startable, Libp2p } from '@libp2p/interface'
|
|
2
|
+
import { hashKey } from 'p2p-fret'
|
|
3
|
+
import type { FretService } from 'p2p-fret'
|
|
4
|
+
import type { PartitionDetector } from './partition-detector.js'
|
|
5
|
+
import type { ArachnodeFretAdapter, ArachnodeInfo } from '../storage/arachnode-fret-adapter.js'
|
|
6
|
+
import { createLogger } from '../logger.js'
|
|
7
|
+
|
|
8
|
+
const log = createLogger('rebalance-monitor')
|
|
9
|
+
const textEncoder = new TextEncoder()
|
|
10
|
+
|
|
11
|
+
export interface RebalanceEvent {
|
|
12
|
+
/** Block IDs this node has gained responsibility for */
|
|
13
|
+
gained: string[]
|
|
14
|
+
/** Block IDs this node has lost responsibility for */
|
|
15
|
+
lost: string[]
|
|
16
|
+
/** Peers that are now closer for the lost blocks: blockId → peerId[] */
|
|
17
|
+
newOwners: Map<string, string[]>
|
|
18
|
+
/**
|
|
19
|
+
* Blocks this node KEEPS responsibility for whose cohort now contains peers it has not seen
|
|
20
|
+
* co-responsible before: blockId → the newly co-responsible peer ids (never self). This is the
|
|
21
|
+
* cohort-GROWTH arm: the founder case (a block committed while the deployment was one node) never
|
|
22
|
+
* appears in `lost` — the holder keeps the block — so without this arm nothing ever pushes the
|
|
23
|
+
* second copy and the block stays readable only by its sole holder. The reaction pushes each
|
|
24
|
+
* block to these peers (capped by the replication floor). A block can appear in both `gained`
|
|
25
|
+
* and `grown` (first observation after a restart/regain — the push then finds no local data and
|
|
26
|
+
* is a benign no-op); it can never appear in both `lost` and `grown` (lost ⇒ not responsible).
|
|
27
|
+
*/
|
|
28
|
+
grown: Map<string, string[]>
|
|
29
|
+
/**
|
|
30
|
+
* Replication floor `N` for this event — the cohort size FRET assembled at check time
|
|
31
|
+
* ({@link RebalanceMonitor.getCohortSize}). The reaction gates release of a `lost` block on
|
|
32
|
+
* confirming it replicated to this many new owners, so a lost block is never released below the
|
|
33
|
+
* floor. See `docs/arachnode-ring-handoff.md` § Part 2.
|
|
34
|
+
*/
|
|
35
|
+
floor: number
|
|
36
|
+
/** Timestamp of the topology change that triggered this */
|
|
37
|
+
triggeredAt: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What the growth reaction learned about ONE block reported `grown`. Fed back to
|
|
42
|
+
* {@link RebalanceMonitor.recordGrowthOutcome} so the seen set is confirmation-driven: a peer
|
|
43
|
+
* enters a block's seen set only once a replica is confirmed on it, or once the block has
|
|
44
|
+
* otherwise reached its floor. A block the reaction had NO information about (its confirm was
|
|
45
|
+
* deduped against one already in flight) gets no outcome at all — the monitor's state stays
|
|
46
|
+
* untouched and the next check re-detects.
|
|
47
|
+
*/
|
|
48
|
+
export interface GrowthOutcome {
|
|
49
|
+
/** Newly co-responsible peers that may now be recorded as seen for this block. */
|
|
50
|
+
satisfiedPeers: string[]
|
|
51
|
+
/** True when nothing about this block is still owed a push. */
|
|
52
|
+
complete: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RebalanceMonitorConfig {
|
|
56
|
+
/** Debounce window for topology changes (ms). Default: 5000 */
|
|
57
|
+
debounceMs?: number
|
|
58
|
+
/** Maximum frequency of full rebalance scans (ms). Default: 60000 */
|
|
59
|
+
minRebalanceIntervalMs?: number
|
|
60
|
+
/** Whether to suppress rebalancing during detected partitions. Default: true */
|
|
61
|
+
suppressDuringPartition?: boolean
|
|
62
|
+
/**
|
|
63
|
+
* Maximum blocks reported `grown` per check. Bounds the work a single peer join can trigger on a
|
|
64
|
+
* node with a large owned-block set — the primary bound is already the floor (a cohort is at most
|
|
65
|
+
* floor-sized, so each grown block pushes to ≤ floor−1 peers, and the reaction stops per block
|
|
66
|
+
* once the floor is met); this cap bounds the block COUNT per pass. A block dropped by the cap is
|
|
67
|
+
* NOT recorded as seen, so the next check re-detects the same growth — deferred, never lost.
|
|
68
|
+
* Default: 64
|
|
69
|
+
*/
|
|
70
|
+
growthBlockBudget?: number
|
|
71
|
+
/**
|
|
72
|
+
* How many incomplete growth outcomes ({@link GrowthOutcome} with `complete: false`) a block
|
|
73
|
+
* absorbs before its still-unsatisfied peers are moved to a per-block abandoned set and no longer
|
|
74
|
+
* pushed to. Without this bound a peer that permanently refuses would be re-pushed on every check
|
|
75
|
+
* forever, and its block would re-consume `growthBlockBudget` slots and starve genuinely-new
|
|
76
|
+
* growth. An abandoned peer that leaves the cohort and later rejoins is retried from scratch.
|
|
77
|
+
* Default: 5
|
|
78
|
+
*/
|
|
79
|
+
growthMaxAttempts?: number
|
|
80
|
+
/**
|
|
81
|
+
* Self-arming re-check timer for outstanding growth work (reported-but-unconfirmed peers, or
|
|
82
|
+
* blocks deferred by `growthBlockBudget`). Checks otherwise fire only on libp2p connection
|
|
83
|
+
* events, so a failed push on a then-quiet network would never be retried. Armed at the end of a
|
|
84
|
+
* check only while work is outstanding; fires `maybeRebalance()` so the existing
|
|
85
|
+
* `minRebalanceIntervalMs` throttle still bounds the push rate. `0` disables.
|
|
86
|
+
* Default: `minRebalanceIntervalMs`
|
|
87
|
+
*/
|
|
88
|
+
growthRecheckIntervalMs?: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface RebalanceMonitorDeps {
|
|
92
|
+
libp2p: Libp2p
|
|
93
|
+
fret: FretService
|
|
94
|
+
partitionDetector: PartitionDetector
|
|
95
|
+
fretAdapter: ArachnodeFretAdapter
|
|
96
|
+
/**
|
|
97
|
+
* The owned-block tracked set. When provided (e.g. the shared `ownedBlocks` set wired in
|
|
98
|
+
* `libp2p-node-base`), the monitor references this exact `Set` instead of constructing its own,
|
|
99
|
+
* so it stays in lock-step with the `SpreadOnChurnMonitor` that shares it. Omit for standalone
|
|
100
|
+
* construction (unit tests) — a fresh private `Set` preserves all existing behavior. Note: only
|
|
101
|
+
* `trackedBlocks` is shared; `responsibilitySnapshot` stays per-monitor (it is rebalance's own
|
|
102
|
+
* was-responsible memory, not owned-block tracking).
|
|
103
|
+
*/
|
|
104
|
+
trackedBlocks?: Set<string>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type RebalanceHandler = (event: RebalanceEvent) => void
|
|
108
|
+
|
|
109
|
+
/** Per-block rebalance/growth state (the `responsibilitySnapshot` entry). */
|
|
110
|
+
interface BlockGrowthState {
|
|
111
|
+
responsible: boolean
|
|
112
|
+
/**
|
|
113
|
+
* The growth arm's seen set: peers CONFIRMED to hold a replica of this block (or satisfied
|
|
114
|
+
* another way — floor met, or nothing local to push). Peers enter ONLY via
|
|
115
|
+
* {@link RebalanceMonitor.recordGrowthOutcome}, never at report time.
|
|
116
|
+
*/
|
|
117
|
+
cohortPeers: Set<string>
|
|
118
|
+
/** Peers reported grown at the last emitting check whose confirmation is still outstanding. */
|
|
119
|
+
pendingPeers: Set<string>
|
|
120
|
+
/** Consecutive incomplete growth outcomes — the give-up counter against `growthMaxAttempts`. */
|
|
121
|
+
growthAttempts: number
|
|
122
|
+
/**
|
|
123
|
+
* Peers given up on after `growthMaxAttempts` incomplete outcomes — excluded from growth reports
|
|
124
|
+
* until they leave the cohort and rejoin (each check intersects this with the current cohort).
|
|
125
|
+
*/
|
|
126
|
+
abandonedPeers: Set<string>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const emptyGrowthState = (responsible: boolean): BlockGrowthState => ({
|
|
130
|
+
responsible,
|
|
131
|
+
cohortPeers: new Set<string>(),
|
|
132
|
+
pendingPeers: new Set<string>(),
|
|
133
|
+
growthAttempts: 0,
|
|
134
|
+
abandonedPeers: new Set<string>()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const intersect = (remembered: Set<string> | undefined, current: Set<string>): Set<string> =>
|
|
138
|
+
new Set([...(remembered ?? [])].filter(id => current.has(id)))
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Carry a still-responsible block's growth state into the next check, intersecting both remembered
|
|
142
|
+
* peer sets against the CURRENT cohort: a peer that left drops out of `cohortPeers` (so its return
|
|
143
|
+
* is re-detected — the departure self-heal) and out of `abandonedPeers` (so a rejoin is retried from
|
|
144
|
+
* scratch). `pendingPeers` is always rebuilt from this check's own report, never carried.
|
|
145
|
+
*/
|
|
146
|
+
const carryGrowthState = (prior: BlockGrowthState | undefined, currentPeers: Set<string>): BlockGrowthState => ({
|
|
147
|
+
responsible: true,
|
|
148
|
+
cohortPeers: intersect(prior?.cohortPeers, currentPeers),
|
|
149
|
+
pendingPeers: new Set<string>(),
|
|
150
|
+
growthAttempts: prior?.growthAttempts ?? 0,
|
|
151
|
+
abandonedPeers: intersect(prior?.abandonedPeers, currentPeers)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
export class RebalanceMonitor implements Startable {
|
|
155
|
+
private running = false
|
|
156
|
+
private readonly trackedBlocks: Set<string>
|
|
157
|
+
// Per-monitor was-responsible memory (NOT shared, unlike trackedBlocks). When the shared
|
|
158
|
+
// trackedBlocks set is mutated externally — spread's no-local-data self-prune, or the node's
|
|
159
|
+
// responsibility-loss eviction going through untrackBlock — a snapshot entry for a since-removed
|
|
160
|
+
// block may linger here. That is acceptable: performRebalanceCheck only iterates trackedBlocks, so
|
|
161
|
+
// a lingering entry is inert; if the block is later re-fed, its responsibility is simply re-derived.
|
|
162
|
+
//
|
|
163
|
+
// `cohortPeers` is the growth arm's CONFIRMED-co-responsible memory. A MISSING entry is
|
|
164
|
+
// deliberately treated as "prior cohort = empty", so the first check after a topology event
|
|
165
|
+
// reports the whole non-self cohort as grown — that is what heals the founder case (A alone
|
|
166
|
+
// commits; B joins → first check pushes to B) and the restarted-holder case (snapshot memory is
|
|
167
|
+
// process-local, so a restarted holder re-pushes to everyone once). Peers enter the set only
|
|
168
|
+
// through recordGrowthOutcome — a reported-but-unconfirmed peer stays out, so a failed push is
|
|
169
|
+
// re-detected on the next check instead of being recorded as done.
|
|
170
|
+
private readonly responsibilitySnapshot = new Map<string, BlockGrowthState>()
|
|
171
|
+
private readonly handlers: RebalanceHandler[] = []
|
|
172
|
+
private debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
173
|
+
private recheckTimer: ReturnType<typeof setTimeout> | null = null
|
|
174
|
+
private lastRebalanceAt = 0
|
|
175
|
+
private pendingTopologyChange = false
|
|
176
|
+
private topologyChangeTimestamp = 0
|
|
177
|
+
private lastGrowthDeferred = 0
|
|
178
|
+
|
|
179
|
+
private readonly debounceMs: number
|
|
180
|
+
private readonly minRebalanceIntervalMs: number
|
|
181
|
+
private readonly suppressDuringPartition: boolean
|
|
182
|
+
private readonly growthBlockBudget: number
|
|
183
|
+
private readonly growthMaxAttempts: number
|
|
184
|
+
private readonly growthRecheckIntervalMs: number
|
|
185
|
+
|
|
186
|
+
private readonly onConnectionOpen: () => void
|
|
187
|
+
private readonly onConnectionClose: () => void
|
|
188
|
+
|
|
189
|
+
constructor(
|
|
190
|
+
private readonly deps: RebalanceMonitorDeps,
|
|
191
|
+
config: RebalanceMonitorConfig = {}
|
|
192
|
+
) {
|
|
193
|
+
// Share the injected owned-block set when present (so spread + rebalance never drift);
|
|
194
|
+
// otherwise own a private set (standalone construction / unit tests). Only trackedBlocks is
|
|
195
|
+
// shared — responsibilitySnapshot stays per-monitor.
|
|
196
|
+
this.trackedBlocks = deps.trackedBlocks ?? new Set<string>()
|
|
197
|
+
this.debounceMs = config.debounceMs ?? 5000
|
|
198
|
+
this.minRebalanceIntervalMs = config.minRebalanceIntervalMs ?? 60000
|
|
199
|
+
this.suppressDuringPartition = config.suppressDuringPartition ?? true
|
|
200
|
+
this.growthBlockBudget = config.growthBlockBudget ?? 64
|
|
201
|
+
this.growthMaxAttempts = config.growthMaxAttempts ?? 5
|
|
202
|
+
this.growthRecheckIntervalMs = config.growthRecheckIntervalMs ?? this.minRebalanceIntervalMs
|
|
203
|
+
|
|
204
|
+
this.onConnectionOpen = () => this.handleTopologyChange()
|
|
205
|
+
this.onConnectionClose = () => this.handleTopologyChange()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async start(): Promise<void> {
|
|
209
|
+
if (this.running) return
|
|
210
|
+
this.running = true
|
|
211
|
+
|
|
212
|
+
this.deps.libp2p.addEventListener('connection:open', this.onConnectionOpen)
|
|
213
|
+
this.deps.libp2p.addEventListener('connection:close', this.onConnectionClose)
|
|
214
|
+
|
|
215
|
+
log('started, tracking %d blocks', this.trackedBlocks.size)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async stop(): Promise<void> {
|
|
219
|
+
if (!this.running) return
|
|
220
|
+
this.running = false
|
|
221
|
+
|
|
222
|
+
this.deps.libp2p.removeEventListener('connection:open', this.onConnectionOpen)
|
|
223
|
+
this.deps.libp2p.removeEventListener('connection:close', this.onConnectionClose)
|
|
224
|
+
|
|
225
|
+
if (this.debounceTimer) {
|
|
226
|
+
clearTimeout(this.debounceTimer)
|
|
227
|
+
this.debounceTimer = null
|
|
228
|
+
}
|
|
229
|
+
if (this.recheckTimer) {
|
|
230
|
+
clearTimeout(this.recheckTimer)
|
|
231
|
+
this.recheckTimer = null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.pendingTopologyChange = false
|
|
235
|
+
log('stopped')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
onRebalance(handler: RebalanceHandler): void {
|
|
239
|
+
this.handlers.push(handler)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
trackBlock(blockId: string): void {
|
|
243
|
+
this.trackedBlocks.add(blockId)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
untrackBlock(blockId: string): void {
|
|
247
|
+
this.trackedBlocks.delete(blockId)
|
|
248
|
+
this.responsibilitySnapshot.delete(blockId)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getTrackedBlockCount(): number {
|
|
252
|
+
return this.trackedBlocks.size
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async checkNow(): Promise<RebalanceEvent | null> {
|
|
256
|
+
return this.performRebalanceCheck(Date.now())
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Feedback from the growth reaction for one block reported `grown`. `satisfiedPeers` enter the
|
|
261
|
+
* block's seen set; an incomplete outcome counts an attempt against `growthMaxAttempts`, and on
|
|
262
|
+
* reaching the bound the block's still-unsatisfied reported peers are abandoned (no longer
|
|
263
|
+
* pushed to until they leave the cohort and rejoin). Never called for a block the reaction had
|
|
264
|
+
* no information about (a confirm deduped against one already in flight) — a missing outcome
|
|
265
|
+
* leaves the block's state untouched so the next check retries.
|
|
266
|
+
*/
|
|
267
|
+
recordGrowthOutcome(blockId: string, outcome: GrowthOutcome): void {
|
|
268
|
+
const state = this.responsibilitySnapshot.get(blockId)
|
|
269
|
+
// No state (untracked since) or responsibility lost since the report: the growth state was
|
|
270
|
+
// cleared, and recording into it would survive the clear and suppress the regain re-push.
|
|
271
|
+
if (!state || !state.responsible) return
|
|
272
|
+
|
|
273
|
+
for (const peerId of outcome.satisfiedPeers) {
|
|
274
|
+
state.cohortPeers.add(peerId)
|
|
275
|
+
state.pendingPeers.delete(peerId)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (outcome.complete) {
|
|
279
|
+
state.growthAttempts = 0
|
|
280
|
+
state.pendingPeers.clear()
|
|
281
|
+
} else {
|
|
282
|
+
// NOTE: growthMaxAttempts is a floor on the retry count, not an exact one. `pendingPeers` is
|
|
283
|
+
// rebuilt from each check's own report, so two cases blunt the bound: a check that defers
|
|
284
|
+
// this block on growthBlockBudget clears pendingPeers, and a give-up landing right then
|
|
285
|
+
// abandons nobody while still resetting the counter; and two checks racing (the second
|
|
286
|
+
// re-reporting the same peer after the first's confirm left `inFlight` but before its outcome
|
|
287
|
+
// landed) double-count one attempt. Both are rare, both only change how many pushes a doomed
|
|
288
|
+
// peer absorbs. If a deployment ever tracks far more blocks than growthBlockBudget, deferral
|
|
289
|
+
// stops being rare — carry the report's peer list on GrowthOutcome and match it against
|
|
290
|
+
// pendingPeers instead of trusting the latest report.
|
|
291
|
+
state.growthAttempts++
|
|
292
|
+
if (state.growthAttempts >= this.growthMaxAttempts) {
|
|
293
|
+
for (const peerId of state.pendingPeers) {
|
|
294
|
+
state.abandonedPeers.add(peerId)
|
|
295
|
+
}
|
|
296
|
+
log('growth give-up: block=%s abandoning %d unsatisfied peer(s) after %d attempts',
|
|
297
|
+
blockId, state.pendingPeers.size, state.growthAttempts)
|
|
298
|
+
state.pendingPeers.clear()
|
|
299
|
+
state.growthAttempts = 0
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.updateRecheckTimer()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Growth-arm observability: how many tracked blocks still await confirmation on reported peers,
|
|
308
|
+
* how many (block, peer) pairs have been given up on, and whether the re-check timer is armed.
|
|
309
|
+
*/
|
|
310
|
+
getGrowthDiagnostics(): { blocksAwaitingConfirmation: number; abandonedPairs: number; recheckArmed: boolean } {
|
|
311
|
+
let blocksAwaitingConfirmation = 0
|
|
312
|
+
let abandonedPairs = 0
|
|
313
|
+
for (const [blockId, state] of this.responsibilitySnapshot) {
|
|
314
|
+
if (!state.responsible || !this.trackedBlocks.has(blockId)) continue
|
|
315
|
+
if (state.pendingPeers.size > 0) blocksAwaitingConfirmation++
|
|
316
|
+
abandonedPairs += state.abandonedPeers.size
|
|
317
|
+
}
|
|
318
|
+
return { blocksAwaitingConfirmation, abandonedPairs, recheckArmed: this.recheckTimer !== null }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Growth work is outstanding while any reported peer is unconfirmed or blocks were budget-deferred. */
|
|
322
|
+
private hasOutstandingGrowthWork(): boolean {
|
|
323
|
+
if (this.lastGrowthDeferred > 0) return true
|
|
324
|
+
for (const [blockId, state] of this.responsibilitySnapshot) {
|
|
325
|
+
if (state.responsible && state.pendingPeers.size > 0 && this.trackedBlocks.has(blockId)) {
|
|
326
|
+
return true
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return false
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Arm the growth re-check timer while work is outstanding; disarm it when there is none. The
|
|
334
|
+
* timer fires maybeRebalance(), so minRebalanceIntervalMs still bounds the push rate, and it
|
|
335
|
+
* re-arms itself after firing for as long as work remains. unref'd so it never holds the
|
|
336
|
+
* process open; stop() clears it.
|
|
337
|
+
*/
|
|
338
|
+
private updateRecheckTimer(): void {
|
|
339
|
+
if (this.growthRecheckIntervalMs <= 0) return
|
|
340
|
+
|
|
341
|
+
if (!this.running || !this.hasOutstandingGrowthWork()) {
|
|
342
|
+
if (this.recheckTimer) {
|
|
343
|
+
clearTimeout(this.recheckTimer)
|
|
344
|
+
this.recheckTimer = null
|
|
345
|
+
}
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (this.recheckTimer) return // already armed
|
|
350
|
+
|
|
351
|
+
this.recheckTimer = setTimeout(() => {
|
|
352
|
+
this.recheckTimer = null
|
|
353
|
+
void this.maybeRebalance()
|
|
354
|
+
.catch(err => { log('recheck error: %O', err) })
|
|
355
|
+
.finally(() => this.updateRecheckTimer())
|
|
356
|
+
}, this.growthRecheckIntervalMs)
|
|
357
|
+
;(this.recheckTimer as unknown as { unref?: () => void }).unref?.()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private handleTopologyChange(): void {
|
|
361
|
+
if (!this.running) return
|
|
362
|
+
|
|
363
|
+
if (!this.pendingTopologyChange) {
|
|
364
|
+
this.topologyChangeTimestamp = Date.now()
|
|
365
|
+
}
|
|
366
|
+
this.pendingTopologyChange = true
|
|
367
|
+
|
|
368
|
+
if (this.debounceTimer) {
|
|
369
|
+
clearTimeout(this.debounceTimer)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this.debounceTimer = setTimeout(() => {
|
|
373
|
+
this.debounceTimer = null
|
|
374
|
+
this.pendingTopologyChange = false
|
|
375
|
+
this.maybeRebalance()
|
|
376
|
+
}, this.debounceMs)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private async maybeRebalance(): Promise<void> {
|
|
380
|
+
if (!this.running) return
|
|
381
|
+
|
|
382
|
+
const now = Date.now()
|
|
383
|
+
const elapsed = now - this.lastRebalanceAt
|
|
384
|
+
if (elapsed < this.minRebalanceIntervalMs) {
|
|
385
|
+
log('throttled, %dms since last rebalance', elapsed)
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const event = await this.performRebalanceCheck(this.topologyChangeTimestamp || now)
|
|
390
|
+
if (event) {
|
|
391
|
+
this.emitEvent(event)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private async performRebalanceCheck(triggeredAt: number): Promise<RebalanceEvent | null> {
|
|
396
|
+
if (this.suppressDuringPartition && this.deps.partitionDetector.detectPartition()) {
|
|
397
|
+
log('partition detected, suppressing rebalance')
|
|
398
|
+
return null
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (this.trackedBlocks.size === 0) {
|
|
402
|
+
this.lastRebalanceAt = Date.now()
|
|
403
|
+
// Nothing left to grow, so any prior deferral is moot — drop it and let the re-check timer
|
|
404
|
+
// disarm, rather than re-arming forever against blocks that were untracked out from under it.
|
|
405
|
+
this.lastGrowthDeferred = 0
|
|
406
|
+
this.updateRecheckTimer()
|
|
407
|
+
return null
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const selfId = this.deps.libp2p.peerId.toString()
|
|
411
|
+
const gained: string[] = []
|
|
412
|
+
const lost: string[] = []
|
|
413
|
+
const newOwners = new Map<string, string[]>()
|
|
414
|
+
const grown = new Map<string, string[]>()
|
|
415
|
+
let growthDeferred = 0
|
|
416
|
+
const growthCandidates: Array<{ blockId: string; newPeers: string[]; state: BlockGrowthState }> = []
|
|
417
|
+
|
|
418
|
+
for (const blockId of this.trackedBlocks) {
|
|
419
|
+
const key = textEncoder.encode(blockId)
|
|
420
|
+
const coord = await hashKey(key)
|
|
421
|
+
|
|
422
|
+
// Get the current cohort — assembleCohort returns peer IDs sorted by distance
|
|
423
|
+
const cohort = this.deps.fret.assembleCohort(coord, this.getCohortSize())
|
|
424
|
+
const isResponsible = cohort.includes(selfId)
|
|
425
|
+
const prior = this.responsibilitySnapshot.get(blockId)
|
|
426
|
+
const wasResponsible = prior?.responsible ?? false
|
|
427
|
+
|
|
428
|
+
if (isResponsible && !wasResponsible) {
|
|
429
|
+
gained.push(blockId)
|
|
430
|
+
} else if (!isResponsible && wasResponsible) {
|
|
431
|
+
lost.push(blockId)
|
|
432
|
+
// The cohort members are the new owners
|
|
433
|
+
newOwners.set(blockId, cohort.filter(id => id !== selfId))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Growth arm: while this node STAYS responsible, any cohort peer not yet CONFIRMED to hold
|
|
437
|
+
// the block (and not abandoned) gets it pushed (up to the floor). Runs on every responsible
|
|
438
|
+
// check — NOT gated on wasResponsible — so a first observation (no snapshot entry) treats
|
|
439
|
+
// the whole non-self cohort as new; see the responsibilitySnapshot comment for why that is
|
|
440
|
+
// load-bearing. Not responsible ⇒ arm skipped, so `lost` ∩ `grown` is impossible.
|
|
441
|
+
//
|
|
442
|
+
// Reporting a peer does NOT record it as seen — only recordGrowthOutcome does — so a push
|
|
443
|
+
// that fails (dial timeout, receiver refused to persist, partition mid-reaction, reaction
|
|
444
|
+
// threw) leaves the peer un-seen and the next check re-detects it.
|
|
445
|
+
let state: BlockGrowthState
|
|
446
|
+
if (isResponsible) {
|
|
447
|
+
const currentSet = new Set(cohort.filter(id => id !== selfId))
|
|
448
|
+
state = carryGrowthState(prior, currentSet)
|
|
449
|
+
const newPeers = [...currentSet].filter(id => !state.cohortPeers.has(id) && !state.abandonedPeers.has(id))
|
|
450
|
+
if (newPeers.length > 0) {
|
|
451
|
+
growthCandidates.push({ blockId, newPeers, state })
|
|
452
|
+
} else {
|
|
453
|
+
// Nothing owed for this block. growthAttempts counts CONSECUTIVE incomplete outcomes
|
|
454
|
+
// against outstanding growth, so it must not carry across a quiet stretch — otherwise a
|
|
455
|
+
// block that failed a few times, then had that peer leave, would spend the leftovers on
|
|
456
|
+
// whichever peer joins next and abandon it early.
|
|
457
|
+
state.growthAttempts = 0
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
// Not responsible: clear ALL growth state (seen set, attempts, abandoned peers), so a
|
|
461
|
+
// later regain re-pushes to the whole cohort (benign when the local data is gone — the
|
|
462
|
+
// push finds nothing and no-ops).
|
|
463
|
+
state = emptyGrowthState(false)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.responsibilitySnapshot.set(blockId, state)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Fill the growth budget in two passes: fresh growth (no failed attempts yet) first, retrying
|
|
470
|
+
// blocks with what remains — otherwise a stuck retry set at the front of the tracked-block
|
|
471
|
+
// insertion order would starve peers that just joined.
|
|
472
|
+
for (const candidate of [
|
|
473
|
+
...growthCandidates.filter(c => c.state.growthAttempts === 0),
|
|
474
|
+
...growthCandidates.filter(c => c.state.growthAttempts > 0)
|
|
475
|
+
]) {
|
|
476
|
+
if (grown.size < this.growthBlockBudget) {
|
|
477
|
+
grown.set(candidate.blockId, candidate.newPeers)
|
|
478
|
+
candidate.state.pendingPeers = new Set(candidate.newPeers)
|
|
479
|
+
} else {
|
|
480
|
+
// Budget-dropped: the seen set was not touched, so the next check re-detects the same
|
|
481
|
+
// growth — a deferral, not a loss.
|
|
482
|
+
growthDeferred++
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
this.lastGrowthDeferred = growthDeferred
|
|
486
|
+
|
|
487
|
+
this.lastRebalanceAt = Date.now()
|
|
488
|
+
|
|
489
|
+
if (growthDeferred > 0) {
|
|
490
|
+
// Deferred blocks drain one budget-full per check. Checks fire on libp2p connection events
|
|
491
|
+
// AND — while growth work is outstanding — on the growthRecheckIntervalMs timer armed
|
|
492
|
+
// below, so a backlog drains even on a quiet network.
|
|
493
|
+
log('growth budget reached: %d blocks deferred to the next check (budget=%d)',
|
|
494
|
+
growthDeferred, this.growthBlockBudget)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
this.updateRecheckTimer()
|
|
498
|
+
|
|
499
|
+
if (gained.length === 0 && lost.length === 0 && grown.size === 0) {
|
|
500
|
+
return null
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
log('rebalance check: gained=%d lost=%d grown=%d', gained.length, lost.length, grown.size)
|
|
504
|
+
|
|
505
|
+
return { gained, lost, newOwners, grown, floor: this.getCohortSize(), triggeredAt }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* The replication floor `N` — the cohort size FRET assembles for a block. Public so the ring-shift
|
|
510
|
+
* handoff and the rebalance reaction can gate release on confirming replication to this many
|
|
511
|
+
* holders (`docs/arachnode-ring-handoff.md` § Replication floor). Derives from FRET's network-size
|
|
512
|
+
* estimate: `clamp(ceil(sqrt(n_est)), 1, 3)`, defaulting to 3 when no confident estimate exists.
|
|
513
|
+
*/
|
|
514
|
+
getCohortSize(): number {
|
|
515
|
+
const diag: any = (this.deps.fret as any).getDiagnostics?.()
|
|
516
|
+
const estimate = diag?.estimate ?? diag?.n
|
|
517
|
+
if (typeof estimate === 'number' && Number.isFinite(estimate) && estimate > 0) {
|
|
518
|
+
return Math.max(1, Math.min(3, Math.ceil(Math.sqrt(estimate))))
|
|
519
|
+
}
|
|
520
|
+
return 3
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private emitEvent(event: RebalanceEvent): void {
|
|
524
|
+
for (const handler of this.handlers) {
|
|
525
|
+
try {
|
|
526
|
+
handler(event)
|
|
527
|
+
} catch (err) {
|
|
528
|
+
log('handler error: %O', err)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Update ArachnodeInfo status through the fret adapter.
|
|
535
|
+
*/
|
|
536
|
+
setStatus(status: ArachnodeInfo['status']): void {
|
|
537
|
+
this.deps.fretAdapter.setStatus(status)
|
|
538
|
+
}
|
|
539
|
+
}
|