@optimystic/db-p2p 0.2.3 → 0.7.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/dist/src/cluster/block-transfer-service.d.ts +66 -0
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -0
- package/dist/src/cluster/block-transfer-service.js +166 -0
- package/dist/src/cluster/block-transfer-service.js.map +1 -0
- package/dist/src/cluster/block-transfer.d.ts +65 -0
- package/dist/src/cluster/block-transfer.d.ts.map +1 -0
- package/dist/src/cluster/block-transfer.js +208 -0
- package/dist/src/cluster/block-transfer.js.map +1 -0
- package/dist/src/cluster/cluster-repo.d.ts +23 -4
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +119 -39
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +64 -0
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -0
- package/dist/src/cluster/rebalance-monitor.js +157 -0
- package/dist/src/cluster/rebalance-monitor.js.map +1 -0
- package/dist/src/cluster/service.js +1 -1
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/dispute/arbitrator-selection.d.ts +10 -0
- package/dist/src/dispute/arbitrator-selection.d.ts.map +1 -0
- package/dist/src/dispute/arbitrator-selection.js +22 -0
- package/dist/src/dispute/arbitrator-selection.js.map +1 -0
- package/dist/src/dispute/client.d.ts +17 -0
- package/dist/src/dispute/client.d.ts.map +1 -0
- package/dist/src/dispute/client.js +28 -0
- package/dist/src/dispute/client.js.map +1 -0
- package/dist/src/dispute/dispute-service.d.ts +83 -0
- package/dist/src/dispute/dispute-service.d.ts.map +1 -0
- package/dist/src/dispute/dispute-service.js +368 -0
- package/dist/src/dispute/dispute-service.js.map +1 -0
- package/dist/src/dispute/engine-health-monitor.d.ts +22 -0
- package/dist/src/dispute/engine-health-monitor.d.ts.map +1 -0
- package/dist/src/dispute/engine-health-monitor.js +75 -0
- package/dist/src/dispute/engine-health-monitor.js.map +1 -0
- package/dist/src/dispute/index.d.ts +7 -0
- package/dist/src/dispute/index.d.ts.map +1 -0
- package/dist/src/dispute/index.js +7 -0
- package/dist/src/dispute/index.js.map +1 -0
- package/dist/src/dispute/service.d.ts +41 -0
- package/dist/src/dispute/service.d.ts.map +1 -0
- package/dist/src/dispute/service.js +82 -0
- package/dist/src/dispute/service.js.map +1 -0
- package/dist/src/dispute/types.d.ts +106 -0
- package/dist/src/dispute/types.d.ts.map +1 -0
- package/dist/src/dispute/types.js +7 -0
- package/dist/src/dispute/types.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +23 -2
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +100 -15
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +6 -0
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +67 -13
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +1 -0
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +2 -0
- package/dist/src/logger.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +15 -4
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +33 -20
- package/dist/src/network/network-manager-service.js.map +1 -1
- 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 +23 -2
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/repo/client.d.ts +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +18 -1
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +3 -1
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +42 -2
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +20 -4
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +67 -11
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts +18 -2
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +88 -91
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/index.d.ts +3 -0
- package/dist/src/reputation/index.d.ts.map +1 -0
- package/dist/src/reputation/index.js +3 -0
- package/dist/src/reputation/index.js.map +1 -0
- package/dist/src/reputation/peer-reputation.d.ts +23 -0
- package/dist/src/reputation/peer-reputation.d.ts.map +1 -0
- package/dist/src/reputation/peer-reputation.js +121 -0
- package/dist/src/reputation/peer-reputation.js.map +1 -0
- package/dist/src/reputation/types.d.ts +89 -0
- package/dist/src/reputation/types.d.ts.map +1 -0
- package/dist/src/reputation/types.js +42 -0
- package/dist/src/reputation/types.js.map +1 -0
- package/dist/src/storage/arachnode-fret-adapter.d.ts +5 -0
- package/dist/src/storage/arachnode-fret-adapter.d.ts.map +1 -1
- package/dist/src/storage/arachnode-fret-adapter.js +10 -0
- package/dist/src/storage/arachnode-fret-adapter.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +5 -0
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +8 -0
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/package.json +11 -10
- package/src/cluster/block-transfer-service.ts +231 -0
- package/src/cluster/block-transfer.ts +265 -0
- package/src/cluster/cluster-repo.ts +148 -42
- package/src/cluster/rebalance-monitor.ts +223 -0
- package/src/dispute/arbitrator-selection.ts +28 -0
- package/src/dispute/client.ts +41 -0
- package/src/dispute/dispute-service.ts +456 -0
- package/src/dispute/engine-health-monitor.ts +86 -0
- package/src/dispute/index.ts +17 -0
- package/src/dispute/service.ts +119 -0
- package/src/dispute/types.ts +114 -0
- package/src/index.ts +5 -0
- package/src/libp2p-key-network.ts +120 -22
- package/src/libp2p-node-base.ts +78 -14
- package/src/logger.ts +2 -1
- package/src/network/network-manager-service.ts +47 -16
- package/src/protocol-client.ts +29 -7
- package/src/repo/client.ts +20 -6
- package/src/repo/cluster-coordinator.ts +43 -2
- package/src/repo/coordinator-repo.ts +77 -14
- package/src/repo/redirect.ts +0 -2
- package/src/repo/service.ts +95 -87
- package/src/reputation/index.ts +12 -0
- package/src/reputation/peer-reputation.ts +147 -0
- package/src/reputation/types.ts +117 -0
- package/src/storage/arachnode-fret-adapter.ts +11 -0
- package/src/storage/block-storage.ts +6 -0
- package/src/storage/storage-repo.ts +9 -0
- package/dist/index.min.js +0 -53
- package/dist/index.min.js.map +0 -7
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { Startable, Stream } from '@libp2p/interface';
|
|
2
|
+
import type { IRepo, PeerId, IPeerNetwork } from '@optimystic/db-core';
|
|
3
|
+
import { pipe } from 'it-pipe';
|
|
4
|
+
import * as lp from 'it-length-prefixed';
|
|
5
|
+
import { fromString as u8FromString } from 'uint8arrays/from-string';
|
|
6
|
+
import { toString as u8ToString } from 'uint8arrays/to-string';
|
|
7
|
+
import { ProtocolClient } from '../protocol-client.js';
|
|
8
|
+
import { createLogger } from '../logger.js';
|
|
9
|
+
|
|
10
|
+
const log = createLogger('block-transfer-service');
|
|
11
|
+
|
|
12
|
+
/** Protocol path */
|
|
13
|
+
const BLOCK_TRANSFER_PREFIX = '/db-p2p/block-transfer/';
|
|
14
|
+
const BLOCK_TRANSFER_VERSION = '1.0.0';
|
|
15
|
+
|
|
16
|
+
export const buildBlockTransferProtocol = (protocolPrefix: string = ''): string =>
|
|
17
|
+
`${protocolPrefix}${BLOCK_TRANSFER_PREFIX}${BLOCK_TRANSFER_VERSION}`;
|
|
18
|
+
|
|
19
|
+
/** Request to transfer blocks */
|
|
20
|
+
export interface BlockTransferRequest {
|
|
21
|
+
type: 'pull' | 'push';
|
|
22
|
+
/** Block IDs being transferred */
|
|
23
|
+
blockIds: string[];
|
|
24
|
+
/** Reason for transfer */
|
|
25
|
+
reason: 'rebalance' | 'replication' | 'recovery';
|
|
26
|
+
/** For push: base64-encoded block data per block ID */
|
|
27
|
+
blockData?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Response with block data */
|
|
31
|
+
export interface BlockTransferResponse {
|
|
32
|
+
/** Blocks successfully transferred: blockId → base64-encoded data */
|
|
33
|
+
blocks: Record<string, string>;
|
|
34
|
+
/** Block IDs that couldn't be found/transferred */
|
|
35
|
+
missing: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Service (server-side handler) ---
|
|
39
|
+
|
|
40
|
+
export interface BlockTransferServiceInit {
|
|
41
|
+
protocolPrefix?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BlockTransferServiceComponents {
|
|
45
|
+
registrar: { handle: (...args: any[]) => Promise<void>; unhandle: (...args: any[]) => Promise<void> };
|
|
46
|
+
repo: IRepo;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Libp2p service that handles incoming block transfer requests.
|
|
51
|
+
*
|
|
52
|
+
* Responds to pull requests by reading blocks from local storage.
|
|
53
|
+
* Handles push requests by accepting block data and storing it locally.
|
|
54
|
+
*/
|
|
55
|
+
export class BlockTransferService implements Startable {
|
|
56
|
+
private running = false;
|
|
57
|
+
private readonly protocol: string;
|
|
58
|
+
private readonly repo: IRepo;
|
|
59
|
+
private readonly registrar: BlockTransferServiceComponents['registrar'];
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
private readonly components: BlockTransferServiceComponents,
|
|
63
|
+
init: BlockTransferServiceInit = {}
|
|
64
|
+
) {
|
|
65
|
+
this.protocol = buildBlockTransferProtocol(init.protocolPrefix ?? '');
|
|
66
|
+
this.repo = components.repo;
|
|
67
|
+
this.registrar = components.registrar;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async start(): Promise<void> {
|
|
71
|
+
if (this.running) return;
|
|
72
|
+
await this.registrar.handle(this.protocol, async (data: any) => {
|
|
73
|
+
await this.handleRequest(data.stream);
|
|
74
|
+
});
|
|
75
|
+
this.running = true;
|
|
76
|
+
log('started on %s', this.protocol);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async stop(): Promise<void> {
|
|
80
|
+
if (!this.running) return;
|
|
81
|
+
await this.registrar.unhandle(this.protocol);
|
|
82
|
+
this.running = false;
|
|
83
|
+
log('stopped');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async handleRequest(stream: Stream): Promise<void> {
|
|
87
|
+
try {
|
|
88
|
+
const request = await this.readRequest(stream);
|
|
89
|
+
log('request type=%s blocks=%d reason=%s', request.type, request.blockIds.length, request.reason);
|
|
90
|
+
|
|
91
|
+
let response: BlockTransferResponse;
|
|
92
|
+
if (request.type === 'pull') {
|
|
93
|
+
response = await this.handlePull(request);
|
|
94
|
+
} else {
|
|
95
|
+
response = await this.handlePush(request);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await this.sendResponse(stream, response);
|
|
99
|
+
log('response blocks=%d missing=%d', Object.keys(response.blocks).length, response.missing.length);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
log('error: %s', (error as Error).message);
|
|
102
|
+
try {
|
|
103
|
+
await this.sendResponse(stream, { blocks: {}, missing: [] });
|
|
104
|
+
} catch {
|
|
105
|
+
// ignore send errors
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
try { await stream.close(); } catch { /* ignore */ }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async handlePull(request: BlockTransferRequest): Promise<BlockTransferResponse> {
|
|
113
|
+
const blocks: Record<string, string> = {};
|
|
114
|
+
const missing: string[] = [];
|
|
115
|
+
|
|
116
|
+
const result = await this.repo.get({ blockIds: request.blockIds });
|
|
117
|
+
|
|
118
|
+
for (const blockId of request.blockIds) {
|
|
119
|
+
const blockResult = result[blockId];
|
|
120
|
+
if (blockResult?.block) {
|
|
121
|
+
blocks[blockId] = Buffer.from(JSON.stringify(blockResult.block)).toString('base64');
|
|
122
|
+
} else {
|
|
123
|
+
missing.push(blockId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { blocks, missing };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// TODO: handlePush validates incoming block data but does not persist it.
|
|
131
|
+
// The pushed data is a serialized IBlock, not a full BlockArchive with revisions.
|
|
132
|
+
// Persistence should be wired when RebalanceMonitor integrates with BlockStorage.saveRestored().
|
|
133
|
+
private async handlePush(request: BlockTransferRequest): Promise<BlockTransferResponse> {
|
|
134
|
+
const blocks: Record<string, string> = {};
|
|
135
|
+
const missing: string[] = [];
|
|
136
|
+
|
|
137
|
+
if (!request.blockData) {
|
|
138
|
+
return { blocks: {}, missing: request.blockIds };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Accept pushed blocks — for each block, validate we received parseable data
|
|
142
|
+
for (const blockId of request.blockIds) {
|
|
143
|
+
const data = request.blockData[blockId];
|
|
144
|
+
if (data) {
|
|
145
|
+
// Verify we received valid data
|
|
146
|
+
try {
|
|
147
|
+
JSON.parse(Buffer.from(data, 'base64').toString('utf8'));
|
|
148
|
+
blocks[blockId] = data;
|
|
149
|
+
} catch {
|
|
150
|
+
missing.push(blockId);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
missing.push(blockId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { blocks, missing };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async readRequest(stream: Stream): Promise<BlockTransferRequest> {
|
|
161
|
+
const messages: Uint8Array[] = [];
|
|
162
|
+
await pipe(
|
|
163
|
+
stream,
|
|
164
|
+
lp.decode,
|
|
165
|
+
async (source) => {
|
|
166
|
+
for await (const msg of source) {
|
|
167
|
+
messages.push(msg.subarray());
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (messages.length === 0) {
|
|
173
|
+
throw new Error('No request received');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return JSON.parse(u8ToString(messages[0]!, 'utf8')) as BlockTransferRequest;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async sendResponse(stream: Stream, response: BlockTransferResponse): Promise<void> {
|
|
180
|
+
const bytes = u8FromString(JSON.stringify(response), 'utf8');
|
|
181
|
+
const encoded = pipe([bytes], lp.encode);
|
|
182
|
+
for await (const chunk of encoded) {
|
|
183
|
+
stream.send(chunk);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Factory for creating BlockTransferService following the libp2p service pattern. */
|
|
189
|
+
export const blockTransferService = (init: BlockTransferServiceInit = {}) =>
|
|
190
|
+
(components: BlockTransferServiceComponents) => new BlockTransferService(components, init);
|
|
191
|
+
|
|
192
|
+
// --- Client ---
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Client for sending block transfer requests to remote peers.
|
|
196
|
+
*/
|
|
197
|
+
export class BlockTransferClient extends ProtocolClient {
|
|
198
|
+
private readonly protocol: string;
|
|
199
|
+
|
|
200
|
+
constructor(
|
|
201
|
+
peerId: PeerId,
|
|
202
|
+
peerNetwork: IPeerNetwork,
|
|
203
|
+
protocolPrefix: string = ''
|
|
204
|
+
) {
|
|
205
|
+
super(peerId, peerNetwork);
|
|
206
|
+
this.protocol = buildBlockTransferProtocol(protocolPrefix);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Pull blocks from the remote peer. */
|
|
210
|
+
async pullBlocks(
|
|
211
|
+
blockIds: string[],
|
|
212
|
+
reason: BlockTransferRequest['reason'] = 'rebalance'
|
|
213
|
+
): Promise<BlockTransferResponse> {
|
|
214
|
+
const request: BlockTransferRequest = { type: 'pull', blockIds, reason };
|
|
215
|
+
return await this.processMessage<BlockTransferResponse>(request, this.protocol);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Push blocks to the remote peer. */
|
|
219
|
+
async pushBlocks(
|
|
220
|
+
blockIds: string[],
|
|
221
|
+
blockDataBuffers: Uint8Array[],
|
|
222
|
+
reason: BlockTransferRequest['reason'] = 'rebalance'
|
|
223
|
+
): Promise<BlockTransferResponse> {
|
|
224
|
+
const blockData: Record<string, string> = {};
|
|
225
|
+
for (let i = 0; i < blockIds.length; i++) {
|
|
226
|
+
blockData[blockIds[i]!] = Buffer.from(blockDataBuffers[i]!).toString('base64');
|
|
227
|
+
}
|
|
228
|
+
const request: BlockTransferRequest = { type: 'push', blockIds, reason, blockData };
|
|
229
|
+
return await this.processMessage<BlockTransferResponse>(request, this.protocol);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { IRepo, IPeerNetwork } from '@optimystic/db-core';
|
|
2
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
3
|
+
import type { PartitionDetector } from './partition-detector.js';
|
|
4
|
+
import type { RestorationCoordinator } from '../storage/restoration-coordinator-v2.js';
|
|
5
|
+
import { BlockTransferClient } from './block-transfer-service.js';
|
|
6
|
+
import type { RebalanceEvent } from './rebalance-monitor.js';
|
|
7
|
+
import { createLogger } from '../logger.js';
|
|
8
|
+
|
|
9
|
+
const log = createLogger('block-transfer');
|
|
10
|
+
|
|
11
|
+
export interface BlockTransferConfig {
|
|
12
|
+
/** Max concurrent transfers. Default: 4 */
|
|
13
|
+
maxConcurrency?: number;
|
|
14
|
+
/** Timeout per block transfer (ms). Default: 30000 */
|
|
15
|
+
transferTimeoutMs?: number;
|
|
16
|
+
/** Retry attempts for failed transfers. Default: 2 */
|
|
17
|
+
maxRetries?: number;
|
|
18
|
+
/** Whether to push blocks to new owners proactively. Default: true */
|
|
19
|
+
enablePush?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Coordinates block transfers in response to rebalance events.
|
|
24
|
+
*
|
|
25
|
+
* For gained blocks: delegates to RestorationCoordinator.restore() which
|
|
26
|
+
* already handles ring-based discovery and fetching.
|
|
27
|
+
*
|
|
28
|
+
* For lost blocks: proactively pushes block data to new responsible peers
|
|
29
|
+
* via the BlockTransfer protocol.
|
|
30
|
+
*/
|
|
31
|
+
export class BlockTransferCoordinator {
|
|
32
|
+
private readonly maxConcurrency: number;
|
|
33
|
+
private readonly transferTimeoutMs: number;
|
|
34
|
+
private readonly maxRetries: number;
|
|
35
|
+
private readonly enablePush: boolean;
|
|
36
|
+
private inFlight = new Set<string>();
|
|
37
|
+
private concurrency = 0;
|
|
38
|
+
private readonly waitQueue: Array<() => void> = [];
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
private readonly repo: IRepo,
|
|
42
|
+
private readonly peerNetwork: IPeerNetwork,
|
|
43
|
+
private readonly restorationCoordinator: RestorationCoordinator,
|
|
44
|
+
private readonly partitionDetector: PartitionDetector,
|
|
45
|
+
private readonly protocolPrefix: string = '',
|
|
46
|
+
config: BlockTransferConfig = {}
|
|
47
|
+
) {
|
|
48
|
+
this.maxConcurrency = config.maxConcurrency ?? 4;
|
|
49
|
+
this.transferTimeoutMs = config.transferTimeoutMs ?? 30000;
|
|
50
|
+
this.maxRetries = config.maxRetries ?? 2;
|
|
51
|
+
this.enablePush = config.enablePush ?? true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Pull blocks that this node has gained responsibility for.
|
|
56
|
+
* Uses RestorationCoordinator to discover holders and fetch block data.
|
|
57
|
+
*/
|
|
58
|
+
async pullBlocks(blockIds: string[]): Promise<{ succeeded: string[]; failed: string[] }> {
|
|
59
|
+
if (this.partitionDetector.detectPartition()) {
|
|
60
|
+
log('pull:partition-detected, skipping %d blocks', blockIds.length);
|
|
61
|
+
return { succeeded: [], failed: blockIds };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const succeeded: string[] = [];
|
|
65
|
+
const failed: string[] = [];
|
|
66
|
+
|
|
67
|
+
const ids = blockIds.filter(id => !this.inFlight.has(`pull:${id}`));
|
|
68
|
+
|
|
69
|
+
await Promise.all(ids.map(id => this.executePull(id, succeeded, failed)));
|
|
70
|
+
|
|
71
|
+
return { succeeded, failed };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Push blocks that this node has lost responsibility for to new owners.
|
|
76
|
+
*/
|
|
77
|
+
async pushBlocks(
|
|
78
|
+
blockIds: string[],
|
|
79
|
+
newOwners: Map<string, string[]>
|
|
80
|
+
): Promise<{ succeeded: string[]; failed: string[] }> {
|
|
81
|
+
if (!this.enablePush) {
|
|
82
|
+
return { succeeded: [], failed: [] };
|
|
83
|
+
}
|
|
84
|
+
if (this.partitionDetector.detectPartition()) {
|
|
85
|
+
log('push:partition-detected, skipping %d blocks', blockIds.length);
|
|
86
|
+
return { succeeded: [], failed: blockIds };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const succeeded: string[] = [];
|
|
90
|
+
const failed: string[] = [];
|
|
91
|
+
|
|
92
|
+
const ids = blockIds.filter(id => !this.inFlight.has(`push:${id}`) && newOwners.has(id));
|
|
93
|
+
|
|
94
|
+
await Promise.all(ids.map(id => this.executePush(id, newOwners, succeeded, failed)));
|
|
95
|
+
|
|
96
|
+
return { succeeded, failed };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Handle a complete rebalance event — pull gained, push lost.
|
|
101
|
+
*/
|
|
102
|
+
async handleRebalanceEvent(event: RebalanceEvent): Promise<void> {
|
|
103
|
+
log('rebalance:start gained=%d lost=%d', event.gained.length, event.lost.length);
|
|
104
|
+
|
|
105
|
+
const [pullResult, pushResult] = await Promise.all([
|
|
106
|
+
event.gained.length > 0 ? this.pullBlocks(event.gained) : { succeeded: [], failed: [] },
|
|
107
|
+
event.lost.length > 0 && event.newOwners.size > 0
|
|
108
|
+
? this.pushBlocks(event.lost, event.newOwners)
|
|
109
|
+
: { succeeded: [], failed: [] }
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
log('rebalance:done pull=%d/%d push=%d/%d',
|
|
113
|
+
pullResult.succeeded.length, event.gained.length,
|
|
114
|
+
pushResult.succeeded.length, event.lost.length);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async executePull(
|
|
118
|
+
blockId: string,
|
|
119
|
+
succeeded: string[],
|
|
120
|
+
failed: string[]
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const key = `pull:${blockId}`;
|
|
123
|
+
if (this.inFlight.has(key)) return;
|
|
124
|
+
this.inFlight.add(key);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
for (let attempt = 0; ; attempt++) {
|
|
128
|
+
await this.acquireSemaphore();
|
|
129
|
+
let archive: Awaited<ReturnType<RestorationCoordinator['restore']>>;
|
|
130
|
+
try {
|
|
131
|
+
archive = await this.withTimeout(
|
|
132
|
+
this.restorationCoordinator.restore(blockId),
|
|
133
|
+
this.transferTimeoutMs
|
|
134
|
+
);
|
|
135
|
+
} finally {
|
|
136
|
+
this.releaseSemaphore();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (archive) {
|
|
140
|
+
log('pull:ok block=%s', blockId);
|
|
141
|
+
succeeded.push(blockId);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (attempt < this.maxRetries) {
|
|
145
|
+
log('pull:retry block=%s attempt=%d', blockId, attempt + 1);
|
|
146
|
+
await this.delay(this.backoffMs(attempt));
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
log('pull:failed block=%s', blockId);
|
|
150
|
+
failed.push(blockId);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
} finally {
|
|
154
|
+
this.inFlight.delete(key);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async executePush(
|
|
159
|
+
blockId: string,
|
|
160
|
+
newOwners: Map<string, string[]>,
|
|
161
|
+
succeeded: string[],
|
|
162
|
+
failed: string[]
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const key = `push:${blockId}`;
|
|
165
|
+
if (this.inFlight.has(key)) return;
|
|
166
|
+
this.inFlight.add(key);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
for (let attempt = 0; ; attempt++) {
|
|
170
|
+
await this.acquireSemaphore();
|
|
171
|
+
let pushed = false;
|
|
172
|
+
try {
|
|
173
|
+
const owners = newOwners.get(blockId);
|
|
174
|
+
if (!owners || owners.length === 0) {
|
|
175
|
+
failed.push(blockId);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Read block data from local storage
|
|
180
|
+
const result = await this.repo.get({ blockIds: [blockId] });
|
|
181
|
+
const blockResult = result[blockId];
|
|
182
|
+
if (!blockResult?.block) {
|
|
183
|
+
log('push:no-local-data block=%s', blockId);
|
|
184
|
+
failed.push(blockId);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const blockData = new TextEncoder().encode(JSON.stringify(blockResult.block));
|
|
189
|
+
|
|
190
|
+
// Push to at least one new owner
|
|
191
|
+
for (const ownerPeerIdStr of owners) {
|
|
192
|
+
try {
|
|
193
|
+
const peerId = peerIdFromString(ownerPeerIdStr);
|
|
194
|
+
const client = new BlockTransferClient(peerId, this.peerNetwork, this.protocolPrefix);
|
|
195
|
+
const response = await this.withTimeout(
|
|
196
|
+
client.pushBlocks([blockId], [blockData]),
|
|
197
|
+
this.transferTimeoutMs
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (response && !response.missing.includes(blockId)) {
|
|
201
|
+
pushed = true;
|
|
202
|
+
log('push:ok block=%s peer=%s', blockId, ownerPeerIdStr);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
log('push:peer-error block=%s peer=%s err=%s',
|
|
207
|
+
blockId, ownerPeerIdStr, (err as Error).message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
this.releaseSemaphore();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (pushed) {
|
|
215
|
+
succeeded.push(blockId);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (attempt < this.maxRetries) {
|
|
219
|
+
log('push:retry block=%s attempt=%d', blockId, attempt + 1);
|
|
220
|
+
await this.delay(this.backoffMs(attempt));
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
log('push:failed block=%s', blockId);
|
|
224
|
+
failed.push(blockId);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
} finally {
|
|
228
|
+
this.inFlight.delete(key);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- Semaphore for concurrency limiting ---
|
|
233
|
+
|
|
234
|
+
private async acquireSemaphore(): Promise<void> {
|
|
235
|
+
if (this.concurrency < this.maxConcurrency) {
|
|
236
|
+
this.concurrency++;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await new Promise<void>(resolve => this.waitQueue.push(resolve));
|
|
240
|
+
this.concurrency++;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private releaseSemaphore(): void {
|
|
244
|
+
this.concurrency--;
|
|
245
|
+
const next = this.waitQueue.shift();
|
|
246
|
+
if (next) next();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Helpers ---
|
|
250
|
+
|
|
251
|
+
private backoffMs(attempt: number): number {
|
|
252
|
+
return Math.min(1000 * Math.pow(2, attempt), 10000);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private delay(ms: number): Promise<void> {
|
|
256
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | undefined> {
|
|
260
|
+
return Promise.race([
|
|
261
|
+
promise,
|
|
262
|
+
new Promise<undefined>(resolve => setTimeout(() => resolve(undefined), ms))
|
|
263
|
+
]);
|
|
264
|
+
}
|
|
265
|
+
}
|