@peerbit/shared-log 3.1.10 → 4.0.1
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 +101 -42
- package/lib/esm/index.js +753 -249
- package/lib/esm/index.js.map +1 -1
- 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 +1097 -342
- package/src/replication.ts +49 -4
- package/src/role.ts +67 -6
package/src/index.ts
CHANGED
|
@@ -7,19 +7,16 @@ import {
|
|
|
7
7
|
LogEvents,
|
|
8
8
|
LogProperties
|
|
9
9
|
} from "@peerbit/log";
|
|
10
|
-
import { Program } from "@peerbit/program";
|
|
10
|
+
import { Program, ProgramEvents } from "@peerbit/program";
|
|
11
11
|
import {
|
|
12
|
-
BinaryReader,
|
|
13
12
|
BinaryWriter,
|
|
14
13
|
BorshError,
|
|
15
|
-
deserialize,
|
|
16
14
|
field,
|
|
17
15
|
serialize,
|
|
18
16
|
variant
|
|
19
17
|
} from "@dao-xyz/borsh";
|
|
20
18
|
import {
|
|
21
19
|
AccessError,
|
|
22
|
-
getPublicKeyFromPeerId,
|
|
23
20
|
PublicSignKey,
|
|
24
21
|
sha256,
|
|
25
22
|
sha256Base64Sync
|
|
@@ -36,24 +33,34 @@ import {
|
|
|
36
33
|
SubscriptionEvent,
|
|
37
34
|
UnsubcriptionEvent
|
|
38
35
|
} from "@peerbit/pubsub-interface";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import { REPLICATOR_TYPE_VARIANT, Observer, Replicator, Role } from "./role.js";
|
|
36
|
+
import { AbortError, delay, TimeoutError, waitFor } from "@peerbit/time";
|
|
37
|
+
import { Observer, Replicator, Role } from "./role.js";
|
|
42
38
|
import {
|
|
43
39
|
AbsoluteReplicas,
|
|
44
|
-
MinReplicas,
|
|
45
40
|
ReplicationError,
|
|
41
|
+
ReplicationLimits,
|
|
42
|
+
ReplicatorRect,
|
|
43
|
+
RequestRoleMessage,
|
|
44
|
+
ResponseRoleMessage,
|
|
46
45
|
decodeReplicas,
|
|
47
46
|
encodeReplicas,
|
|
47
|
+
hashToUniformNumber,
|
|
48
48
|
maxReplicas
|
|
49
49
|
} from "./replication.js";
|
|
50
50
|
import pDefer, { DeferredPromise } from "p-defer";
|
|
51
51
|
import { Cache } from "@peerbit/cache";
|
|
52
|
+
import { CustomEvent } from "@libp2p/interface";
|
|
53
|
+
import yallist from "yallist";
|
|
54
|
+
import { AcknowledgeDelivery, SilentDelivery } from "@peerbit/stream-interface";
|
|
55
|
+
import { AnyBlockStore, RemoteBlocks } from "@peerbit/blocks";
|
|
56
|
+
import { BlocksMessage } from "./blocks.js";
|
|
57
|
+
import debounce from "p-debounce";
|
|
52
58
|
|
|
53
59
|
export * from "./replication.js";
|
|
60
|
+
|
|
54
61
|
export { Observer, Replicator, Role };
|
|
55
62
|
|
|
56
|
-
export const logger = loggerFn({ module: "
|
|
63
|
+
export const logger = loggerFn({ module: "shared-log" });
|
|
57
64
|
|
|
58
65
|
const groupByGid = async <T extends Entry<any> | EntryWithRefs<any>>(
|
|
59
66
|
entries: T[]
|
|
@@ -73,30 +80,80 @@ const groupByGid = async <T extends Entry<any> | EntryWithRefs<any>>(
|
|
|
73
80
|
return groupByGid;
|
|
74
81
|
};
|
|
75
82
|
|
|
76
|
-
export type SyncFilter = (entries: Entry<any>) => Promise<boolean> | boolean;
|
|
77
|
-
|
|
78
|
-
type ReplicationLimits = { min: MinReplicas; max?: MinReplicas };
|
|
79
83
|
export type ReplicationLimitsOptions =
|
|
80
84
|
| Partial<ReplicationLimits>
|
|
81
85
|
| { min?: number; max?: number };
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
type StringRoleOptions = "observer" | "replicator";
|
|
88
|
+
|
|
89
|
+
export type ReplicationErrorFunction = (objectives: {
|
|
90
|
+
coverage: number;
|
|
91
|
+
balance: number;
|
|
92
|
+
memory: number;
|
|
93
|
+
}) => number;
|
|
94
|
+
|
|
95
|
+
type AdaptiveReplicatorOptions = {
|
|
96
|
+
type: "replicator";
|
|
97
|
+
limits?: { memory: number };
|
|
98
|
+
error?: ReplicationErrorFunction;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type FixedReplicatorOptions = {
|
|
102
|
+
type: "replicator";
|
|
103
|
+
factor: number;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type ObserverType = {
|
|
107
|
+
type: "observer";
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type RoleOptions =
|
|
111
|
+
| StringRoleOptions
|
|
112
|
+
| ObserverType
|
|
113
|
+
| FixedReplicatorOptions
|
|
114
|
+
| AdaptiveReplicatorOptions;
|
|
115
|
+
|
|
116
|
+
const isAdaptiveReplicatorOption = (
|
|
117
|
+
options: FixedReplicatorOptions | AdaptiveReplicatorOptions
|
|
118
|
+
): options is AdaptiveReplicatorOptions => {
|
|
119
|
+
if (
|
|
120
|
+
(options as AdaptiveReplicatorOptions).limits ||
|
|
121
|
+
(options as AdaptiveReplicatorOptions).error ||
|
|
122
|
+
(options as FixedReplicatorOptions).factor == null
|
|
123
|
+
) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type SharedLogOptions = {
|
|
130
|
+
role?: RoleOptions;
|
|
84
131
|
replicas?: ReplicationLimitsOptions;
|
|
85
|
-
sync?: SyncFilter;
|
|
86
|
-
role?: Role;
|
|
87
132
|
respondToIHaveTimeout?: number;
|
|
88
133
|
canReplicate?: (publicKey: PublicSignKey) => Promise<boolean> | boolean;
|
|
89
|
-
}
|
|
134
|
+
};
|
|
90
135
|
|
|
91
136
|
export const DEFAULT_MIN_REPLICAS = 2;
|
|
137
|
+
export const WAIT_FOR_REPLICATOR_TIMEOUT = 9000;
|
|
138
|
+
export const WAIT_FOR_ROLE_MATURITY = 5000;
|
|
139
|
+
const REBALANCE_DEBOUNCE_INTERAVAL = 50;
|
|
92
140
|
|
|
93
141
|
export type Args<T> = LogProperties<T> & LogEvents<T> & SharedLogOptions;
|
|
142
|
+
|
|
94
143
|
export type SharedAppendOptions<T> = AppendOptions<T> & {
|
|
95
144
|
replicas?: AbsoluteReplicas | number;
|
|
96
145
|
};
|
|
97
146
|
|
|
147
|
+
type UpdateRoleEvent = { publicKey: PublicSignKey; role: Role };
|
|
148
|
+
export interface SharedLogEvents extends ProgramEvents {
|
|
149
|
+
role: CustomEvent<UpdateRoleEvent>;
|
|
150
|
+
}
|
|
151
|
+
|
|
98
152
|
@variant("shared_log")
|
|
99
|
-
export class SharedLog<T = Uint8Array> extends Program<
|
|
153
|
+
export class SharedLog<T = Uint8Array> extends Program<
|
|
154
|
+
Args<T>,
|
|
155
|
+
SharedLogEvents
|
|
156
|
+
> {
|
|
100
157
|
@field({ type: Log })
|
|
101
158
|
log: Log<T>;
|
|
102
159
|
|
|
@@ -104,22 +161,22 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
104
161
|
rpc: RPC<TransportMessage, TransportMessage>;
|
|
105
162
|
|
|
106
163
|
// options
|
|
107
|
-
private _sync?: SyncFilter;
|
|
108
164
|
private _role: Observer | Replicator;
|
|
165
|
+
private _roleOptions: AdaptiveReplicatorOptions | Observer | Replicator;
|
|
109
166
|
|
|
110
|
-
private _sortedPeersCache:
|
|
111
|
-
private _lastSubscriptionMessageId: number;
|
|
167
|
+
private _sortedPeersCache: yallist<ReplicatorRect> | undefined;
|
|
112
168
|
private _gidPeersHistory: Map<string, Set<string>>;
|
|
113
169
|
|
|
114
170
|
private _onSubscriptionFn: (arg: any) => any;
|
|
115
171
|
private _onUnsubscriptionFn: (arg: any) => any;
|
|
116
172
|
|
|
117
173
|
private _canReplicate?: (
|
|
118
|
-
publicKey: PublicSignKey
|
|
174
|
+
publicKey: PublicSignKey,
|
|
175
|
+
role: Replicator
|
|
119
176
|
) => Promise<boolean> | boolean;
|
|
120
177
|
|
|
121
178
|
private _logProperties?: LogProperties<T> & LogEvents<T>;
|
|
122
|
-
|
|
179
|
+
private _closeController: AbortController;
|
|
123
180
|
private _loadedOnce = false;
|
|
124
181
|
private _gidParentCache: Cache<Entry<any>[]>;
|
|
125
182
|
private _respondToIHaveTimeout;
|
|
@@ -134,9 +191,19 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
134
191
|
|
|
135
192
|
private _pendingIHave: Map<
|
|
136
193
|
string,
|
|
137
|
-
{ clear: () => void; callback: () => void }
|
|
194
|
+
{ clear: () => void; callback: (entry: Entry<T>) => void }
|
|
138
195
|
>;
|
|
139
196
|
|
|
197
|
+
private latestRoleMessages: Map<string, bigint>;
|
|
198
|
+
|
|
199
|
+
private remoteBlocks: RemoteBlocks;
|
|
200
|
+
|
|
201
|
+
private openTime: number;
|
|
202
|
+
|
|
203
|
+
private rebalanceParticipationDebounced:
|
|
204
|
+
| ReturnType<typeof debounce>
|
|
205
|
+
| undefined;
|
|
206
|
+
|
|
140
207
|
replicas: ReplicationLimits;
|
|
141
208
|
|
|
142
209
|
constructor(properties?: { id?: Uint8Array }) {
|
|
@@ -149,37 +216,89 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
149
216
|
return this._role;
|
|
150
217
|
}
|
|
151
218
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this._role = role;
|
|
155
|
-
await this.initializeWithRole();
|
|
156
|
-
await this.rpc.subscribe(serialize(this._role));
|
|
219
|
+
private setupRole(options?: RoleOptions) {
|
|
220
|
+
this.rebalanceParticipationDebounced = undefined;
|
|
157
221
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
222
|
+
const setupDebouncedRebalancing = (options?: AdaptiveReplicatorOptions) => {
|
|
223
|
+
this.replicationController = new ReplicationController({
|
|
224
|
+
targetMemoryLimit: options?.limits?.memory,
|
|
225
|
+
errorFunction: options?.error
|
|
226
|
+
});
|
|
162
227
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this._role instanceof Replicator ? true : false,
|
|
167
|
-
getPublicKeyFromPeerId(this.node.peerId)
|
|
228
|
+
this.rebalanceParticipationDebounced = debounce(
|
|
229
|
+
() => this.rebalanceParticipation(),
|
|
230
|
+
REBALANCE_DEBOUNCE_INTERAVAL // TODO make dynamic
|
|
168
231
|
);
|
|
232
|
+
};
|
|
169
233
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
234
|
+
if (options instanceof Observer || options instanceof Replicator) {
|
|
235
|
+
throw new Error("Unsupported role option type");
|
|
236
|
+
} else if (options === "observer") {
|
|
237
|
+
this._roleOptions = new Observer();
|
|
238
|
+
} else if (options === "replicator") {
|
|
239
|
+
setupDebouncedRebalancing();
|
|
240
|
+
this._roleOptions = { type: options };
|
|
241
|
+
} else if (options) {
|
|
242
|
+
if (options.type === "replicator") {
|
|
243
|
+
if (isAdaptiveReplicatorOption(options)) {
|
|
244
|
+
setupDebouncedRebalancing(options);
|
|
245
|
+
this._roleOptions = options;
|
|
246
|
+
} else {
|
|
247
|
+
this._roleOptions = new Replicator({ factor: options.factor });
|
|
248
|
+
}
|
|
179
249
|
} else {
|
|
180
|
-
|
|
250
|
+
this._roleOptions = new Observer();
|
|
181
251
|
}
|
|
252
|
+
} else {
|
|
253
|
+
// Default option
|
|
254
|
+
setupDebouncedRebalancing();
|
|
255
|
+
this._roleOptions = { type: "replicator" };
|
|
182
256
|
}
|
|
257
|
+
|
|
258
|
+
// setup the initial role
|
|
259
|
+
if (
|
|
260
|
+
this._roleOptions instanceof Replicator ||
|
|
261
|
+
this._roleOptions instanceof Observer
|
|
262
|
+
) {
|
|
263
|
+
this._role = this._roleOptions; // Fixed
|
|
264
|
+
} else {
|
|
265
|
+
this._role = new Replicator({
|
|
266
|
+
// initial role in a dynamic setup
|
|
267
|
+
factor: 1,
|
|
268
|
+
timestamp: BigInt(+new Date())
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return this._role;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async updateRole(role: RoleOptions, onRoleChange = true) {
|
|
276
|
+
return this._updateRole(this.setupRole(role), onRoleChange);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async _updateRole(
|
|
280
|
+
role: Observer | Replicator = this._role,
|
|
281
|
+
onRoleChange = true
|
|
282
|
+
) {
|
|
283
|
+
this._role = role;
|
|
284
|
+
const { changed } = await this._modifyReplicators(
|
|
285
|
+
this.role,
|
|
286
|
+
this.node.identity.publicKey
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (!this._loadedOnce) {
|
|
290
|
+
await this.log.load();
|
|
291
|
+
this._loadedOnce = true;
|
|
292
|
+
}
|
|
293
|
+
await this.rpc.subscribe();
|
|
294
|
+
|
|
295
|
+
await this.rpc.send(new ResponseRoleMessage(role));
|
|
296
|
+
|
|
297
|
+
if (onRoleChange && changed) {
|
|
298
|
+
this.onRoleChange(undefined, this._role, this.node.identity.publicKey);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return changed;
|
|
183
302
|
}
|
|
184
303
|
|
|
185
304
|
async append(
|
|
@@ -207,14 +326,27 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
207
326
|
}
|
|
208
327
|
|
|
209
328
|
const result = await this.log.append(data, appendOptions);
|
|
329
|
+
const leaders = await this.findLeaders(
|
|
330
|
+
result.entry.meta.gid,
|
|
331
|
+
decodeReplicas(result.entry).getValue(this)
|
|
332
|
+
);
|
|
333
|
+
const isLeader = leaders.includes(this.node.identity.publicKey.hashcode());
|
|
210
334
|
|
|
211
335
|
await this.rpc.send(
|
|
212
336
|
await createExchangeHeadsMessage(
|
|
213
337
|
this.log,
|
|
214
338
|
[result.entry],
|
|
215
339
|
this._gidParentCache
|
|
216
|
-
)
|
|
340
|
+
),
|
|
341
|
+
{
|
|
342
|
+
mode: isLeader
|
|
343
|
+
? new SilentDelivery({ redundancy: 1, to: leaders })
|
|
344
|
+
: new AcknowledgeDelivery({ redundancy: 1, to: leaders })
|
|
345
|
+
}
|
|
217
346
|
);
|
|
347
|
+
|
|
348
|
+
this.rebalanceParticipationDebounced?.();
|
|
349
|
+
|
|
218
350
|
return result;
|
|
219
351
|
}
|
|
220
352
|
|
|
@@ -231,21 +363,23 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
231
363
|
: options.replicas.max
|
|
232
364
|
: undefined
|
|
233
365
|
};
|
|
366
|
+
|
|
234
367
|
this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 10 * 1000; // TODO make into arg
|
|
235
368
|
this._pendingDeletes = new Map();
|
|
236
369
|
this._pendingIHave = new Map();
|
|
370
|
+
this.latestRoleMessages = new Map();
|
|
371
|
+
this.openTime = +new Date();
|
|
237
372
|
|
|
238
373
|
this._gidParentCache = new Cache({ max: 1000 });
|
|
374
|
+
this._closeController = new AbortController();
|
|
239
375
|
|
|
240
376
|
this._canReplicate = options?.canReplicate;
|
|
241
|
-
this._sync = options?.sync;
|
|
242
377
|
this._logProperties = options;
|
|
243
|
-
this._role = options?.role || new Replicator();
|
|
244
378
|
|
|
245
|
-
this.
|
|
246
|
-
this._onSubscriptionFn = this._onSubscription.bind(this);
|
|
379
|
+
this.setupRole(options?.role);
|
|
247
380
|
|
|
248
|
-
this.
|
|
381
|
+
this._onSubscriptionFn = this._onSubscription.bind(this);
|
|
382
|
+
this._sortedPeersCache = yallist.create();
|
|
249
383
|
this._gidPeersHistory = new Map();
|
|
250
384
|
|
|
251
385
|
await this.node.services.pubsub.addEventListener(
|
|
@@ -259,9 +393,26 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
259
393
|
this._onUnsubscriptionFn
|
|
260
394
|
);
|
|
261
395
|
|
|
262
|
-
|
|
263
|
-
|
|
396
|
+
const id = sha256Base64Sync(this.log.id);
|
|
397
|
+
const storage = await this.node.memory.sublevel(id);
|
|
398
|
+
const localBlocks = await new AnyBlockStore(
|
|
399
|
+
await storage.sublevel("blocks")
|
|
400
|
+
);
|
|
401
|
+
const cache = await storage.sublevel("cache");
|
|
402
|
+
|
|
403
|
+
this.remoteBlocks = new RemoteBlocks({
|
|
404
|
+
local: localBlocks,
|
|
405
|
+
publish: (message, options) =>
|
|
406
|
+
this.rpc.send(new BlocksMessage(message), {
|
|
407
|
+
to: options?.to
|
|
408
|
+
}),
|
|
409
|
+
waitFor: this.rpc.waitFor.bind(this.rpc)
|
|
410
|
+
});
|
|
264
411
|
|
|
412
|
+
await this.remoteBlocks.start();
|
|
413
|
+
|
|
414
|
+
await this.log.open(this.remoteBlocks, this.node.identity, {
|
|
415
|
+
keychain: this.node.services.keychain,
|
|
265
416
|
...this._logProperties,
|
|
266
417
|
onChange: (change) => {
|
|
267
418
|
if (this._pendingIHave.size > 0) {
|
|
@@ -269,7 +420,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
269
420
|
const ih = this._pendingIHave.get(added.hash);
|
|
270
421
|
if (ih) {
|
|
271
422
|
ih.clear();
|
|
272
|
-
ih.callback();
|
|
423
|
+
ih.callback(added);
|
|
273
424
|
}
|
|
274
425
|
}
|
|
275
426
|
}
|
|
@@ -304,42 +455,37 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
304
455
|
return this._logProperties?.canAppend?.(entry) ?? true;
|
|
305
456
|
},
|
|
306
457
|
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
|
-
}
|
|
458
|
+
...this._logProperties?.trim
|
|
316
459
|
},
|
|
317
|
-
cache:
|
|
318
|
-
this.node.memory &&
|
|
319
|
-
(await this.node.memory.sublevel(sha256Base64Sync(this.log.id)))
|
|
460
|
+
cache: cache
|
|
320
461
|
});
|
|
321
462
|
|
|
322
|
-
|
|
463
|
+
// Open for communcation
|
|
464
|
+
await this.rpc.open({
|
|
465
|
+
queryType: TransportMessage,
|
|
466
|
+
responseType: TransportMessage,
|
|
467
|
+
responseHandler: this._onMessage.bind(this),
|
|
468
|
+
topic: this.topic
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
await this._updateRole();
|
|
472
|
+
await this.rebalanceParticipation();
|
|
323
473
|
|
|
324
474
|
// Take into account existing subscription
|
|
325
475
|
(await this.node.services.pubsub.getSubscribers(this.topic))?.forEach(
|
|
326
476
|
(v, k) => {
|
|
327
|
-
this.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
);
|
|
477
|
+
if (v.equals(this.node.identity.publicKey)) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
this.handleSubscriptionChange(v, [this.topic], true);
|
|
332
481
|
}
|
|
333
482
|
);
|
|
483
|
+
}
|
|
334
484
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
responseHandler: this._onMessage.bind(this),
|
|
340
|
-
topic: this.topic,
|
|
341
|
-
subscriptionData: serialize(this.role)
|
|
342
|
-
});
|
|
485
|
+
async getMemoryUsage() {
|
|
486
|
+
return (
|
|
487
|
+
((await this.log.memory?.size()) || 0) + (await this.log.blocks.size())
|
|
488
|
+
);
|
|
343
489
|
}
|
|
344
490
|
|
|
345
491
|
get topic() {
|
|
@@ -347,6 +493,19 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
347
493
|
}
|
|
348
494
|
|
|
349
495
|
private async _close() {
|
|
496
|
+
this._closeController.abort();
|
|
497
|
+
|
|
498
|
+
this.node.services.pubsub.removeEventListener(
|
|
499
|
+
"subscribe",
|
|
500
|
+
this._onSubscriptionFn
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
|
|
504
|
+
this.node.services.pubsub.removeEventListener(
|
|
505
|
+
"unsubscribe",
|
|
506
|
+
this._onUnsubscriptionFn
|
|
507
|
+
);
|
|
508
|
+
|
|
350
509
|
for (const [k, v] of this._pendingDeletes) {
|
|
351
510
|
v.clear();
|
|
352
511
|
v.promise.resolve(); // TODO or reject?
|
|
@@ -355,24 +514,15 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
355
514
|
v.clear();
|
|
356
515
|
}
|
|
357
516
|
|
|
517
|
+
await this.remoteBlocks.stop();
|
|
358
518
|
this._gidParentCache.clear();
|
|
359
519
|
this._pendingDeletes = new Map();
|
|
360
520
|
this._pendingIHave = new Map();
|
|
521
|
+
this.latestRoleMessages.clear();
|
|
361
522
|
|
|
362
523
|
this._gidPeersHistory = new Map();
|
|
363
524
|
this._sortedPeersCache = undefined;
|
|
364
525
|
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
526
|
}
|
|
377
527
|
async close(from?: Program): Promise<boolean> {
|
|
378
528
|
const superClosed = await super.close(from);
|
|
@@ -389,8 +539,8 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
389
539
|
if (!superDropped) {
|
|
390
540
|
return superDropped;
|
|
391
541
|
}
|
|
392
|
-
await this._close();
|
|
393
542
|
await this.log.drop();
|
|
543
|
+
await this._close();
|
|
394
544
|
return true;
|
|
395
545
|
}
|
|
396
546
|
|
|
@@ -411,7 +561,6 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
411
561
|
*/
|
|
412
562
|
|
|
413
563
|
const { heads } = msg;
|
|
414
|
-
// replication topic === trustedNetwork address
|
|
415
564
|
|
|
416
565
|
logger.debug(
|
|
417
566
|
`${this.node.identity.publicKey.hashcode()}: Recieved heads: ${
|
|
@@ -431,26 +580,32 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
431
580
|
}
|
|
432
581
|
}
|
|
433
582
|
|
|
434
|
-
if (
|
|
435
|
-
|
|
583
|
+
if (filteredHeads.length === 0) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
436
586
|
|
|
437
|
-
|
|
438
|
-
|
|
587
|
+
const toMerge: EntryWithRefs<any>[] = [];
|
|
588
|
+
let toDelete: Entry<any>[] | undefined = undefined;
|
|
589
|
+
let maybeDelete: EntryWithRefs<any>[][] | undefined = undefined;
|
|
439
590
|
|
|
440
|
-
|
|
591
|
+
const groupedByGid = await groupByGid(filteredHeads);
|
|
592
|
+
const promises: Promise<void>[] = [];
|
|
441
593
|
|
|
442
|
-
|
|
594
|
+
for (const [gid, entries] of groupedByGid) {
|
|
595
|
+
const fn = async () => {
|
|
443
596
|
const headsWithGid = this.log.headsIndex.gids.get(gid);
|
|
597
|
+
|
|
444
598
|
const maxReplicasFromHead =
|
|
445
599
|
headsWithGid && headsWithGid.size > 0
|
|
446
600
|
? maxReplicas(this, [...headsWithGid.values()])
|
|
447
601
|
: this.replicas.min.getValue(this);
|
|
448
602
|
|
|
449
|
-
const maxReplicasFromNewEntries = maxReplicas(
|
|
450
|
-
|
|
451
|
-
|
|
603
|
+
const maxReplicasFromNewEntries = maxReplicas(
|
|
604
|
+
this,
|
|
605
|
+
entries.map((x) => x.entry)
|
|
606
|
+
);
|
|
452
607
|
|
|
453
|
-
const isLeader = await this.
|
|
608
|
+
const isLeader = await this.waitForIsLeader(
|
|
454
609
|
gid,
|
|
455
610
|
Math.max(maxReplicasFromHead, maxReplicasFromNewEntries)
|
|
456
611
|
);
|
|
@@ -481,50 +636,49 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
481
636
|
}. Because not leader`
|
|
482
637
|
);
|
|
483
638
|
}
|
|
484
|
-
}
|
|
639
|
+
};
|
|
640
|
+
promises.push(fn());
|
|
641
|
+
}
|
|
642
|
+
await Promise.all(promises);
|
|
485
643
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
Promise.all(this.pruneSafely(toDelete)).catch((e) => {
|
|
490
|
-
logger.error(e.toString());
|
|
491
|
-
});
|
|
492
|
-
}
|
|
644
|
+
if (this.closed) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
493
647
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
648
|
+
if (toMerge.length > 0) {
|
|
649
|
+
await this.log.join(toMerge);
|
|
650
|
+
toDelete &&
|
|
651
|
+
this.prune(toDelete).catch((e) => {
|
|
652
|
+
logger.error(e.toString());
|
|
653
|
+
});
|
|
654
|
+
this.rebalanceParticipationDebounced?.();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (maybeDelete) {
|
|
658
|
+
for (const entries of maybeDelete as EntryWithRefs<any>[][]) {
|
|
659
|
+
const headsWithGid = this.log.headsIndex.gids.get(
|
|
660
|
+
entries[0].entry.meta.gid
|
|
661
|
+
);
|
|
662
|
+
if (headsWithGid && headsWithGid.size > 0) {
|
|
663
|
+
const minReplicas = maxReplicas(this, headsWithGid.values());
|
|
664
|
+
|
|
665
|
+
const isLeader = await this.isLeader(
|
|
666
|
+
entries[0].entry.meta.gid,
|
|
667
|
+
minReplicas
|
|
498
668
|
);
|
|
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
|
-
}
|
|
669
|
+
|
|
670
|
+
if (!isLeader) {
|
|
671
|
+
this.prune(entries.map((x) => x.entry)).catch((e) => {
|
|
672
|
+
logger.error(e.toString());
|
|
673
|
+
});
|
|
515
674
|
}
|
|
516
675
|
}
|
|
517
676
|
}
|
|
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
677
|
}
|
|
525
678
|
}
|
|
526
679
|
} else if (msg instanceof RequestIHave) {
|
|
527
680
|
const hasAndIsLeader: string[] = [];
|
|
681
|
+
|
|
528
682
|
for (const hash of msg.hashes) {
|
|
529
683
|
const indexedEntry = this.log.entryIndex.getShallow(hash);
|
|
530
684
|
if (
|
|
@@ -542,12 +696,21 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
542
696
|
clearTimeout(timeout);
|
|
543
697
|
prevPendingIHave?.clear();
|
|
544
698
|
},
|
|
545
|
-
callback: () => {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
699
|
+
callback: async (entry) => {
|
|
700
|
+
if (
|
|
701
|
+
await this.isLeader(
|
|
702
|
+
entry.meta.gid,
|
|
703
|
+
decodeReplicas(entry).getValue(this)
|
|
704
|
+
)
|
|
705
|
+
) {
|
|
706
|
+
this.rpc.send(new ResponseIHave({ hashes: [entry.hash] }), {
|
|
707
|
+
to: [context.from!]
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
prevPendingIHave && prevPendingIHave.callback(entry);
|
|
712
|
+
|
|
713
|
+
this._pendingIHave.delete(entry.hash);
|
|
551
714
|
}
|
|
552
715
|
};
|
|
553
716
|
const timeout = setTimeout(() => {
|
|
@@ -560,17 +723,71 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
560
723
|
this._pendingIHave.set(hash, pendingIHave);
|
|
561
724
|
}
|
|
562
725
|
}
|
|
563
|
-
this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
|
|
726
|
+
await this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
|
|
564
727
|
to: [context.from!]
|
|
565
728
|
});
|
|
566
729
|
} else if (msg instanceof ResponseIHave) {
|
|
567
730
|
for (const hash of msg.hashes) {
|
|
568
731
|
this._pendingDeletes.get(hash)?.callback(context.from!.hashcode());
|
|
569
732
|
}
|
|
733
|
+
} else if (msg instanceof BlocksMessage) {
|
|
734
|
+
await this.remoteBlocks.onMessage(msg.message);
|
|
735
|
+
} else if (msg instanceof RequestRoleMessage) {
|
|
736
|
+
if (!context.from) {
|
|
737
|
+
throw new Error("Missing form in update role message");
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (context.from.equals(this.node.identity.publicKey)) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
await this.rpc.send(new ResponseRoleMessage({ role: this.role }), {
|
|
745
|
+
to: [context.from!]
|
|
746
|
+
});
|
|
747
|
+
} else if (msg instanceof ResponseRoleMessage) {
|
|
748
|
+
if (!context.from) {
|
|
749
|
+
throw new Error("Missing form in update role message");
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (context.from.equals(this.node.identity.publicKey)) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
this.waitFor(context.from, {
|
|
757
|
+
signal: this._closeController.signal,
|
|
758
|
+
timeout: WAIT_FOR_REPLICATOR_TIMEOUT
|
|
759
|
+
})
|
|
760
|
+
.then(async () => {
|
|
761
|
+
/* await delay(1000 * Math.random()) */
|
|
762
|
+
|
|
763
|
+
const prev = this.latestRoleMessages.get(context.from!.hashcode());
|
|
764
|
+
if (prev && prev > context.timestamp) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
this.latestRoleMessages.set(
|
|
768
|
+
context.from!.hashcode(),
|
|
769
|
+
context.timestamp
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
await this.modifyReplicators(msg.role, context.from!);
|
|
773
|
+
})
|
|
774
|
+
.catch((e) => {
|
|
775
|
+
if (e instanceof AbortError) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
logger.error(
|
|
780
|
+
"Failed to find peer who updated their role: " + e?.message
|
|
781
|
+
);
|
|
782
|
+
});
|
|
570
783
|
} else {
|
|
571
784
|
throw new Error("Unexpected message");
|
|
572
785
|
}
|
|
573
786
|
} catch (e: any) {
|
|
787
|
+
if (e instanceof AbortError) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
574
791
|
if (e instanceof BorshError) {
|
|
575
792
|
logger.trace(
|
|
576
793
|
`${this.node.identity.publicKey.hashcode()}: Failed to handle message on topic: ${JSON.stringify(
|
|
@@ -579,6 +796,7 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
579
796
|
);
|
|
580
797
|
return;
|
|
581
798
|
}
|
|
799
|
+
|
|
582
800
|
if (e instanceof AccessError) {
|
|
583
801
|
logger.trace(
|
|
584
802
|
`${this.node.identity.publicKey.hashcode()}: Failed to handle message for log: ${JSON.stringify(
|
|
@@ -591,41 +809,89 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
591
809
|
}
|
|
592
810
|
}
|
|
593
811
|
|
|
594
|
-
getReplicatorsSorted():
|
|
812
|
+
getReplicatorsSorted(): yallist<ReplicatorRect> | undefined {
|
|
595
813
|
return this._sortedPeersCache;
|
|
596
814
|
}
|
|
597
815
|
|
|
816
|
+
async waitForReplicator(...keys: PublicSignKey[]) {
|
|
817
|
+
const check = () => {
|
|
818
|
+
for (const k of keys) {
|
|
819
|
+
if (
|
|
820
|
+
!this.getReplicatorsSorted()
|
|
821
|
+
?.toArray()
|
|
822
|
+
?.find((x) => x.publicKey.equals(k))
|
|
823
|
+
) {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return true;
|
|
828
|
+
};
|
|
829
|
+
return waitFor(() => check(), { signal: this._closeController.signal });
|
|
830
|
+
}
|
|
831
|
+
|
|
598
832
|
async isLeader(
|
|
599
833
|
slot: { toString(): string },
|
|
600
|
-
numberOfLeaders: number
|
|
834
|
+
numberOfLeaders: number,
|
|
835
|
+
options?: {
|
|
836
|
+
candidates?: string[];
|
|
837
|
+
roleAge?: number;
|
|
838
|
+
}
|
|
601
839
|
): Promise<boolean> {
|
|
602
|
-
const isLeader = (
|
|
603
|
-
|
|
604
|
-
);
|
|
840
|
+
const isLeader = (
|
|
841
|
+
await this.findLeaders(slot, numberOfLeaders, options)
|
|
842
|
+
).find((l) => l === this.node.identity.publicKey.hashcode());
|
|
605
843
|
return !!isLeader;
|
|
606
844
|
}
|
|
607
845
|
|
|
846
|
+
private async waitForIsLeader(
|
|
847
|
+
slot: { toString(): string },
|
|
848
|
+
numberOfLeaders: number,
|
|
849
|
+
timeout = WAIT_FOR_REPLICATOR_TIMEOUT
|
|
850
|
+
): Promise<boolean> {
|
|
851
|
+
return new Promise((res, rej) => {
|
|
852
|
+
const removeListeners = () => {
|
|
853
|
+
this.events.removeEventListener("role", roleListener);
|
|
854
|
+
this._closeController.signal.addEventListener("abort", abortListener);
|
|
855
|
+
};
|
|
856
|
+
const abortListener = () => {
|
|
857
|
+
removeListeners();
|
|
858
|
+
clearTimeout(timer);
|
|
859
|
+
res(false);
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const timer = setTimeout(() => {
|
|
863
|
+
removeListeners();
|
|
864
|
+
res(false);
|
|
865
|
+
}, timeout);
|
|
866
|
+
|
|
867
|
+
const check = () =>
|
|
868
|
+
this.isLeader(slot, numberOfLeaders).then((isLeader) => {
|
|
869
|
+
if (isLeader) {
|
|
870
|
+
removeListeners();
|
|
871
|
+
clearTimeout(timer);
|
|
872
|
+
res(isLeader);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
const roleListener = () => {
|
|
877
|
+
check();
|
|
878
|
+
};
|
|
879
|
+
this.events.addEventListener("role", roleListener);
|
|
880
|
+
this._closeController.signal.addEventListener("abort", abortListener);
|
|
881
|
+
|
|
882
|
+
check();
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
608
886
|
async findLeaders(
|
|
609
887
|
subject: { toString(): string },
|
|
610
|
-
|
|
888
|
+
numberOfLeaders: number,
|
|
889
|
+
options?: {
|
|
890
|
+
roleAge?: number;
|
|
891
|
+
}
|
|
611
892
|
): 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
893
|
// For a fixed set or members, the choosen leaders will always be the same (address invariant)
|
|
620
894
|
// 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
895
|
|
|
630
896
|
// Convert this thing we wan't to distribute to 8 bytes so we get can convert it into a u64
|
|
631
897
|
// modulus into an index
|
|
@@ -634,31 +900,220 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
634
900
|
const seed = await sha256(utf8writer.finalize());
|
|
635
901
|
|
|
636
902
|
// convert hash of slot to a number
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
903
|
+
const cursor = hashToUniformNumber(seed); // bounded between 0 and 1
|
|
904
|
+
return this.findLeadersFromUniformNumber(cursor, numberOfLeaders, options);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private findLeadersFromUniformNumber(
|
|
908
|
+
cursor: number,
|
|
909
|
+
numberOfLeaders: number,
|
|
910
|
+
options?: {
|
|
911
|
+
roleAge?: number;
|
|
912
|
+
}
|
|
913
|
+
) {
|
|
914
|
+
const leaders: Set<string> = new Set();
|
|
915
|
+
const width = 1; // this.getParticipationSum(roleAge);
|
|
916
|
+
const peers = this.getReplicatorsSorted();
|
|
917
|
+
if (!peers || peers?.length === 0) {
|
|
918
|
+
return [];
|
|
919
|
+
}
|
|
920
|
+
numberOfLeaders = Math.min(numberOfLeaders, peers.length);
|
|
921
|
+
|
|
922
|
+
const t = +new Date();
|
|
923
|
+
const roleAge =
|
|
924
|
+
options?.roleAge ??
|
|
925
|
+
Math.min(WAIT_FOR_ROLE_MATURITY, +new Date() - this.openTime);
|
|
926
|
+
|
|
646
927
|
for (let i = 0; i < numberOfLeaders; i++) {
|
|
647
|
-
|
|
928
|
+
let matured = 0;
|
|
929
|
+
const maybeIncrementMatured = (role: Replicator) => {
|
|
930
|
+
if (t - Number(role.timestamp) > roleAge) {
|
|
931
|
+
matured++;
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
const x = ((cursor + i / numberOfLeaders) % 1) * width;
|
|
936
|
+
let currentNode = peers.head;
|
|
937
|
+
const diffs: { diff: number; rect: ReplicatorRect }[] = [];
|
|
938
|
+
while (currentNode) {
|
|
939
|
+
const start = currentNode.value.offset % width;
|
|
940
|
+
const absDelta = Math.abs(start - x);
|
|
941
|
+
const diff = Math.min(absDelta, width - absDelta);
|
|
942
|
+
|
|
943
|
+
if (diff < currentNode.value.role.factor / 2 + 0.00001) {
|
|
944
|
+
leaders.add(currentNode.value.publicKey.hashcode());
|
|
945
|
+
maybeIncrementMatured(currentNode.value.role);
|
|
946
|
+
} else {
|
|
947
|
+
diffs.push({
|
|
948
|
+
diff:
|
|
949
|
+
currentNode.value.role.factor > 0
|
|
950
|
+
? diff / currentNode.value.role.factor
|
|
951
|
+
: Number.MAX_SAFE_INTEGER,
|
|
952
|
+
rect: currentNode.value
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
currentNode = currentNode.next;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (matured === 0) {
|
|
960
|
+
diffs.sort((x, y) => x.diff - y.diff);
|
|
961
|
+
for (const node of diffs) {
|
|
962
|
+
leaders.add(node.rect.publicKey.hashcode());
|
|
963
|
+
maybeIncrementMatured(node.rect.role);
|
|
964
|
+
if (matured > 0) {
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return [...leaders];
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
*
|
|
976
|
+
* @returns groups where at least one in any group will have the entry you are looking for
|
|
977
|
+
*/
|
|
978
|
+
getReplicatorUnion(roleAge: number = WAIT_FOR_ROLE_MATURITY) {
|
|
979
|
+
// Total replication "width"
|
|
980
|
+
const width = 1; //this.getParticipationSum(roleAge);
|
|
981
|
+
|
|
982
|
+
// How much width you need to "query" to
|
|
983
|
+
|
|
984
|
+
const peers = this.getReplicatorsSorted()!; // TODO types
|
|
985
|
+
const minReplicas = Math.min(
|
|
986
|
+
peers.length,
|
|
987
|
+
this.replicas.min.getValue(this)
|
|
988
|
+
);
|
|
989
|
+
const coveringWidth = width / minReplicas;
|
|
990
|
+
|
|
991
|
+
let walker = peers.head;
|
|
992
|
+
if (this.role instanceof Replicator) {
|
|
993
|
+
// start at our node (local first)
|
|
994
|
+
while (walker) {
|
|
995
|
+
if (walker.value.publicKey.equals(this.node.identity.publicKey)) {
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
walker = walker.next;
|
|
999
|
+
}
|
|
1000
|
+
} else {
|
|
1001
|
+
const seed = Math.round(peers.length * Math.random()); // start at a random point
|
|
1002
|
+
for (let i = 0; i < seed - 1; i++) {
|
|
1003
|
+
if (walker?.next == null) {
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
walker = walker.next;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const set: string[] = [];
|
|
1011
|
+
let distance = 0;
|
|
1012
|
+
const startNode = walker;
|
|
1013
|
+
if (!startNode) {
|
|
1014
|
+
return [];
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
let nextPoint = startNode.value.offset;
|
|
1018
|
+
const t = +new Date();
|
|
1019
|
+
while (walker && distance < coveringWidth) {
|
|
1020
|
+
const absDelta = Math.abs(walker!.value.offset - nextPoint);
|
|
1021
|
+
const diff = Math.min(absDelta, width - absDelta);
|
|
1022
|
+
|
|
1023
|
+
if (diff < walker!.value.role.factor / 2 + 0.00001) {
|
|
1024
|
+
set.push(walker!.value.publicKey.hashcode());
|
|
1025
|
+
if (
|
|
1026
|
+
t - Number(walker!.value.role.timestamp) >
|
|
1027
|
+
roleAge /* ||
|
|
1028
|
+
walker!.value.publicKey.equals(this.node.identity.publicKey)) */
|
|
1029
|
+
) {
|
|
1030
|
+
nextPoint = (nextPoint + walker!.value.role.factor) % 1;
|
|
1031
|
+
distance += walker!.value.role.factor;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
walker = walker.next || peers.head;
|
|
1036
|
+
|
|
1037
|
+
if (
|
|
1038
|
+
walker?.value.publicKey &&
|
|
1039
|
+
startNode?.value.publicKey.equals(walker?.value.publicKey)
|
|
1040
|
+
) {
|
|
1041
|
+
break; // TODO throw error for failing to fetch ffull width
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return set;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
async replicator(
|
|
1049
|
+
entry: Entry<any>,
|
|
1050
|
+
options?: {
|
|
1051
|
+
candidates?: string[];
|
|
1052
|
+
roleAge?: number;
|
|
648
1053
|
}
|
|
649
|
-
|
|
1054
|
+
) {
|
|
1055
|
+
return this.isLeader(
|
|
1056
|
+
entry.gid,
|
|
1057
|
+
decodeReplicas(entry).getValue(this),
|
|
1058
|
+
options
|
|
1059
|
+
);
|
|
650
1060
|
}
|
|
651
1061
|
|
|
652
|
-
private
|
|
653
|
-
|
|
1062
|
+
private onRoleChange(
|
|
1063
|
+
prev: Observer | Replicator | undefined,
|
|
1064
|
+
role: Observer | Replicator,
|
|
654
1065
|
publicKey: PublicSignKey
|
|
655
1066
|
) {
|
|
1067
|
+
if (this.closed) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
this.distribute();
|
|
1072
|
+
|
|
1073
|
+
if (role instanceof Replicator) {
|
|
1074
|
+
const timer = setTimeout(async () => {
|
|
1075
|
+
this._closeController.signal.removeEventListener("abort", listener);
|
|
1076
|
+
await this.rebalanceParticipationDebounced?.();
|
|
1077
|
+
this.distribute();
|
|
1078
|
+
}, WAIT_FOR_ROLE_MATURITY + 2000);
|
|
1079
|
+
|
|
1080
|
+
const listener = () => {
|
|
1081
|
+
clearTimeout(timer);
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
this._closeController.signal.addEventListener("abort", listener);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
this.events.dispatchEvent(
|
|
1088
|
+
new CustomEvent<UpdateRoleEvent>("role", {
|
|
1089
|
+
detail: { publicKey, role }
|
|
1090
|
+
})
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
private async modifyReplicators(
|
|
1095
|
+
role: Observer | Replicator,
|
|
1096
|
+
publicKey: PublicSignKey
|
|
1097
|
+
) {
|
|
1098
|
+
const { prev, changed } = await this._modifyReplicators(role, publicKey);
|
|
1099
|
+
if (changed) {
|
|
1100
|
+
await this.rebalanceParticipationDebounced?.(); // await this.rebalanceParticipation(false);
|
|
1101
|
+
this.onRoleChange(prev, role, publicKey);
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
private async _modifyReplicators(
|
|
1108
|
+
role: Observer | Replicator,
|
|
1109
|
+
publicKey: PublicSignKey
|
|
1110
|
+
): Promise<{ prev?: Replicator; changed: boolean }> {
|
|
656
1111
|
if (
|
|
657
|
-
|
|
1112
|
+
role instanceof Replicator &&
|
|
658
1113
|
this._canReplicate &&
|
|
659
|
-
!(await this._canReplicate(publicKey))
|
|
1114
|
+
!(await this._canReplicate(publicKey, role))
|
|
660
1115
|
) {
|
|
661
|
-
return false;
|
|
1116
|
+
return { changed: false };
|
|
662
1117
|
}
|
|
663
1118
|
|
|
664
1119
|
const sortedPeer = this._sortedPeersCache;
|
|
@@ -666,81 +1121,125 @@ export class SharedLog<T = Uint8Array> extends Program<Args<T>> {
|
|
|
666
1121
|
if (this.closed === false) {
|
|
667
1122
|
throw new Error("Unexpected, sortedPeersCache is undefined");
|
|
668
1123
|
}
|
|
669
|
-
return false;
|
|
1124
|
+
return { changed: false };
|
|
670
1125
|
}
|
|
671
|
-
|
|
672
|
-
if (
|
|
1126
|
+
|
|
1127
|
+
if (role instanceof Replicator && role.factor > 0) {
|
|
673
1128
|
// TODO use Set + list for fast lookup
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1129
|
+
// check also that peer is online
|
|
1130
|
+
|
|
1131
|
+
const isOnline =
|
|
1132
|
+
this.node.identity.publicKey.equals(publicKey) ||
|
|
1133
|
+
(await this.waitFor(publicKey, { signal: this._closeController.signal })
|
|
1134
|
+
.then(() => true)
|
|
1135
|
+
.catch(() => false));
|
|
1136
|
+
|
|
1137
|
+
if (isOnline) {
|
|
1138
|
+
// insert or if already there do nothing
|
|
1139
|
+
const code = hashToUniformNumber(publicKey.bytes);
|
|
1140
|
+
const rect: ReplicatorRect = {
|
|
1141
|
+
publicKey,
|
|
1142
|
+
offset: code,
|
|
1143
|
+
role
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
let currentNode = sortedPeer.head;
|
|
1147
|
+
if (!currentNode) {
|
|
1148
|
+
sortedPeer.push(rect);
|
|
1149
|
+
return { changed: true };
|
|
1150
|
+
} else {
|
|
1151
|
+
while (currentNode) {
|
|
1152
|
+
if (currentNode.value.publicKey.equals(publicKey)) {
|
|
1153
|
+
// update the value
|
|
1154
|
+
// rect.timestamp = currentNode.value.timestamp;
|
|
1155
|
+
const prev = currentNode.value;
|
|
1156
|
+
currentNode.value = rect;
|
|
1157
|
+
// TODO change detection and only do change stuff if diff?
|
|
1158
|
+
return { prev: prev.role, changed: true };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (code > currentNode.value.offset) {
|
|
1162
|
+
const next = currentNode?.next;
|
|
1163
|
+
if (next) {
|
|
1164
|
+
currentNode = next;
|
|
1165
|
+
continue;
|
|
1166
|
+
} else {
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
} else {
|
|
1170
|
+
currentNode = currentNode.prev;
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const prev = currentNode;
|
|
1176
|
+
if (!prev?.next?.value.publicKey.equals(publicKey)) {
|
|
1177
|
+
_insertAfter(sortedPeer, prev || undefined, rect);
|
|
1178
|
+
} else {
|
|
1179
|
+
throw new Error("Unexpected");
|
|
1180
|
+
}
|
|
1181
|
+
return { changed: true };
|
|
1182
|
+
}
|
|
678
1183
|
} else {
|
|
679
|
-
return false;
|
|
1184
|
+
return { changed: false };
|
|
680
1185
|
}
|
|
681
1186
|
} else {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1187
|
+
let currentNode = sortedPeer.head;
|
|
1188
|
+
while (currentNode) {
|
|
1189
|
+
if (currentNode.value.publicKey.equals(publicKey)) {
|
|
1190
|
+
sortedPeer.removeNode(currentNode);
|
|
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,257 @@ 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: ReplicationController;
|
|
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
|
+
private sumFactors() {
|
|
1530
|
+
let sum = 0;
|
|
1531
|
+
for (const element of this.getReplicatorsSorted()?.toArray() || []) {
|
|
1532
|
+
sum += element.role.factor;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
return sum;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
async rebalanceParticipation(onRoleChange = true) {
|
|
1539
|
+
// update more participation rate to converge to the average expected rate or bounded by
|
|
1540
|
+
// resources such as memory and or cpu
|
|
1541
|
+
|
|
1542
|
+
if (this.closed) {
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// The role is fixed (no changes depending on memory usage or peer count etc)
|
|
1547
|
+
if (this._roleOptions instanceof Role) {
|
|
1548
|
+
return false;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// TODO second condition: what if the current role is Observer?
|
|
1552
|
+
if (
|
|
1553
|
+
this._roleOptions.type == "replicator" &&
|
|
1554
|
+
this._role instanceof Replicator
|
|
1555
|
+
) {
|
|
1556
|
+
const peers = this.getReplicatorsSorted();
|
|
1557
|
+
const usedMemory = await this.getMemoryUsage();
|
|
1558
|
+
|
|
1559
|
+
const newFactor =
|
|
1560
|
+
await this.replicationController.adjustReplicationFactor(
|
|
1561
|
+
usedMemory,
|
|
1562
|
+
this._role.factor,
|
|
1563
|
+
this.sumFactors(),
|
|
1564
|
+
peers?.length || 1
|
|
1565
|
+
);
|
|
1566
|
+
|
|
1567
|
+
const newRole = new Replicator({
|
|
1568
|
+
factor: newFactor,
|
|
1569
|
+
timestamp: this._role.timestamp
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
const relativeDifference =
|
|
1573
|
+
Math.abs(this._role.factor - newRole.factor) / this._role.factor;
|
|
1574
|
+
|
|
1575
|
+
if (relativeDifference > 0.0001) {
|
|
1576
|
+
const canReplicate =
|
|
1577
|
+
!this._canReplicate ||
|
|
1578
|
+
(await this._canReplicate(this.node.identity.publicKey, newRole));
|
|
1579
|
+
if (!canReplicate) {
|
|
1580
|
+
return false;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
await this._updateRole(newRole, onRoleChange);
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function _insertAfter(
|
|
1593
|
+
self: yallist<any>,
|
|
1594
|
+
node: yallist.Node<ReplicatorRect> | undefined,
|
|
1595
|
+
value: ReplicatorRect
|
|
1596
|
+
) {
|
|
1597
|
+
const inserted = !node
|
|
1598
|
+
? new yallist.Node(
|
|
1599
|
+
value,
|
|
1600
|
+
null as any,
|
|
1601
|
+
self.head as yallist.Node<ReplicatorRect> | undefined,
|
|
1602
|
+
self
|
|
1603
|
+
)
|
|
1604
|
+
: new yallist.Node(
|
|
1605
|
+
value,
|
|
1606
|
+
node,
|
|
1607
|
+
node.next as yallist.Node<ReplicatorRect> | undefined,
|
|
1608
|
+
self
|
|
1609
|
+
);
|
|
1610
|
+
|
|
1611
|
+
// is tail
|
|
1612
|
+
if (inserted.next === null) {
|
|
1613
|
+
self.tail = inserted;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// is head
|
|
1617
|
+
if (inserted.prev === null) {
|
|
1618
|
+
self.head = inserted;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
self.length++;
|
|
1622
|
+
return inserted;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
class ReplicationController {
|
|
1626
|
+
integral = 0;
|
|
1627
|
+
prevError = 0;
|
|
1628
|
+
prevMemoryUsage = 0;
|
|
1629
|
+
lastTs = 0;
|
|
1630
|
+
kp: number;
|
|
1631
|
+
ki: number;
|
|
1632
|
+
kd: number;
|
|
1633
|
+
errorFunction: ReplicationErrorFunction;
|
|
1634
|
+
targetMemoryLimit?: number;
|
|
1635
|
+
constructor(
|
|
1636
|
+
options: {
|
|
1637
|
+
errorFunction?: ReplicationErrorFunction;
|
|
1638
|
+
targetMemoryLimit?: number;
|
|
1639
|
+
kp?: number;
|
|
1640
|
+
ki?: number;
|
|
1641
|
+
kd?: number;
|
|
1642
|
+
} = {}
|
|
1643
|
+
) {
|
|
1644
|
+
const {
|
|
1645
|
+
targetMemoryLimit,
|
|
1646
|
+
kp = 0.1,
|
|
1647
|
+
ki = 0 /* 0.01, */,
|
|
1648
|
+
kd = 0.1,
|
|
1649
|
+
errorFunction = ({ balance, coverage, memory }) =>
|
|
1650
|
+
memory * 0.8 + balance * 0.1 + coverage * 0.1
|
|
1651
|
+
} = options;
|
|
1652
|
+
this.kp = kp;
|
|
1653
|
+
this.ki = ki;
|
|
1654
|
+
this.kd = kd;
|
|
1655
|
+
this.targetMemoryLimit = targetMemoryLimit;
|
|
1656
|
+
this.errorFunction = errorFunction;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
async adjustReplicationFactor(
|
|
1660
|
+
memoryUsage: number,
|
|
1661
|
+
currentFactor: number,
|
|
1662
|
+
totalFactor: number,
|
|
1663
|
+
peerCount: number
|
|
1664
|
+
) {
|
|
1665
|
+
/* if (memoryUsage === this.prevMemoryUsage) {
|
|
1666
|
+
return Math.max(Math.min(currentFactor, maxFraction), minFraction);
|
|
1667
|
+
} */
|
|
1668
|
+
|
|
1669
|
+
this.prevMemoryUsage = memoryUsage;
|
|
1670
|
+
if (memoryUsage <= 0) {
|
|
1671
|
+
this.integral = 0;
|
|
1672
|
+
}
|
|
1673
|
+
const normalizedFactor = currentFactor / totalFactor;
|
|
1674
|
+
/*const estimatedTotalSize = memoryUsage / normalizedFactor;
|
|
1675
|
+
|
|
1676
|
+
const errorMemory =
|
|
1677
|
+
currentFactor > 0 && memoryUsage > 0
|
|
1678
|
+
? (Math.max(Math.min(maxFraction, this.targetMemoryLimit / estimatedTotalSize), minFraction) -
|
|
1679
|
+
normalizedFactor) * totalFactor
|
|
1680
|
+
: minFraction; */
|
|
1681
|
+
|
|
1682
|
+
const estimatedTotalSize = memoryUsage / currentFactor;
|
|
1683
|
+
const errorCoverage = Math.min(1 - totalFactor, 1);
|
|
1684
|
+
|
|
1685
|
+
let errorMemory = 0;
|
|
1686
|
+
const errorTarget = 1 / peerCount - currentFactor;
|
|
1687
|
+
|
|
1688
|
+
if (this.targetMemoryLimit != null) {
|
|
1689
|
+
errorMemory =
|
|
1690
|
+
currentFactor > 0 && memoryUsage > 0
|
|
1691
|
+
? Math.max(
|
|
1692
|
+
Math.min(1, this.targetMemoryLimit / estimatedTotalSize),
|
|
1693
|
+
0
|
|
1694
|
+
) - currentFactor
|
|
1695
|
+
: 0.0001;
|
|
1696
|
+
|
|
1697
|
+
/* errorTarget = errorMemory + (1 / peerCount - currentFactor) / 10; */
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// alpha * errorCoverage + (1 - alpha) * errorTarget;
|
|
1701
|
+
const totalError = this.errorFunction({
|
|
1702
|
+
balance: errorTarget,
|
|
1703
|
+
coverage: errorCoverage,
|
|
1704
|
+
memory: errorMemory
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
if (this.lastTs === 0) {
|
|
1708
|
+
this.lastTs = +new Date();
|
|
1709
|
+
}
|
|
1710
|
+
const kpAdjusted = Math.min(
|
|
1711
|
+
Math.max(this.kp, (+new Date() - this.lastTs) / 100),
|
|
1712
|
+
0.8
|
|
1713
|
+
);
|
|
1714
|
+
const pTerm = kpAdjusted * totalError;
|
|
1715
|
+
|
|
1716
|
+
this.lastTs = +new Date();
|
|
1717
|
+
|
|
1718
|
+
// Integral term
|
|
1719
|
+
this.integral += totalError;
|
|
1720
|
+
const beta = 0.4;
|
|
1721
|
+
this.integral = beta * totalError + (1 - beta) * this.integral;
|
|
1722
|
+
|
|
1723
|
+
const iTerm = this.ki * this.integral;
|
|
1724
|
+
|
|
1725
|
+
// Derivative term
|
|
1726
|
+
const derivative = totalError - this.prevError;
|
|
1727
|
+
const dTerm = this.kd * derivative;
|
|
1728
|
+
|
|
1729
|
+
// Calculate the new replication factor
|
|
1730
|
+
const newFactor = currentFactor + pTerm + iTerm + dTerm;
|
|
1731
|
+
|
|
1732
|
+
// Update state for the next iteration
|
|
1733
|
+
this.prevError = totalError;
|
|
1734
|
+
|
|
1735
|
+
return Math.max(Math.min(newFactor, 1), 0);
|
|
1736
|
+
}
|
|
982
1737
|
}
|