@peerbit/shared-log 3.1.10 → 4.0.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/lib/esm/__benchmark__/index.js +12 -3
- package/lib/esm/__benchmark__/index.js.map +1 -1
- package/lib/esm/blocks.d.ts +6 -0
- package/lib/esm/blocks.js +29 -0
- package/lib/esm/blocks.js.map +1 -0
- package/lib/esm/exchange-heads.d.ts +0 -1
- package/lib/esm/exchange-heads.js +0 -1
- package/lib/esm/exchange-heads.js.map +1 -1
- package/lib/esm/index.d.ts +80 -42
- package/lib/esm/index.js +685 -249
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/pid.d.ts +24 -0
- package/lib/esm/pid.js +60 -0
- package/lib/esm/pid.js.map +1 -0
- package/lib/esm/replication.d.ts +28 -5
- package/lib/esm/replication.js +37 -2
- package/lib/esm/replication.js.map +1 -1
- package/lib/esm/role.d.ts +22 -2
- package/lib/esm/role.js +57 -9
- package/lib/esm/role.js.map +1 -1
- package/package.json +9 -8
- package/src/__benchmark__/index.ts +13 -4
- package/src/blocks.ts +19 -0
- package/src/exchange-heads.ts +1 -2
- package/src/index.ts +982 -350
- package/src/pid.ts +104 -0
- package/src/replication.ts +49 -4
- package/src/role.ts +67 -6
package/src/index.ts
CHANGED
|
@@ -7,19 +7,10 @@ import {
|
|
|
7
7
|
LogEvents,
|
|
8
8
|
LogProperties
|
|
9
9
|
} from "@peerbit/log";
|
|
10
|
-
import { Program } from "@peerbit/program";
|
|
11
|
-
import {
|
|
12
|
-
BinaryReader,
|
|
13
|
-
BinaryWriter,
|
|
14
|
-
BorshError,
|
|
15
|
-
deserialize,
|
|
16
|
-
field,
|
|
17
|
-
serialize,
|
|
18
|
-
variant
|
|
19
|
-
} from "@dao-xyz/borsh";
|
|
10
|
+
import { Program, ProgramEvents } from "@peerbit/program";
|
|
11
|
+
import { BinaryWriter, BorshError, field, variant } from "@dao-xyz/borsh";
|
|
20
12
|
import {
|
|
21
13
|
AccessError,
|
|
22
|
-
getPublicKeyFromPeerId,
|
|
23
14
|
PublicSignKey,
|
|
24
15
|
sha256,
|
|
25
16
|
sha256Base64Sync
|
|
@@ -36,24 +27,35 @@ import {
|
|
|
36
27
|
SubscriptionEvent,
|
|
37
28
|
UnsubcriptionEvent
|
|
38
29
|
} from "@peerbit/pubsub-interface";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import { REPLICATOR_TYPE_VARIANT, Observer, Replicator, Role } from "./role.js";
|
|
30
|
+
import { AbortError, delay, TimeoutError, waitFor } from "@peerbit/time";
|
|
31
|
+
import { Observer, Replicator, Role } from "./role.js";
|
|
42
32
|
import {
|
|
43
33
|
AbsoluteReplicas,
|
|
44
|
-
MinReplicas,
|
|
45
34
|
ReplicationError,
|
|
35
|
+
ReplicationLimits,
|
|
36
|
+
ReplicatorRect,
|
|
37
|
+
RequestRoleMessage,
|
|
38
|
+
ResponseRoleMessage,
|
|
46
39
|
decodeReplicas,
|
|
47
40
|
encodeReplicas,
|
|
41
|
+
hashToUniformNumber,
|
|
48
42
|
maxReplicas
|
|
49
43
|
} from "./replication.js";
|
|
50
44
|
import pDefer, { DeferredPromise } from "p-defer";
|
|
51
45
|
import { Cache } from "@peerbit/cache";
|
|
52
|
-
|
|
46
|
+
import { CustomEvent } from "@libp2p/interface";
|
|
47
|
+
import yallist from "yallist";
|
|
48
|
+
import { AcknowledgeDelivery, SilentDelivery } from "@peerbit/stream-interface";
|
|
49
|
+
import { AnyBlockStore, RemoteBlocks } from "@peerbit/blocks";
|
|
50
|
+
import { BlocksMessage } from "./blocks.js";
|
|
51
|
+
import debounce from "p-debounce";
|
|
52
|
+
import { PIDReplicationController, ReplicationErrorFunction } from "./pid.js";
|
|
53
|
+
export type { ReplicationErrorFunction };
|
|
53
54
|
export * from "./replication.js";
|
|
55
|
+
|
|
54
56
|
export { Observer, Replicator, Role };
|
|
55
57
|
|
|
56
|
-
export const logger = loggerFn({ module: "
|
|
58
|
+
export const logger = loggerFn({ module: "shared-log" });
|
|
57
59
|
|
|
58
60
|
const groupByGid = async <T extends Entry<any> | EntryWithRefs<any>>(
|
|
59
61
|
entries: T[]
|
|
@@ -73,30 +75,74 @@ const groupByGid = async <T extends Entry<any> | EntryWithRefs<any>>(
|
|
|
73
75
|
return groupByGid;
|
|
74
76
|
};
|
|
75
77
|
|
|
76
|
-
export type SyncFilter = (entries: Entry<any>) => Promise<boolean> | boolean;
|
|
77
|
-
|
|
78
|
-
type ReplicationLimits = { min: MinReplicas; max?: MinReplicas };
|
|
79
78
|
export type ReplicationLimitsOptions =
|
|
80
79
|
| Partial<ReplicationLimits>
|
|
81
80
|
| { min?: number; max?: number };
|
|
82
81
|
|
|
83
|
-
|
|
82
|
+
type StringRoleOptions = "observer" | "replicator";
|
|
83
|
+
|
|
84
|
+
type AdaptiveReplicatorOptions = {
|
|
85
|
+
type: "replicator";
|
|
86
|
+
limits?: { memory: number };
|
|
87
|
+
error?: ReplicationErrorFunction;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type FixedReplicatorOptions = {
|
|
91
|
+
type: "replicator";
|
|
92
|
+
factor: number;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type ObserverType = {
|
|
96
|
+
type: "observer";
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type RoleOptions =
|
|
100
|
+
| StringRoleOptions
|
|
101
|
+
| ObserverType
|
|
102
|
+
| FixedReplicatorOptions
|
|
103
|
+
| AdaptiveReplicatorOptions;
|
|
104
|
+
|
|
105
|
+
const isAdaptiveReplicatorOption = (
|
|
106
|
+
options: FixedReplicatorOptions | AdaptiveReplicatorOptions
|
|
107
|
+
): options is AdaptiveReplicatorOptions => {
|
|
108
|
+
if (
|
|
109
|
+
(options as AdaptiveReplicatorOptions).limits ||
|
|
110
|
+
(options as AdaptiveReplicatorOptions).error ||
|
|
111
|
+
(options as FixedReplicatorOptions).factor == null
|
|
112
|
+
) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type SharedLogOptions = {
|
|
119
|
+
role?: RoleOptions;
|
|
84
120
|
replicas?: ReplicationLimitsOptions;
|
|
85
|
-
sync?: SyncFilter;
|
|
86
|
-
role?: Role;
|
|
87
121
|
respondToIHaveTimeout?: number;
|
|
88
122
|
canReplicate?: (publicKey: PublicSignKey) => Promise<boolean> | boolean;
|
|
89
|
-
}
|
|
123
|
+
};
|
|
90
124
|
|
|
91
125
|
export const DEFAULT_MIN_REPLICAS = 2;
|
|
126
|
+
export const WAIT_FOR_REPLICATOR_TIMEOUT = 9000;
|
|
127
|
+
export const WAIT_FOR_ROLE_MATURITY = 5000;
|
|
128
|
+
const REBALANCE_DEBOUNCE_INTERAVAL = 50;
|
|
92
129
|
|
|
93
130
|
export type Args<T> = LogProperties<T> & LogEvents<T> & SharedLogOptions;
|
|
131
|
+
|
|
94
132
|
export type SharedAppendOptions<T> = AppendOptions<T> & {
|
|
95
133
|
replicas?: AbsoluteReplicas | number;
|
|
96
134
|
};
|
|
97
135
|
|
|
136
|
+
type UpdateRoleEvent = { publicKey: PublicSignKey; role: Role };
|
|
137
|
+
export interface SharedLogEvents extends ProgramEvents {
|
|
138
|
+
role: CustomEvent<UpdateRoleEvent>;
|
|
139
|
+
}
|
|
140
|
+
|
|
98
141
|
@variant("shared_log")
|
|
99
|
-
export class SharedLog<T = Uint8Array> extends Program<
|
|
142
|
+
export class SharedLog<T = Uint8Array> extends Program<
|
|
143
|
+
Args<T>,
|
|
144
|
+
SharedLogEvents
|
|
145
|
+
> {
|
|
100
146
|
@field({ type: Log })
|
|
101
147
|
log: Log<T>;
|
|
102
148
|
|
|
@@ -104,22 +150,23 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
104
150
|
rpc: RPC<TransportMessage, TransportMessage>;
|
|
105
151
|
|
|
106
152
|
// options
|
|
107
|
-
private _sync?: SyncFilter;
|
|
108
153
|
private _role: Observer | Replicator;
|
|
154
|
+
private _roleOptions: AdaptiveReplicatorOptions | Observer | Replicator;
|
|
109
155
|
|
|
110
|
-
private _sortedPeersCache:
|
|
111
|
-
private
|
|
156
|
+
private _sortedPeersCache: yallist<ReplicatorRect> | undefined;
|
|
157
|
+
private _totalParticipation: number;
|
|
112
158
|
private _gidPeersHistory: Map<string, Set<string>>;
|
|
113
159
|
|
|
114
160
|
private _onSubscriptionFn: (arg: any) => any;
|
|
115
161
|
private _onUnsubscriptionFn: (arg: any) => any;
|
|
116
162
|
|
|
117
163
|
private _canReplicate?: (
|
|
118
|
-
publicKey: PublicSignKey
|
|
164
|
+
publicKey: PublicSignKey,
|
|
165
|
+
role: Replicator
|
|
119
166
|
) => Promise<boolean> | boolean;
|
|
120
167
|
|
|
121
168
|
private _logProperties?: LogProperties<T> & LogEvents<T>;
|
|
122
|
-
|
|
169
|
+
private _closeController: AbortController;
|
|
123
170
|
private _loadedOnce = false;
|
|
124
171
|
private _gidParentCache: Cache<Entry<any>[]>;
|
|
125
172
|
private _respondToIHaveTimeout;
|
|
@@ -134,9 +181,19 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
134
181
|
|
|
135
182
|
private _pendingIHave: Map<
|
|
136
183
|
string,
|
|
137
|
-
{ clear: () => void; callback: () => void }
|
|
184
|
+
{ clear: () => void; callback: (entry: Entry<T>) => void }
|
|
138
185
|
>;
|
|
139
186
|
|
|
187
|
+
private latestRoleMessages: Map<string, bigint>;
|
|
188
|
+
|
|
189
|
+
private remoteBlocks: RemoteBlocks;
|
|
190
|
+
|
|
191
|
+
private openTime: number;
|
|
192
|
+
|
|
193
|
+
private rebalanceParticipationDebounced:
|
|
194
|
+
| ReturnType<typeof debounce>
|
|
195
|
+
| undefined;
|
|
196
|
+
|
|
140
197
|
replicas: ReplicationLimits;
|
|
141
198
|
|
|
142
199
|
constructor(properties?: { id?: Uint8Array }) {
|
|
@@ -149,37 +206,93 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
149
206
|
return this._role;
|
|
150
207
|
}
|
|
151
208
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this._role = role;
|
|
155
|
-
await this.initializeWithRole();
|
|
156
|
-
await this.rpc.subscribe(serialize(this._role));
|
|
157
|
-
|
|
158
|
-
if (wasRepicators) {
|
|
159
|
-
await this.replicationReorganization();
|
|
160
|
-
}
|
|
209
|
+
get totalParticipation(): number {
|
|
210
|
+
return this._totalParticipation;
|
|
161
211
|
}
|
|
162
212
|
|
|
163
|
-
private
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
213
|
+
private setupRole(options?: RoleOptions) {
|
|
214
|
+
this.rebalanceParticipationDebounced = undefined;
|
|
215
|
+
|
|
216
|
+
const setupDebouncedRebalancing = (options?: AdaptiveReplicatorOptions) => {
|
|
217
|
+
this.replicationController = new PIDReplicationController({
|
|
218
|
+
targetMemoryLimit: options?.limits?.memory,
|
|
219
|
+
errorFunction: options?.error
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
this.rebalanceParticipationDebounced = debounce(
|
|
223
|
+
() => this.rebalanceParticipation(),
|
|
224
|
+
REBALANCE_DEBOUNCE_INTERAVAL // TODO make dynamic
|
|
168
225
|
);
|
|
226
|
+
};
|
|
169
227
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
228
|
+
if (options instanceof Observer || options instanceof Replicator) {
|
|
229
|
+
throw new Error("Unsupported role option type");
|
|
230
|
+
} else if (options === "observer") {
|
|
231
|
+
this._roleOptions = new Observer();
|
|
232
|
+
} else if (options === "replicator") {
|
|
233
|
+
setupDebouncedRebalancing();
|
|
234
|
+
this._roleOptions = { type: options };
|
|
235
|
+
} else if (options) {
|
|
236
|
+
if (options.type === "replicator") {
|
|
237
|
+
if (isAdaptiveReplicatorOption(options)) {
|
|
238
|
+
setupDebouncedRebalancing(options);
|
|
239
|
+
this._roleOptions = options;
|
|
240
|
+
} else {
|
|
241
|
+
this._roleOptions = new Replicator({ factor: options.factor });
|
|
242
|
+
}
|
|
179
243
|
} else {
|
|
180
|
-
|
|
244
|
+
this._roleOptions = new Observer();
|
|
181
245
|
}
|
|
246
|
+
} else {
|
|
247
|
+
// Default option
|
|
248
|
+
setupDebouncedRebalancing();
|
|
249
|
+
this._roleOptions = { type: "replicator" };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// setup the initial role
|
|
253
|
+
if (
|
|
254
|
+
this._roleOptions instanceof Replicator ||
|
|
255
|
+
this._roleOptions instanceof Observer
|
|
256
|
+
) {
|
|
257
|
+
this._role = this._roleOptions; // Fixed
|
|
258
|
+
} else {
|
|
259
|
+
this._role = new Replicator({
|
|
260
|
+
// initial role in a dynamic setup
|
|
261
|
+
factor: 1,
|
|
262
|
+
timestamp: BigInt(+new Date())
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return this._role;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async updateRole(role: RoleOptions, onRoleChange = true) {
|
|
270
|
+
return this._updateRole(this.setupRole(role), onRoleChange);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private async _updateRole(
|
|
274
|
+
role: Observer | Replicator = this._role,
|
|
275
|
+
onRoleChange = true
|
|
276
|
+
) {
|
|
277
|
+
this._role = role;
|
|
278
|
+
const { changed } = await this._modifyReplicators(
|
|
279
|
+
this.role,
|
|
280
|
+
this.node.identity.publicKey
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (!this._loadedOnce) {
|
|
284
|
+
await this.log.load();
|
|
285
|
+
this._loadedOnce = true;
|
|
182
286
|
}
|
|
287
|
+
await this.rpc.subscribe();
|
|
288
|
+
|
|
289
|
+
await this.rpc.send(new ResponseRoleMessage(role));
|
|
290
|
+
|
|
291
|
+
if (onRoleChange && changed) {
|
|
292
|
+
this.onRoleChange(undefined, this._role, this.node.identity.publicKey);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return changed;
|
|
183
296
|
}
|
|
184
297
|
|
|
185
298
|
async append(
|
|
@@ -207,14 +320,27 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
207
320
|
}
|
|
208
321
|
|
|
209
322
|
const result = await this.log.append(data, appendOptions);
|
|
323
|
+
const leaders = await this.findLeaders(
|
|
324
|
+
result.entry.meta.gid,
|
|
325
|
+
decodeReplicas(result.entry).getValue(this)
|
|
326
|
+
);
|
|
327
|
+
const isLeader = leaders.includes(this.node.identity.publicKey.hashcode());
|
|
210
328
|
|
|
211
329
|
await this.rpc.send(
|
|
212
330
|
await createExchangeHeadsMessage(
|
|
213
331
|
this.log,
|
|
214
332
|
[result.entry],
|
|
215
333
|
this._gidParentCache
|
|
216
|
-
)
|
|
334
|
+
),
|
|
335
|
+
{
|
|
336
|
+
mode: isLeader
|
|
337
|
+
? new SilentDelivery({ redundancy: 1, to: leaders })
|
|
338
|
+
: new AcknowledgeDelivery({ redundancy: 1, to: leaders })
|
|
339
|
+
}
|
|
217
340
|
);
|
|
341
|
+
|
|
342
|
+
this.rebalanceParticipationDebounced?.();
|
|
343
|
+
|
|
218
344
|
return result;
|
|
219
345
|
}
|
|
220
346
|
|
|
@@ -231,21 +357,24 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
231
357
|
: options.replicas.max
|
|
232
358
|
: undefined
|
|
233
359
|
};
|
|
360
|
+
|
|
234
361
|
this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 10 * 1000; // TODO make into arg
|
|
235
362
|
this._pendingDeletes = new Map();
|
|
236
363
|
this._pendingIHave = new Map();
|
|
364
|
+
this.latestRoleMessages = new Map();
|
|
365
|
+
this.openTime = +new Date();
|
|
237
366
|
|
|
238
367
|
this._gidParentCache = new Cache({ max: 1000 });
|
|
368
|
+
this._closeController = new AbortController();
|
|
239
369
|
|
|
240
370
|
this._canReplicate = options?.canReplicate;
|
|
241
|
-
this._sync = options?.sync;
|
|
242
371
|
this._logProperties = options;
|
|
243
|
-
this._role = options?.role || new Replicator();
|
|
244
372
|
|
|
245
|
-
this.
|
|
246
|
-
this._onSubscriptionFn = this._onSubscription.bind(this);
|
|
373
|
+
this.setupRole(options?.role);
|
|
247
374
|
|
|
248
|
-
this.
|
|
375
|
+
this._onSubscriptionFn = this._onSubscription.bind(this);
|
|
376
|
+
this._totalParticipation = 0;
|
|
377
|
+
this._sortedPeersCache = yallist.create();
|
|
249
378
|
this._gidPeersHistory = new Map();
|
|
250
379
|
|
|
251
380
|
await this.node.services.pubsub.addEventListener(
|
|
@@ -259,9 +388,26 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
259
388
|
this._onUnsubscriptionFn
|
|
260
389
|
);
|
|
261
390
|
|
|
262
|
-
|
|
263
|
-
|
|
391
|
+
const id = sha256Base64Sync(this.log.id);
|
|
392
|
+
const storage = await this.node.memory.sublevel(id);
|
|
393
|
+
const localBlocks = await new AnyBlockStore(
|
|
394
|
+
await storage.sublevel("blocks")
|
|
395
|
+
);
|
|
396
|
+
const cache = await storage.sublevel("cache");
|
|
397
|
+
|
|
398
|
+
this.remoteBlocks = new RemoteBlocks({
|
|
399
|
+
local: localBlocks,
|
|
400
|
+
publish: (message, options) =>
|
|
401
|
+
this.rpc.send(new BlocksMessage(message), {
|
|
402
|
+
to: options?.to
|
|
403
|
+
}),
|
|
404
|
+
waitFor: this.rpc.waitFor.bind(this.rpc)
|
|
405
|
+
});
|
|
264
406
|
|
|
407
|
+
await this.remoteBlocks.start();
|
|
408
|
+
|
|
409
|
+
await this.log.open(this.remoteBlocks, this.node.identity, {
|
|
410
|
+
keychain: this.node.services.keychain,
|
|
265
411
|
...this._logProperties,
|
|
266
412
|
onChange: (change) => {
|
|
267
413
|
if (this._pendingIHave.size > 0) {
|
|
@@ -269,7 +415,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
269
415
|
const ih = this._pendingIHave.get(added.hash);
|
|
270
416
|
if (ih) {
|
|
271
417
|
ih.clear();
|
|
272
|
-
ih.callback();
|
|
418
|
+
ih.callback(added);
|
|
273
419
|
}
|
|
274
420
|
}
|
|
275
421
|
}
|
|
@@ -304,42 +450,37 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
304
450
|
return this._logProperties?.canAppend?.(entry) ?? true;
|
|
305
451
|
},
|
|
306
452
|
trim: this._logProperties?.trim && {
|
|
307
|
-
...this._logProperties?.trim
|
|
308
|
-
filter: {
|
|
309
|
-
canTrim: async (entry) =>
|
|
310
|
-
!(await this.isLeader(
|
|
311
|
-
entry.meta.gid,
|
|
312
|
-
decodeReplicas(entry).getValue(this)
|
|
313
|
-
)), // TODO types
|
|
314
|
-
cacheId: () => this._lastSubscriptionMessageId
|
|
315
|
-
}
|
|
453
|
+
...this._logProperties?.trim
|
|
316
454
|
},
|
|
317
|
-
cache:
|
|
318
|
-
this.node.memory &&
|
|
319
|
-
(await this.node.memory.sublevel(sha256Base64Sync(this.log.id)))
|
|
455
|
+
cache: cache
|
|
320
456
|
});
|
|
321
457
|
|
|
322
|
-
|
|
458
|
+
// Open for communcation
|
|
459
|
+
await this.rpc.open({
|
|
460
|
+
queryType: TransportMessage,
|
|
461
|
+
responseType: TransportMessage,
|
|
462
|
+
responseHandler: this._onMessage.bind(this),
|
|
463
|
+
topic: this.topic
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
await this._updateRole();
|
|
467
|
+
await this.rebalanceParticipation();
|
|
323
468
|
|
|
324
469
|
// Take into account existing subscription
|
|
325
470
|
(await this.node.services.pubsub.getSubscribers(this.topic))?.forEach(
|
|
326
471
|
(v, k) => {
|
|
327
|
-
this.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
);
|
|
472
|
+
if (v.equals(this.node.identity.publicKey)) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
this.handleSubscriptionChange(v, [this.topic], true);
|
|
332
476
|
}
|
|
333
477
|
);
|
|
478
|
+
}
|
|
334
479
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
responseHandler: this._onMessage.bind(this),
|
|
340
|
-
topic: this.topic,
|
|
341
|
-
subscriptionData: serialize(this.role)
|
|
342
|
-
});
|
|
480
|
+
async getMemoryUsage() {
|
|
481
|
+
return (
|
|
482
|
+
((await this.log.memory?.size()) || 0) + (await this.log.blocks.size())
|
|
483
|
+
);
|
|
343
484
|
}
|
|
344
485
|
|
|
345
486
|
get topic() {
|
|
@@ -347,6 +488,19 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
347
488
|
}
|
|
348
489
|
|
|
349
490
|
private async _close() {
|
|
491
|
+
this._closeController.abort();
|
|
492
|
+
|
|
493
|
+
this.node.services.pubsub.removeEventListener(
|
|
494
|
+
"subscribe",
|
|
495
|
+
this._onSubscriptionFn
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
|
|
499
|
+
this.node.services.pubsub.removeEventListener(
|
|
500
|
+
"unsubscribe",
|
|
501
|
+
this._onUnsubscriptionFn
|
|
502
|
+
);
|
|
503
|
+
|
|
350
504
|
for (const [k, v] of this._pendingDeletes) {
|
|
351
505
|
v.clear();
|
|
352
506
|
v.promise.resolve(); // TODO or reject?
|
|
@@ -355,24 +509,15 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
355
509
|
v.clear();
|
|
356
510
|
}
|
|
357
511
|
|
|
512
|
+
await this.remoteBlocks.stop();
|
|
358
513
|
this._gidParentCache.clear();
|
|
359
514
|
this._pendingDeletes = new Map();
|
|
360
515
|
this._pendingIHave = new Map();
|
|
516
|
+
this.latestRoleMessages.clear();
|
|
361
517
|
|
|
362
518
|
this._gidPeersHistory = new Map();
|
|
363
519
|
this._sortedPeersCache = undefined;
|
|
364
520
|
this._loadedOnce = false;
|
|
365
|
-
|
|
366
|
-
this.node.services.pubsub.removeEventListener(
|
|
367
|
-
"subscribe",
|
|
368
|
-
this._onSubscriptionFn
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
|
|
372
|
-
this.node.services.pubsub.removeEventListener(
|
|
373
|
-
"unsubscribe",
|
|
374
|
-
this._onUnsubscriptionFn
|
|
375
|
-
);
|
|
376
521
|
}
|
|
377
522
|
async close(from?: Program): Promise<boolean> {
|
|
378
523
|
const superClosed = await super.close(from);
|
|
@@ -389,8 +534,8 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
389
534
|
if (!superDropped) {
|
|
390
535
|
return superDropped;
|
|
391
536
|
}
|
|
392
|
-
await this._close();
|
|
393
537
|
await this.log.drop();
|
|
538
|
+
await this._close();
|
|
394
539
|
return true;
|
|
395
540
|
}
|
|
396
541
|
|
|
@@ -411,7 +556,6 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
411
556
|
*/
|
|
412
557
|
|
|
413
558
|
const { heads } = msg;
|
|
414
|
-
// replication topic === trustedNetwork address
|
|
415
559
|
|
|
416
560
|
logger.debug(
|
|
417
561
|
`${this.node.identity.publicKey.hashcode()}: Recieved heads: ${
|
|
@@ -431,26 +575,32 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
431
575
|
}
|
|
432
576
|
}
|
|
433
577
|
|
|
434
|
-
if (
|
|
435
|
-
|
|
578
|
+
if (filteredHeads.length === 0) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
436
581
|
|
|
437
|
-
|
|
438
|
-
|
|
582
|
+
const toMerge: EntryWithRefs<any>[] = [];
|
|
583
|
+
let toDelete: Entry<any>[] | undefined = undefined;
|
|
584
|
+
let maybeDelete: EntryWithRefs<any>[][] | undefined = undefined;
|
|
439
585
|
|
|
440
|
-
|
|
586
|
+
const groupedByGid = await groupByGid(filteredHeads);
|
|
587
|
+
const promises: Promise<void>[] = [];
|
|
441
588
|
|
|
442
|
-
|
|
589
|
+
for (const [gid, entries] of groupedByGid) {
|
|
590
|
+
const fn = async () => {
|
|
443
591
|
const headsWithGid = this.log.headsIndex.gids.get(gid);
|
|
592
|
+
|
|
444
593
|
const maxReplicasFromHead =
|
|
445
594
|
headsWithGid && headsWithGid.size > 0
|
|
446
595
|
? maxReplicas(this, [...headsWithGid.values()])
|
|
447
596
|
: this.replicas.min.getValue(this);
|
|
448
597
|
|
|
449
|
-
const maxReplicasFromNewEntries = maxReplicas(
|
|
450
|
-
|
|
451
|
-
|
|
598
|
+
const maxReplicasFromNewEntries = maxReplicas(
|
|
599
|
+
this,
|
|
600
|
+
entries.map((x) => x.entry)
|
|
601
|
+
);
|
|
452
602
|
|
|
453
|
-
const isLeader = await this.
|
|
603
|
+
const isLeader = await this.waitForIsLeader(
|
|
454
604
|
gid,
|
|
455
605
|
Math.max(maxReplicasFromHead, maxReplicasFromNewEntries)
|
|
456
606
|
);
|
|
@@ -481,50 +631,49 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
481
631
|
}. Because not leader`
|
|
482
632
|
);
|
|
483
633
|
}
|
|
484
|
-
}
|
|
634
|
+
};
|
|
635
|
+
promises.push(fn());
|
|
636
|
+
}
|
|
637
|
+
await Promise.all(promises);
|
|
485
638
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
639
|
+
if (this.closed) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (toMerge.length > 0) {
|
|
644
|
+
await this.log.join(toMerge);
|
|
645
|
+
toDelete &&
|
|
646
|
+
this.prune(toDelete).catch((e) => {
|
|
647
|
+
logger.error(e.toString());
|
|
648
|
+
});
|
|
649
|
+
this.rebalanceParticipationDebounced?.();
|
|
650
|
+
}
|
|
493
651
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
652
|
+
if (maybeDelete) {
|
|
653
|
+
for (const entries of maybeDelete as EntryWithRefs<any>[][]) {
|
|
654
|
+
const headsWithGid = this.log.headsIndex.gids.get(
|
|
655
|
+
entries[0].entry.meta.gid
|
|
656
|
+
);
|
|
657
|
+
if (headsWithGid && headsWithGid.size > 0) {
|
|
658
|
+
const minReplicas = maxReplicas(this, headsWithGid.values());
|
|
659
|
+
|
|
660
|
+
const isLeader = await this.isLeader(
|
|
661
|
+
entries[0].entry.meta.gid,
|
|
662
|
+
minReplicas
|
|
498
663
|
);
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const isLeader = await this.isLeader(
|
|
505
|
-
entries[0].entry.meta.gid,
|
|
506
|
-
minReplicas
|
|
507
|
-
);
|
|
508
|
-
if (!isLeader) {
|
|
509
|
-
Promise.all(
|
|
510
|
-
this.pruneSafely(entries.map((x) => x.entry))
|
|
511
|
-
).catch((e) => {
|
|
512
|
-
logger.error(e.toString());
|
|
513
|
-
});
|
|
514
|
-
}
|
|
664
|
+
|
|
665
|
+
if (!isLeader) {
|
|
666
|
+
this.prune(entries.map((x) => x.entry)).catch((e) => {
|
|
667
|
+
logger.error(e.toString());
|
|
668
|
+
});
|
|
515
669
|
}
|
|
516
670
|
}
|
|
517
671
|
}
|
|
518
|
-
} else {
|
|
519
|
-
await this.log.join(
|
|
520
|
-
await Promise.all(
|
|
521
|
-
filteredHeads.map((x) => this._sync!(x.entry))
|
|
522
|
-
).then((filter) => filteredHeads.filter((v, ix) => filter[ix]))
|
|
523
|
-
);
|
|
524
672
|
}
|
|
525
673
|
}
|
|
526
674
|
} else if (msg instanceof RequestIHave) {
|
|
527
675
|
const hasAndIsLeader: string[] = [];
|
|
676
|
+
|
|
528
677
|
for (const hash of msg.hashes) {
|
|
529
678
|
const indexedEntry = this.log.entryIndex.getShallow(hash);
|
|
530
679
|
if (
|
|
@@ -542,12 +691,21 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
542
691
|
clearTimeout(timeout);
|
|
543
692
|
prevPendingIHave?.clear();
|
|
544
693
|
},
|
|
545
|
-
callback: () => {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
694
|
+
callback: async (entry) => {
|
|
695
|
+
if (
|
|
696
|
+
await this.isLeader(
|
|
697
|
+
entry.meta.gid,
|
|
698
|
+
decodeReplicas(entry).getValue(this)
|
|
699
|
+
)
|
|
700
|
+
) {
|
|
701
|
+
this.rpc.send(new ResponseIHave({ hashes: [entry.hash] }), {
|
|
702
|
+
to: [context.from!]
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
prevPendingIHave && prevPendingIHave.callback(entry);
|
|
707
|
+
|
|
708
|
+
this._pendingIHave.delete(entry.hash);
|
|
551
709
|
}
|
|
552
710
|
};
|
|
553
711
|
const timeout = setTimeout(() => {
|
|
@@ -560,17 +718,71 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
560
718
|
this._pendingIHave.set(hash, pendingIHave);
|
|
561
719
|
}
|
|
562
720
|
}
|
|
563
|
-
this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
|
|
721
|
+
await this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
|
|
564
722
|
to: [context.from!]
|
|
565
723
|
});
|
|
566
724
|
} else if (msg instanceof ResponseIHave) {
|
|
567
725
|
for (const hash of msg.hashes) {
|
|
568
726
|
this._pendingDeletes.get(hash)?.callback(context.from!.hashcode());
|
|
569
727
|
}
|
|
728
|
+
} else if (msg instanceof BlocksMessage) {
|
|
729
|
+
await this.remoteBlocks.onMessage(msg.message);
|
|
730
|
+
} else if (msg instanceof RequestRoleMessage) {
|
|
731
|
+
if (!context.from) {
|
|
732
|
+
throw new Error("Missing form in update role message");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (context.from.equals(this.node.identity.publicKey)) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
await this.rpc.send(new ResponseRoleMessage({ role: this.role }), {
|
|
740
|
+
to: [context.from!]
|
|
741
|
+
});
|
|
742
|
+
} else if (msg instanceof ResponseRoleMessage) {
|
|
743
|
+
if (!context.from) {
|
|
744
|
+
throw new Error("Missing form in update role message");
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (context.from.equals(this.node.identity.publicKey)) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
this.waitFor(context.from, {
|
|
752
|
+
signal: this._closeController.signal,
|
|
753
|
+
timeout: WAIT_FOR_REPLICATOR_TIMEOUT
|
|
754
|
+
})
|
|
755
|
+
.then(async () => {
|
|
756
|
+
/* await delay(1000 * Math.random()) */
|
|
757
|
+
|
|
758
|
+
const prev = this.latestRoleMessages.get(context.from!.hashcode());
|
|
759
|
+
if (prev && prev > context.timestamp) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
this.latestRoleMessages.set(
|
|
763
|
+
context.from!.hashcode(),
|
|
764
|
+
context.timestamp
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
await this.modifyReplicators(msg.role, context.from!);
|
|
768
|
+
})
|
|
769
|
+
.catch((e) => {
|
|
770
|
+
if (e instanceof AbortError) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
logger.error(
|
|
775
|
+
"Failed to find peer who updated their role: " + e?.message
|
|
776
|
+
);
|
|
777
|
+
});
|
|
570
778
|
} else {
|
|
571
779
|
throw new Error("Unexpected message");
|
|
572
780
|
}
|
|
573
781
|
} catch (e: any) {
|
|
782
|
+
if (e instanceof AbortError) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
574
786
|
if (e instanceof BorshError) {
|
|
575
787
|
logger.trace(
|
|
576
788
|
`${this.node.identity.publicKey.hashcode()}: Failed to handle message on topic: ${JSON.stringify(
|
|
@@ -579,6 +791,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
579
791
|
);
|
|
580
792
|
return;
|
|
581
793
|
}
|
|
794
|
+
|
|
582
795
|
if (e instanceof AccessError) {
|
|
583
796
|
logger.trace(
|
|
584
797
|
`${this.node.identity.publicKey.hashcode()}: Failed to handle message for log: ${JSON.stringify(
|
|
@@ -591,41 +804,89 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
591
804
|
}
|
|
592
805
|
}
|
|
593
806
|
|
|
594
|
-
getReplicatorsSorted():
|
|
807
|
+
getReplicatorsSorted(): yallist<ReplicatorRect> | undefined {
|
|
595
808
|
return this._sortedPeersCache;
|
|
596
809
|
}
|
|
597
810
|
|
|
811
|
+
async waitForReplicator(...keys: PublicSignKey[]) {
|
|
812
|
+
const check = () => {
|
|
813
|
+
for (const k of keys) {
|
|
814
|
+
if (
|
|
815
|
+
!this.getReplicatorsSorted()
|
|
816
|
+
?.toArray()
|
|
817
|
+
?.find((x) => x.publicKey.equals(k))
|
|
818
|
+
) {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return true;
|
|
823
|
+
};
|
|
824
|
+
return waitFor(() => check(), { signal: this._closeController.signal });
|
|
825
|
+
}
|
|
826
|
+
|
|
598
827
|
async isLeader(
|
|
599
828
|
slot: { toString(): string },
|
|
600
|
-
numberOfLeaders: number
|
|
829
|
+
numberOfLeaders: number,
|
|
830
|
+
options?: {
|
|
831
|
+
candidates?: string[];
|
|
832
|
+
roleAge?: number;
|
|
833
|
+
}
|
|
601
834
|
): Promise<boolean> {
|
|
602
|
-
const isLeader = (
|
|
603
|
-
|
|
604
|
-
);
|
|
835
|
+
const isLeader = (
|
|
836
|
+
await this.findLeaders(slot, numberOfLeaders, options)
|
|
837
|
+
).find((l) => l === this.node.identity.publicKey.hashcode());
|
|
605
838
|
return !!isLeader;
|
|
606
839
|
}
|
|
607
840
|
|
|
841
|
+
private async waitForIsLeader(
|
|
842
|
+
slot: { toString(): string },
|
|
843
|
+
numberOfLeaders: number,
|
|
844
|
+
timeout = WAIT_FOR_REPLICATOR_TIMEOUT
|
|
845
|
+
): Promise<boolean> {
|
|
846
|
+
return new Promise((res, rej) => {
|
|
847
|
+
const removeListeners = () => {
|
|
848
|
+
this.events.removeEventListener("role", roleListener);
|
|
849
|
+
this._closeController.signal.addEventListener("abort", abortListener);
|
|
850
|
+
};
|
|
851
|
+
const abortListener = () => {
|
|
852
|
+
removeListeners();
|
|
853
|
+
clearTimeout(timer);
|
|
854
|
+
res(false);
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
const timer = setTimeout(() => {
|
|
858
|
+
removeListeners();
|
|
859
|
+
res(false);
|
|
860
|
+
}, timeout);
|
|
861
|
+
|
|
862
|
+
const check = () =>
|
|
863
|
+
this.isLeader(slot, numberOfLeaders).then((isLeader) => {
|
|
864
|
+
if (isLeader) {
|
|
865
|
+
removeListeners();
|
|
866
|
+
clearTimeout(timer);
|
|
867
|
+
res(isLeader);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
const roleListener = () => {
|
|
872
|
+
check();
|
|
873
|
+
};
|
|
874
|
+
this.events.addEventListener("role", roleListener);
|
|
875
|
+
this._closeController.signal.addEventListener("abort", abortListener);
|
|
876
|
+
|
|
877
|
+
check();
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
608
881
|
async findLeaders(
|
|
609
882
|
subject: { toString(): string },
|
|
610
|
-
|
|
883
|
+
numberOfLeaders: number,
|
|
884
|
+
options?: {
|
|
885
|
+
roleAge?: number;
|
|
886
|
+
}
|
|
611
887
|
): Promise<string[]> {
|
|
612
|
-
const lower = this.replicas.min.getValue(this);
|
|
613
|
-
const higher = this.replicas.max?.getValue(this) ?? Number.MAX_SAFE_INTEGER;
|
|
614
|
-
let numberOfLeaders = Math.max(
|
|
615
|
-
Math.min(higher, numberOfLeadersUnbounded),
|
|
616
|
-
lower
|
|
617
|
-
);
|
|
618
|
-
|
|
619
888
|
// For a fixed set or members, the choosen leaders will always be the same (address invariant)
|
|
620
889
|
// This allows for that same content is always chosen to be distributed to same peers, to remove unecessary copies
|
|
621
|
-
const peers: { hash: string; timestamp: number }[] =
|
|
622
|
-
this.getReplicatorsSorted() || [];
|
|
623
|
-
|
|
624
|
-
if (peers.length === 0) {
|
|
625
|
-
return [];
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
numberOfLeaders = Math.min(numberOfLeaders, peers.length);
|
|
629
890
|
|
|
630
891
|
// Convert this thing we wan't to distribute to 8 bytes so we get can convert it into a u64
|
|
631
892
|
// modulus into an index
|
|
@@ -634,31 +895,220 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
634
895
|
const seed = await sha256(utf8writer.finalize());
|
|
635
896
|
|
|
636
897
|
// convert hash of slot to a number
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
898
|
+
const cursor = hashToUniformNumber(seed); // bounded between 0 and 1
|
|
899
|
+
return this.findLeadersFromUniformNumber(cursor, numberOfLeaders, options);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
private findLeadersFromUniformNumber(
|
|
903
|
+
cursor: number,
|
|
904
|
+
numberOfLeaders: number,
|
|
905
|
+
options?: {
|
|
906
|
+
roleAge?: number;
|
|
907
|
+
}
|
|
908
|
+
) {
|
|
909
|
+
const leaders: Set<string> = new Set();
|
|
910
|
+
const width = 1; // this.getParticipationSum(roleAge);
|
|
911
|
+
const peers = this.getReplicatorsSorted();
|
|
912
|
+
if (!peers || peers?.length === 0) {
|
|
913
|
+
return [];
|
|
914
|
+
}
|
|
915
|
+
numberOfLeaders = Math.min(numberOfLeaders, peers.length);
|
|
916
|
+
|
|
917
|
+
const t = +new Date();
|
|
918
|
+
const roleAge =
|
|
919
|
+
options?.roleAge ??
|
|
920
|
+
Math.min(WAIT_FOR_ROLE_MATURITY, +new Date() - this.openTime);
|
|
921
|
+
|
|
646
922
|
for (let i = 0; i < numberOfLeaders; i++) {
|
|
647
|
-
|
|
923
|
+
let matured = 0;
|
|
924
|
+
const maybeIncrementMatured = (role: Replicator) => {
|
|
925
|
+
if (t - Number(role.timestamp) > roleAge) {
|
|
926
|
+
matured++;
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const x = ((cursor + i / numberOfLeaders) % 1) * width;
|
|
931
|
+
let currentNode = peers.head;
|
|
932
|
+
const diffs: { diff: number; rect: ReplicatorRect }[] = [];
|
|
933
|
+
while (currentNode) {
|
|
934
|
+
const start = currentNode.value.offset % width;
|
|
935
|
+
const absDelta = Math.abs(start - x);
|
|
936
|
+
const diff = Math.min(absDelta, width - absDelta);
|
|
937
|
+
|
|
938
|
+
if (diff < currentNode.value.role.factor / 2 + 0.00001) {
|
|
939
|
+
leaders.add(currentNode.value.publicKey.hashcode());
|
|
940
|
+
maybeIncrementMatured(currentNode.value.role);
|
|
941
|
+
} else {
|
|
942
|
+
diffs.push({
|
|
943
|
+
diff:
|
|
944
|
+
currentNode.value.role.factor > 0
|
|
945
|
+
? diff / currentNode.value.role.factor
|
|
946
|
+
: Number.MAX_SAFE_INTEGER,
|
|
947
|
+
rect: currentNode.value
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
currentNode = currentNode.next;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (matured === 0) {
|
|
955
|
+
diffs.sort((x, y) => x.diff - y.diff);
|
|
956
|
+
for (const node of diffs) {
|
|
957
|
+
leaders.add(node.rect.publicKey.hashcode());
|
|
958
|
+
maybeIncrementMatured(node.rect.role);
|
|
959
|
+
if (matured > 0) {
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
648
964
|
}
|
|
649
|
-
|
|
965
|
+
|
|
966
|
+
return [...leaders];
|
|
650
967
|
}
|
|
651
968
|
|
|
652
|
-
|
|
653
|
-
|
|
969
|
+
/**
|
|
970
|
+
*
|
|
971
|
+
* @returns groups where at least one in any group will have the entry you are looking for
|
|
972
|
+
*/
|
|
973
|
+
getReplicatorUnion(roleAge: number = WAIT_FOR_ROLE_MATURITY) {
|
|
974
|
+
// Total replication "width"
|
|
975
|
+
const width = 1; //this.getParticipationSum(roleAge);
|
|
976
|
+
|
|
977
|
+
// How much width you need to "query" to
|
|
978
|
+
|
|
979
|
+
const peers = this.getReplicatorsSorted()!; // TODO types
|
|
980
|
+
const minReplicas = Math.min(
|
|
981
|
+
peers.length,
|
|
982
|
+
this.replicas.min.getValue(this)
|
|
983
|
+
);
|
|
984
|
+
const coveringWidth = width / minReplicas;
|
|
985
|
+
|
|
986
|
+
let walker = peers.head;
|
|
987
|
+
if (this.role instanceof Replicator) {
|
|
988
|
+
// start at our node (local first)
|
|
989
|
+
while (walker) {
|
|
990
|
+
if (walker.value.publicKey.equals(this.node.identity.publicKey)) {
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
walker = walker.next;
|
|
994
|
+
}
|
|
995
|
+
} else {
|
|
996
|
+
const seed = Math.round(peers.length * Math.random()); // start at a random point
|
|
997
|
+
for (let i = 0; i < seed - 1; i++) {
|
|
998
|
+
if (walker?.next == null) {
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
walker = walker.next;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const set: string[] = [];
|
|
1006
|
+
let distance = 0;
|
|
1007
|
+
const startNode = walker;
|
|
1008
|
+
if (!startNode) {
|
|
1009
|
+
return [];
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
let nextPoint = startNode.value.offset;
|
|
1013
|
+
const t = +new Date();
|
|
1014
|
+
while (walker && distance < coveringWidth) {
|
|
1015
|
+
const absDelta = Math.abs(walker!.value.offset - nextPoint);
|
|
1016
|
+
const diff = Math.min(absDelta, width - absDelta);
|
|
1017
|
+
|
|
1018
|
+
if (diff < walker!.value.role.factor / 2 + 0.00001) {
|
|
1019
|
+
set.push(walker!.value.publicKey.hashcode());
|
|
1020
|
+
if (
|
|
1021
|
+
t - Number(walker!.value.role.timestamp) >
|
|
1022
|
+
roleAge /* ||
|
|
1023
|
+
walker!.value.publicKey.equals(this.node.identity.publicKey)) */
|
|
1024
|
+
) {
|
|
1025
|
+
nextPoint = (nextPoint + walker!.value.role.factor) % 1;
|
|
1026
|
+
distance += walker!.value.role.factor;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
walker = walker.next || peers.head;
|
|
1031
|
+
|
|
1032
|
+
if (
|
|
1033
|
+
walker?.value.publicKey &&
|
|
1034
|
+
startNode?.value.publicKey.equals(walker?.value.publicKey)
|
|
1035
|
+
) {
|
|
1036
|
+
break; // TODO throw error for failing to fetch ffull width
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
return set;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async replicator(
|
|
1044
|
+
entry: Entry<any>,
|
|
1045
|
+
options?: {
|
|
1046
|
+
candidates?: string[];
|
|
1047
|
+
roleAge?: number;
|
|
1048
|
+
}
|
|
1049
|
+
) {
|
|
1050
|
+
return this.isLeader(
|
|
1051
|
+
entry.gid,
|
|
1052
|
+
decodeReplicas(entry).getValue(this),
|
|
1053
|
+
options
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private onRoleChange(
|
|
1058
|
+
prev: Observer | Replicator | undefined,
|
|
1059
|
+
role: Observer | Replicator,
|
|
654
1060
|
publicKey: PublicSignKey
|
|
655
1061
|
) {
|
|
1062
|
+
if (this.closed) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
this.distribute();
|
|
1067
|
+
|
|
1068
|
+
if (role instanceof Replicator) {
|
|
1069
|
+
const timer = setTimeout(async () => {
|
|
1070
|
+
this._closeController.signal.removeEventListener("abort", listener);
|
|
1071
|
+
await this.rebalanceParticipationDebounced?.();
|
|
1072
|
+
this.distribute();
|
|
1073
|
+
}, WAIT_FOR_ROLE_MATURITY + 2000);
|
|
1074
|
+
|
|
1075
|
+
const listener = () => {
|
|
1076
|
+
clearTimeout(timer);
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
this._closeController.signal.addEventListener("abort", listener);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
this.events.dispatchEvent(
|
|
1083
|
+
new CustomEvent<UpdateRoleEvent>("role", {
|
|
1084
|
+
detail: { publicKey, role }
|
|
1085
|
+
})
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private async modifyReplicators(
|
|
1090
|
+
role: Observer | Replicator,
|
|
1091
|
+
publicKey: PublicSignKey
|
|
1092
|
+
) {
|
|
1093
|
+
const { prev, changed } = await this._modifyReplicators(role, publicKey);
|
|
1094
|
+
if (changed) {
|
|
1095
|
+
await this.rebalanceParticipationDebounced?.(); // await this.rebalanceParticipation(false);
|
|
1096
|
+
this.onRoleChange(prev, role, publicKey);
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
private async _modifyReplicators(
|
|
1103
|
+
role: Observer | Replicator,
|
|
1104
|
+
publicKey: PublicSignKey
|
|
1105
|
+
): Promise<{ prev?: Replicator; changed: boolean }> {
|
|
656
1106
|
if (
|
|
657
|
-
|
|
1107
|
+
role instanceof Replicator &&
|
|
658
1108
|
this._canReplicate &&
|
|
659
|
-
!(await this._canReplicate(publicKey))
|
|
1109
|
+
!(await this._canReplicate(publicKey, role))
|
|
660
1110
|
) {
|
|
661
|
-
return false;
|
|
1111
|
+
return { changed: false };
|
|
662
1112
|
}
|
|
663
1113
|
|
|
664
1114
|
const sortedPeer = this._sortedPeersCache;
|
|
@@ -666,81 +1116,130 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
666
1116
|
if (this.closed === false) {
|
|
667
1117
|
throw new Error("Unexpected, sortedPeersCache is undefined");
|
|
668
1118
|
}
|
|
669
|
-
return false;
|
|
1119
|
+
return { changed: false };
|
|
670
1120
|
}
|
|
671
|
-
|
|
672
|
-
if (
|
|
1121
|
+
|
|
1122
|
+
if (role instanceof Replicator && role.factor > 0) {
|
|
673
1123
|
// TODO use Set + list for fast lookup
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1124
|
+
// check also that peer is online
|
|
1125
|
+
|
|
1126
|
+
const isOnline =
|
|
1127
|
+
this.node.identity.publicKey.equals(publicKey) ||
|
|
1128
|
+
(await this.waitFor(publicKey, { signal: this._closeController.signal })
|
|
1129
|
+
.then(() => true)
|
|
1130
|
+
.catch(() => false));
|
|
1131
|
+
|
|
1132
|
+
if (isOnline) {
|
|
1133
|
+
// insert or if already there do nothing
|
|
1134
|
+
const code = hashToUniformNumber(publicKey.bytes);
|
|
1135
|
+
const rect: ReplicatorRect = {
|
|
1136
|
+
publicKey,
|
|
1137
|
+
offset: code,
|
|
1138
|
+
role
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
let currentNode = sortedPeer.head;
|
|
1142
|
+
if (!currentNode) {
|
|
1143
|
+
sortedPeer.push(rect);
|
|
1144
|
+
this._totalParticipation += rect.role.factor;
|
|
1145
|
+
return { changed: true };
|
|
1146
|
+
} else {
|
|
1147
|
+
while (currentNode) {
|
|
1148
|
+
if (currentNode.value.publicKey.equals(publicKey)) {
|
|
1149
|
+
// update the value
|
|
1150
|
+
// rect.timestamp = currentNode.value.timestamp;
|
|
1151
|
+
const prev = currentNode.value;
|
|
1152
|
+
currentNode.value = rect;
|
|
1153
|
+
this._totalParticipation += rect.role.factor;
|
|
1154
|
+
this._totalParticipation -= prev.role.factor;
|
|
1155
|
+
// TODO change detection and only do change stuff if diff?
|
|
1156
|
+
return { prev: prev.role, changed: true };
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (code > currentNode.value.offset) {
|
|
1160
|
+
const next = currentNode?.next;
|
|
1161
|
+
if (next) {
|
|
1162
|
+
currentNode = next;
|
|
1163
|
+
continue;
|
|
1164
|
+
} else {
|
|
1165
|
+
break;
|
|
1166
|
+
}
|
|
1167
|
+
} else {
|
|
1168
|
+
currentNode = currentNode.prev;
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const prev = currentNode;
|
|
1174
|
+
if (!prev?.next?.value.publicKey.equals(publicKey)) {
|
|
1175
|
+
this._totalParticipation += rect.role.factor;
|
|
1176
|
+
_insertAfter(sortedPeer, prev || undefined, rect);
|
|
1177
|
+
} else {
|
|
1178
|
+
throw new Error("Unexpected");
|
|
1179
|
+
}
|
|
1180
|
+
return { changed: true };
|
|
1181
|
+
}
|
|
678
1182
|
} else {
|
|
679
|
-
return false;
|
|
1183
|
+
return { changed: false };
|
|
680
1184
|
}
|
|
681
1185
|
} else {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1186
|
+
let currentNode = sortedPeer.head;
|
|
1187
|
+
while (currentNode) {
|
|
1188
|
+
if (currentNode.value.publicKey.equals(publicKey)) {
|
|
1189
|
+
sortedPeer.removeNode(currentNode);
|
|
1190
|
+
this._totalParticipation -= currentNode.value.role.factor;
|
|
1191
|
+
return { prev: currentNode.value.role, changed: true };
|
|
1192
|
+
}
|
|
1193
|
+
currentNode = currentNode.next;
|
|
688
1194
|
}
|
|
1195
|
+
return { changed: false };
|
|
689
1196
|
}
|
|
690
1197
|
}
|
|
691
1198
|
|
|
692
1199
|
async handleSubscriptionChange(
|
|
693
1200
|
publicKey: PublicSignKey,
|
|
694
|
-
changes:
|
|
1201
|
+
changes: string[],
|
|
695
1202
|
subscribed: boolean
|
|
696
1203
|
) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
continue;
|
|
710
|
-
} else {
|
|
711
|
-
this._lastSubscriptionMessageId += 1;
|
|
712
|
-
prev.push(
|
|
713
|
-
await this.modifySortedSubscriptionCache(subscribed, publicKey)
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// TODO don't do this i fnot is replicator?
|
|
719
|
-
for (const [i, subscription] of changes.entries()) {
|
|
720
|
-
if (this.log.idString !== subscription.topic) {
|
|
721
|
-
continue;
|
|
1204
|
+
if (subscribed) {
|
|
1205
|
+
if (this.role instanceof Replicator) {
|
|
1206
|
+
for (const subscription of changes) {
|
|
1207
|
+
if (this.log.idString !== subscription) {
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
this.rpc
|
|
1211
|
+
.send(new ResponseRoleMessage(this.role), {
|
|
1212
|
+
mode: new AcknowledgeDelivery({ redundancy: 1, to: [publicKey] })
|
|
1213
|
+
})
|
|
1214
|
+
.catch((e) => logger.error(e.toString()));
|
|
1215
|
+
}
|
|
722
1216
|
}
|
|
723
|
-
if (subscription.data) {
|
|
724
|
-
try {
|
|
725
|
-
const type = deserialize(subscription.data, Role);
|
|
726
1217
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
logger.warn(
|
|
733
|
-
"Recieved subscription with invalid data on topic: " +
|
|
734
|
-
subscription.topic +
|
|
735
|
-
". Error: " +
|
|
736
|
-
error?.message
|
|
737
|
-
);
|
|
1218
|
+
//if(evt.detail.subscriptions.map((x) => x.topic).includes())
|
|
1219
|
+
} else {
|
|
1220
|
+
for (const topic of changes) {
|
|
1221
|
+
if (this.log.idString !== topic) {
|
|
1222
|
+
continue;
|
|
738
1223
|
}
|
|
1224
|
+
|
|
1225
|
+
await this.modifyReplicators(new Observer(), publicKey);
|
|
739
1226
|
}
|
|
740
1227
|
}
|
|
741
1228
|
}
|
|
742
1229
|
|
|
743
|
-
|
|
1230
|
+
async prune(
|
|
1231
|
+
entries: Entry<any>[],
|
|
1232
|
+
options?: { timeout?: number; unchecked?: boolean }
|
|
1233
|
+
): Promise<any> {
|
|
1234
|
+
if (options?.unchecked) {
|
|
1235
|
+
return Promise.all(
|
|
1236
|
+
entries.map((x) =>
|
|
1237
|
+
this.log.remove(x, {
|
|
1238
|
+
recursively: true
|
|
1239
|
+
})
|
|
1240
|
+
)
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
744
1243
|
// ask network if they have they entry,
|
|
745
1244
|
// so I can delete it
|
|
746
1245
|
|
|
@@ -754,14 +1253,17 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
754
1253
|
const filteredEntries: Entry<any>[] = [];
|
|
755
1254
|
for (const entry of entries) {
|
|
756
1255
|
const pendingPrev = this._pendingDeletes.get(entry.hash);
|
|
757
|
-
|
|
1256
|
+
if (pendingPrev) {
|
|
1257
|
+
promises.push(pendingPrev.promise.promise);
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
758
1260
|
filteredEntries.push(entry);
|
|
759
1261
|
const existCounter = new Set<string>();
|
|
760
1262
|
const minReplicas = decodeReplicas(entry);
|
|
761
1263
|
const deferredPromise: DeferredPromise<void> = pDefer();
|
|
762
1264
|
|
|
763
1265
|
const clear = () => {
|
|
764
|
-
pendingPrev?.clear();
|
|
1266
|
+
//pendingPrev?.clear();
|
|
765
1267
|
const pending = this._pendingDeletes.get(entry.hash);
|
|
766
1268
|
if (pending?.promise == deferredPromise) {
|
|
767
1269
|
this._pendingDeletes.delete(entry.hash);
|
|
@@ -780,7 +1282,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
780
1282
|
|
|
781
1283
|
const timeout = setTimeout(
|
|
782
1284
|
() => {
|
|
783
|
-
reject(new Error("Timeout"));
|
|
1285
|
+
reject(new Error("Timeout for checked pruning"));
|
|
784
1286
|
},
|
|
785
1287
|
options?.timeout ?? 10 * 1000
|
|
786
1288
|
);
|
|
@@ -792,10 +1294,25 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
792
1294
|
},
|
|
793
1295
|
callback: async (publicKeyHash: string) => {
|
|
794
1296
|
const minReplicasValue = minReplicas.getValue(this);
|
|
795
|
-
const
|
|
796
|
-
|
|
1297
|
+
const minMinReplicasValue = this.replicas.max
|
|
1298
|
+
? Math.min(minReplicasValue, this.replicas.max.getValue(this))
|
|
1299
|
+
: minReplicasValue;
|
|
1300
|
+
|
|
1301
|
+
const leaders = await this.findLeaders(
|
|
1302
|
+
entry.gid,
|
|
1303
|
+
minMinReplicasValue
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
if (
|
|
1307
|
+
leaders.find((x) => x === this.node.identity.publicKey.hashcode())
|
|
1308
|
+
) {
|
|
1309
|
+
reject(new Error("Failed to delete, is leader"));
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if (leaders.find((x) => x === publicKeyHash)) {
|
|
797
1314
|
existCounter.add(publicKeyHash);
|
|
798
|
-
if (
|
|
1315
|
+
if (minMinReplicasValue <= existCounter.size) {
|
|
799
1316
|
this.log
|
|
800
1317
|
.remove(entry, {
|
|
801
1318
|
recursively: true
|
|
@@ -812,28 +1329,52 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
812
1329
|
});
|
|
813
1330
|
promises.push(deferredPromise.promise);
|
|
814
1331
|
}
|
|
815
|
-
if (filteredEntries.length
|
|
816
|
-
|
|
817
|
-
new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) })
|
|
818
|
-
);
|
|
1332
|
+
if (filteredEntries.length == 0) {
|
|
1333
|
+
return;
|
|
819
1334
|
}
|
|
820
1335
|
|
|
821
|
-
|
|
1336
|
+
this.rpc.send(
|
|
1337
|
+
new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) })
|
|
1338
|
+
);
|
|
1339
|
+
|
|
1340
|
+
const onNewPeer = async (e: CustomEvent<UpdateRoleEvent>) => {
|
|
1341
|
+
if (e.detail.role instanceof Replicator) {
|
|
1342
|
+
await this.rpc.send(
|
|
1343
|
+
new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) }),
|
|
1344
|
+
{
|
|
1345
|
+
to: [e.detail.publicKey.hashcode()]
|
|
1346
|
+
}
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
// check joining peers
|
|
1351
|
+
this.events.addEventListener("role", onNewPeer);
|
|
1352
|
+
return Promise.all(promises).finally(() =>
|
|
1353
|
+
this.events.removeEventListener("role", onNewPeer)
|
|
1354
|
+
);
|
|
822
1355
|
}
|
|
823
1356
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1357
|
+
async distribute() {
|
|
1358
|
+
/**
|
|
1359
|
+
* TODO use information of new joined/leaving peer to create a subset of heads
|
|
1360
|
+
* that we potentially need to share with other peers
|
|
1361
|
+
*/
|
|
1362
|
+
|
|
1363
|
+
if (this.closed) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
830
1367
|
const changed = false;
|
|
1368
|
+
await this.log.trim();
|
|
831
1369
|
const heads = await this.log.getHeads();
|
|
832
1370
|
const groupedByGid = await groupByGid(heads);
|
|
833
|
-
|
|
1371
|
+
const toDeliver: Map<string, Entry<any>[]> = new Map();
|
|
1372
|
+
const allEntriesToDelete: Entry<any>[] = [];
|
|
1373
|
+
|
|
834
1374
|
for (const [gid, entries] of groupedByGid) {
|
|
835
|
-
|
|
836
|
-
|
|
1375
|
+
if (this.closed) {
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
837
1378
|
|
|
838
1379
|
if (entries.length === 0) {
|
|
839
1380
|
continue; // TODO maybe close store?
|
|
@@ -844,121 +1385,91 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
844
1385
|
gid,
|
|
845
1386
|
maxReplicas(this, entries) // pick max replication policy of all entries, so all information is treated equally important as the most important
|
|
846
1387
|
);
|
|
1388
|
+
const currentPeersSet = new Set(currentPeers);
|
|
1389
|
+
this._gidPeersHistory.set(gid, currentPeersSet);
|
|
847
1390
|
|
|
848
1391
|
for (const currentPeer of currentPeers) {
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1392
|
+
if (currentPeer == this.node.identity.publicKey.hashcode()) {
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (!oldPeersSet?.has(currentPeer)) {
|
|
854
1397
|
// second condition means that if the new peer is us, we should not do anything, since we are expecting to receive heads, not send
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
} on rebalance`
|
|
864
|
-
);
|
|
865
|
-
for (const entry of entries) {
|
|
866
|
-
toSend.set(entry.hash, entry);
|
|
867
|
-
}
|
|
868
|
-
} catch (error) {
|
|
869
|
-
if (error instanceof TimeoutError) {
|
|
870
|
-
logger.error(
|
|
871
|
-
"Missing channel when reorg to peer: " + currentPeer.toString()
|
|
872
|
-
);
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
throw error;
|
|
1398
|
+
let arr = toDeliver.get(currentPeer);
|
|
1399
|
+
if (!arr) {
|
|
1400
|
+
arr = [];
|
|
1401
|
+
toDeliver.set(currentPeer, arr);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
for (const entry of entries) {
|
|
1405
|
+
arr.push(entry);
|
|
876
1406
|
}
|
|
877
1407
|
}
|
|
878
1408
|
}
|
|
879
1409
|
|
|
880
|
-
// We don't need this clause anymore because we got the trim option!
|
|
881
1410
|
if (
|
|
882
1411
|
!currentPeers.find((x) => x === this.node.identity.publicKey.hashcode())
|
|
883
1412
|
) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1413
|
+
if (currentPeers.length > 0) {
|
|
1414
|
+
// If we are observer, never prune locally created entries, since we dont really know who can store them
|
|
1415
|
+
// if we are replicator, we will always persist entries that we need to so filtering on createdLocally will not make a difference
|
|
1416
|
+
const entriesToDelete =
|
|
1417
|
+
this._role instanceof Observer
|
|
1418
|
+
? entries.filter((e) => !e.createdLocally)
|
|
1419
|
+
: entries;
|
|
1420
|
+
entriesToDelete.map((x) => this._gidPeersHistory.delete(x.meta.gid));
|
|
1421
|
+
allEntriesToDelete.push(...entriesToDelete);
|
|
891
1422
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1423
|
+
} else {
|
|
1424
|
+
for (const entry of entries) {
|
|
1425
|
+
this._pendingDeletes
|
|
1426
|
+
.get(entry.hash)
|
|
1427
|
+
?.promise.reject(
|
|
1428
|
+
new Error(
|
|
1429
|
+
"Failed to delete, is leader: " +
|
|
1430
|
+
this.role.constructor.name +
|
|
1431
|
+
". " +
|
|
1432
|
+
this.node.identity.publicKey.hashcode()
|
|
1433
|
+
)
|
|
1434
|
+
);
|
|
899
1435
|
}
|
|
900
|
-
|
|
901
|
-
// TODO if length === 0 maybe close store?
|
|
902
1436
|
}
|
|
903
|
-
|
|
1437
|
+
}
|
|
904
1438
|
|
|
905
|
-
|
|
906
|
-
continue;
|
|
907
|
-
}
|
|
1439
|
+
for (const [target, entries] of toDeliver) {
|
|
908
1440
|
const message = await createExchangeHeadsMessage(
|
|
909
1441
|
this.log,
|
|
910
|
-
|
|
1442
|
+
entries, // TODO send to peers directly
|
|
911
1443
|
this._gidParentCache
|
|
912
1444
|
);
|
|
913
|
-
|
|
914
1445
|
// TODO perhaps send less messages to more receivers for performance reasons?
|
|
915
1446
|
await this.rpc.send(message, {
|
|
916
|
-
to:
|
|
917
|
-
strict: true
|
|
1447
|
+
to: [target]
|
|
918
1448
|
});
|
|
919
1449
|
}
|
|
920
|
-
if (storeChanged) {
|
|
921
|
-
await this.log.trim(); // because for entries createdLocally,we can have trim options that still allow us to delete them
|
|
922
|
-
}
|
|
923
|
-
return storeChanged || changed;
|
|
924
|
-
}
|
|
925
1450
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
getDiscoveryGroups() {
|
|
931
|
-
// TODO Optimize this so we don't have to recreate the array all the time!
|
|
932
|
-
const minReplicas = this.replicas.min.getValue(this);
|
|
933
|
-
const replicators = this.getReplicatorsSorted();
|
|
934
|
-
if (!replicators) {
|
|
935
|
-
return []; // No subscribers and we are not replicating
|
|
936
|
-
}
|
|
937
|
-
const numberOfGroups = Math.min(
|
|
938
|
-
Math.ceil(replicators!.length / minReplicas)
|
|
939
|
-
);
|
|
940
|
-
const groups = new Array<{ hash: string; timestamp: number }[]>(
|
|
941
|
-
numberOfGroups
|
|
942
|
-
);
|
|
943
|
-
for (let i = 0; i < groups.length; i++) {
|
|
944
|
-
groups[i] = [];
|
|
945
|
-
}
|
|
946
|
-
for (let i = 0; i < replicators!.length; i++) {
|
|
947
|
-
groups[i % numberOfGroups].push(replicators![i]);
|
|
1451
|
+
if (allEntriesToDelete.length > 0) {
|
|
1452
|
+
this.prune(allEntriesToDelete).catch((e) => {
|
|
1453
|
+
logger.error(e.toString());
|
|
1454
|
+
});
|
|
948
1455
|
}
|
|
949
1456
|
|
|
950
|
-
return
|
|
951
|
-
}
|
|
952
|
-
async replicator(entry: Entry<any>) {
|
|
953
|
-
return this.isLeader(entry.gid, decodeReplicas(entry).getValue(this));
|
|
1457
|
+
return changed;
|
|
954
1458
|
}
|
|
955
1459
|
|
|
956
1460
|
async _onUnsubscription(evt: CustomEvent<UnsubcriptionEvent>) {
|
|
957
1461
|
logger.debug(
|
|
958
1462
|
`Peer disconnected '${evt.detail.from.hashcode()}' from '${JSON.stringify(
|
|
959
|
-
evt.detail.unsubscriptions.map((x) => x
|
|
1463
|
+
evt.detail.unsubscriptions.map((x) => x)
|
|
960
1464
|
)}'`
|
|
961
1465
|
);
|
|
1466
|
+
this.latestRoleMessages.delete(evt.detail.from.hashcode());
|
|
1467
|
+
|
|
1468
|
+
this.events.dispatchEvent(
|
|
1469
|
+
new CustomEvent<UpdateRoleEvent>("role", {
|
|
1470
|
+
detail: { publicKey: evt.detail.from, role: new Observer() }
|
|
1471
|
+
})
|
|
1472
|
+
);
|
|
962
1473
|
|
|
963
1474
|
return this.handleSubscriptionChange(
|
|
964
1475
|
evt.detail.from,
|
|
@@ -970,13 +1481,134 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
970
1481
|
async _onSubscription(evt: CustomEvent<SubscriptionEvent>) {
|
|
971
1482
|
logger.debug(
|
|
972
1483
|
`New peer '${evt.detail.from.hashcode()}' connected to '${JSON.stringify(
|
|
973
|
-
evt.detail.subscriptions.map((x) => x
|
|
1484
|
+
evt.detail.subscriptions.map((x) => x)
|
|
974
1485
|
)}'`
|
|
975
1486
|
);
|
|
1487
|
+
this.remoteBlocks.onReachable(evt.detail.from);
|
|
1488
|
+
|
|
976
1489
|
return this.handleSubscriptionChange(
|
|
977
1490
|
evt.detail.from,
|
|
978
1491
|
evt.detail.subscriptions,
|
|
979
1492
|
true
|
|
980
1493
|
);
|
|
981
1494
|
}
|
|
1495
|
+
replicationController: PIDReplicationController;
|
|
1496
|
+
|
|
1497
|
+
history: { usedMemory: number; factor: number }[];
|
|
1498
|
+
async addToHistory(usedMemory: number, factor: number) {
|
|
1499
|
+
(this.history || (this.history = [])).push({ usedMemory, factor });
|
|
1500
|
+
|
|
1501
|
+
// Keep only the last N entries in the history array (you can adjust N based on your needs)
|
|
1502
|
+
const maxHistoryLength = 10;
|
|
1503
|
+
if (this.history.length > maxHistoryLength) {
|
|
1504
|
+
this.history.shift();
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
async calculateTrend() {
|
|
1509
|
+
// Calculate the average change in factor per unit change in memory usage
|
|
1510
|
+
const factorChanges = this.history.map((entry, index) => {
|
|
1511
|
+
if (index > 0) {
|
|
1512
|
+
const memoryChange =
|
|
1513
|
+
entry.usedMemory - this.history[index - 1].usedMemory;
|
|
1514
|
+
if (memoryChange !== 0) {
|
|
1515
|
+
const factorChange = entry.factor - this.history[index - 1].factor;
|
|
1516
|
+
return factorChange / memoryChange;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return 0;
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
// Return the average factor change per unit memory change
|
|
1523
|
+
return (
|
|
1524
|
+
factorChanges.reduce((sum, change) => sum + change, 0) /
|
|
1525
|
+
factorChanges.length
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
async rebalanceParticipation(onRoleChange = true) {
|
|
1530
|
+
// update more participation rate to converge to the average expected rate or bounded by
|
|
1531
|
+
// resources such as memory and or cpu
|
|
1532
|
+
|
|
1533
|
+
if (this.closed) {
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// The role is fixed (no changes depending on memory usage or peer count etc)
|
|
1538
|
+
if (this._roleOptions instanceof Role) {
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// TODO second condition: what if the current role is Observer?
|
|
1543
|
+
if (
|
|
1544
|
+
this._roleOptions.type == "replicator" &&
|
|
1545
|
+
this._role instanceof Replicator
|
|
1546
|
+
) {
|
|
1547
|
+
const peers = this.getReplicatorsSorted();
|
|
1548
|
+
const usedMemory = await this.getMemoryUsage();
|
|
1549
|
+
|
|
1550
|
+
const newFactor =
|
|
1551
|
+
await this.replicationController.adjustReplicationFactor(
|
|
1552
|
+
usedMemory,
|
|
1553
|
+
this._role.factor,
|
|
1554
|
+
this._totalParticipation,
|
|
1555
|
+
peers?.length || 1
|
|
1556
|
+
);
|
|
1557
|
+
|
|
1558
|
+
const newRole = new Replicator({
|
|
1559
|
+
factor: newFactor,
|
|
1560
|
+
timestamp: this._role.timestamp
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
const relativeDifference =
|
|
1564
|
+
Math.abs(this._role.factor - newRole.factor) / this._role.factor;
|
|
1565
|
+
|
|
1566
|
+
if (relativeDifference > 0.0001) {
|
|
1567
|
+
const canReplicate =
|
|
1568
|
+
!this._canReplicate ||
|
|
1569
|
+
(await this._canReplicate(this.node.identity.publicKey, newRole));
|
|
1570
|
+
if (!canReplicate) {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
await this._updateRole(newRole, onRoleChange);
|
|
1575
|
+
return true;
|
|
1576
|
+
}
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function _insertAfter(
|
|
1584
|
+
self: yallist<any>,
|
|
1585
|
+
node: yallist.Node<ReplicatorRect> | undefined,
|
|
1586
|
+
value: ReplicatorRect
|
|
1587
|
+
) {
|
|
1588
|
+
const inserted = !node
|
|
1589
|
+
? new yallist.Node(
|
|
1590
|
+
value,
|
|
1591
|
+
null as any,
|
|
1592
|
+
self.head as yallist.Node<ReplicatorRect> | undefined,
|
|
1593
|
+
self
|
|
1594
|
+
)
|
|
1595
|
+
: new yallist.Node(
|
|
1596
|
+
value,
|
|
1597
|
+
node,
|
|
1598
|
+
node.next as yallist.Node<ReplicatorRect> | undefined,
|
|
1599
|
+
self
|
|
1600
|
+
);
|
|
1601
|
+
|
|
1602
|
+
// is tail
|
|
1603
|
+
if (inserted.next === null) {
|
|
1604
|
+
self.tail = inserted;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// is head
|
|
1608
|
+
if (inserted.prev === null) {
|
|
1609
|
+
self.head = inserted;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
self.length++;
|
|
1613
|
+
return inserted;
|
|
982
1614
|
}
|