@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/lib/esm/index.js
CHANGED
|
@@ -11,19 +11,25 @@ import { RPC } from "@peerbit/rpc";
|
|
|
11
11
|
import { TransportMessage } from "./message.js";
|
|
12
12
|
import { Entry, Log } from "@peerbit/log";
|
|
13
13
|
import { Program } from "@peerbit/program";
|
|
14
|
-
import {
|
|
15
|
-
import { AccessError,
|
|
14
|
+
import { BinaryWriter, BorshError, field, variant } from "@dao-xyz/borsh";
|
|
15
|
+
import { AccessError, sha256, sha256Base64Sync } from "@peerbit/crypto";
|
|
16
16
|
import { logger as loggerFn } from "@peerbit/logger";
|
|
17
17
|
import { ExchangeHeadsMessage, RequestIHave, ResponseIHave, createExchangeHeadsMessage } from "./exchange-heads.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { AbsoluteReplicas, ReplicationError, decodeReplicas, encodeReplicas, maxReplicas } from "./replication.js";
|
|
18
|
+
import { AbortError, waitFor } from "@peerbit/time";
|
|
19
|
+
import { Observer, Replicator, Role } from "./role.js";
|
|
20
|
+
import { AbsoluteReplicas, ReplicationError, RequestRoleMessage, ResponseRoleMessage, decodeReplicas, encodeReplicas, hashToUniformNumber, maxReplicas } from "./replication.js";
|
|
22
21
|
import pDefer from "p-defer";
|
|
23
22
|
import { Cache } from "@peerbit/cache";
|
|
23
|
+
import { CustomEvent } from "@libp2p/interface";
|
|
24
|
+
import yallist from "yallist";
|
|
25
|
+
import { AcknowledgeDelivery, SilentDelivery } from "@peerbit/stream-interface";
|
|
26
|
+
import { AnyBlockStore, RemoteBlocks } from "@peerbit/blocks";
|
|
27
|
+
import { BlocksMessage } from "./blocks.js";
|
|
28
|
+
import debounce from "p-debounce";
|
|
29
|
+
import { PIDReplicationController } from "./pid.js";
|
|
24
30
|
export * from "./replication.js";
|
|
25
31
|
export { Observer, Replicator, Role };
|
|
26
|
-
export const logger = loggerFn({ module: "
|
|
32
|
+
export const logger = loggerFn({ module: "shared-log" });
|
|
27
33
|
const groupByGid = async (entries) => {
|
|
28
34
|
const groupByGid = new Map();
|
|
29
35
|
for (const head of entries) {
|
|
@@ -39,25 +45,41 @@ const groupByGid = async (entries) => {
|
|
|
39
45
|
}
|
|
40
46
|
return groupByGid;
|
|
41
47
|
};
|
|
48
|
+
const isAdaptiveReplicatorOption = (options) => {
|
|
49
|
+
if (options.limits ||
|
|
50
|
+
options.error ||
|
|
51
|
+
options.factor == null) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
};
|
|
42
56
|
export const DEFAULT_MIN_REPLICAS = 2;
|
|
57
|
+
export const WAIT_FOR_REPLICATOR_TIMEOUT = 9000;
|
|
58
|
+
export const WAIT_FOR_ROLE_MATURITY = 5000;
|
|
59
|
+
const REBALANCE_DEBOUNCE_INTERAVAL = 50;
|
|
43
60
|
let SharedLog = class SharedLog extends Program {
|
|
44
61
|
log;
|
|
45
62
|
rpc;
|
|
46
63
|
// options
|
|
47
|
-
_sync;
|
|
48
64
|
_role;
|
|
65
|
+
_roleOptions;
|
|
49
66
|
_sortedPeersCache;
|
|
50
|
-
|
|
67
|
+
_totalParticipation;
|
|
51
68
|
_gidPeersHistory;
|
|
52
69
|
_onSubscriptionFn;
|
|
53
70
|
_onUnsubscriptionFn;
|
|
54
71
|
_canReplicate;
|
|
55
72
|
_logProperties;
|
|
73
|
+
_closeController;
|
|
56
74
|
_loadedOnce = false;
|
|
57
75
|
_gidParentCache;
|
|
58
76
|
_respondToIHaveTimeout;
|
|
59
77
|
_pendingDeletes;
|
|
60
78
|
_pendingIHave;
|
|
79
|
+
latestRoleMessages;
|
|
80
|
+
remoteBlocks;
|
|
81
|
+
openTime;
|
|
82
|
+
rebalanceParticipationDebounced;
|
|
61
83
|
replicas;
|
|
62
84
|
constructor(properties) {
|
|
63
85
|
super();
|
|
@@ -67,31 +89,78 @@ let SharedLog = class SharedLog extends Program {
|
|
|
67
89
|
get role() {
|
|
68
90
|
return this._role;
|
|
69
91
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this._role = role;
|
|
73
|
-
await this.initializeWithRole();
|
|
74
|
-
await this.rpc.subscribe(serialize(this._role));
|
|
75
|
-
if (wasRepicators) {
|
|
76
|
-
await this.replicationReorganization();
|
|
77
|
-
}
|
|
92
|
+
get totalParticipation() {
|
|
93
|
+
return this._totalParticipation;
|
|
78
94
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
95
|
+
setupRole(options) {
|
|
96
|
+
this.rebalanceParticipationDebounced = undefined;
|
|
97
|
+
const setupDebouncedRebalancing = (options) => {
|
|
98
|
+
this.replicationController = new PIDReplicationController({
|
|
99
|
+
targetMemoryLimit: options?.limits?.memory,
|
|
100
|
+
errorFunction: options?.error
|
|
101
|
+
});
|
|
102
|
+
this.rebalanceParticipationDebounced = debounce(() => this.rebalanceParticipation(), REBALANCE_DEBOUNCE_INTERAVAL // TODO make dynamic
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
if (options instanceof Observer || options instanceof Replicator) {
|
|
106
|
+
throw new Error("Unsupported role option type");
|
|
86
107
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
else if (options === "observer") {
|
|
109
|
+
this._roleOptions = new Observer();
|
|
110
|
+
}
|
|
111
|
+
else if (options === "replicator") {
|
|
112
|
+
setupDebouncedRebalancing();
|
|
113
|
+
this._roleOptions = { type: options };
|
|
114
|
+
}
|
|
115
|
+
else if (options) {
|
|
116
|
+
if (options.type === "replicator") {
|
|
117
|
+
if (isAdaptiveReplicatorOption(options)) {
|
|
118
|
+
setupDebouncedRebalancing(options);
|
|
119
|
+
this._roleOptions = options;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this._roleOptions = new Replicator({ factor: options.factor });
|
|
123
|
+
}
|
|
90
124
|
}
|
|
91
125
|
else {
|
|
92
|
-
|
|
126
|
+
this._roleOptions = new Observer();
|
|
93
127
|
}
|
|
94
128
|
}
|
|
129
|
+
else {
|
|
130
|
+
// Default option
|
|
131
|
+
setupDebouncedRebalancing();
|
|
132
|
+
this._roleOptions = { type: "replicator" };
|
|
133
|
+
}
|
|
134
|
+
// setup the initial role
|
|
135
|
+
if (this._roleOptions instanceof Replicator ||
|
|
136
|
+
this._roleOptions instanceof Observer) {
|
|
137
|
+
this._role = this._roleOptions; // Fixed
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
this._role = new Replicator({
|
|
141
|
+
// initial role in a dynamic setup
|
|
142
|
+
factor: 1,
|
|
143
|
+
timestamp: BigInt(+new Date())
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return this._role;
|
|
147
|
+
}
|
|
148
|
+
async updateRole(role, onRoleChange = true) {
|
|
149
|
+
return this._updateRole(this.setupRole(role), onRoleChange);
|
|
150
|
+
}
|
|
151
|
+
async _updateRole(role = this._role, onRoleChange = true) {
|
|
152
|
+
this._role = role;
|
|
153
|
+
const { changed } = await this._modifyReplicators(this.role, this.node.identity.publicKey);
|
|
154
|
+
if (!this._loadedOnce) {
|
|
155
|
+
await this.log.load();
|
|
156
|
+
this._loadedOnce = true;
|
|
157
|
+
}
|
|
158
|
+
await this.rpc.subscribe();
|
|
159
|
+
await this.rpc.send(new ResponseRoleMessage(role));
|
|
160
|
+
if (onRoleChange && changed) {
|
|
161
|
+
this.onRoleChange(undefined, this._role, this.node.identity.publicKey);
|
|
162
|
+
}
|
|
163
|
+
return changed;
|
|
95
164
|
}
|
|
96
165
|
async append(data, options) {
|
|
97
166
|
const appendOptions = { ...options };
|
|
@@ -109,7 +178,14 @@ let SharedLog = class SharedLog extends Program {
|
|
|
109
178
|
appendOptions.meta.data = minReplicasData;
|
|
110
179
|
}
|
|
111
180
|
const result = await this.log.append(data, appendOptions);
|
|
112
|
-
await this.
|
|
181
|
+
const leaders = await this.findLeaders(result.entry.meta.gid, decodeReplicas(result.entry).getValue(this));
|
|
182
|
+
const isLeader = leaders.includes(this.node.identity.publicKey.hashcode());
|
|
183
|
+
await this.rpc.send(await createExchangeHeadsMessage(this.log, [result.entry], this._gidParentCache), {
|
|
184
|
+
mode: isLeader
|
|
185
|
+
? new SilentDelivery({ redundancy: 1, to: leaders })
|
|
186
|
+
: new AcknowledgeDelivery({ redundancy: 1, to: leaders })
|
|
187
|
+
});
|
|
188
|
+
this.rebalanceParticipationDebounced?.();
|
|
113
189
|
return result;
|
|
114
190
|
}
|
|
115
191
|
async open(options) {
|
|
@@ -128,20 +204,34 @@ let SharedLog = class SharedLog extends Program {
|
|
|
128
204
|
this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 10 * 1000; // TODO make into arg
|
|
129
205
|
this._pendingDeletes = new Map();
|
|
130
206
|
this._pendingIHave = new Map();
|
|
207
|
+
this.latestRoleMessages = new Map();
|
|
208
|
+
this.openTime = +new Date();
|
|
131
209
|
this._gidParentCache = new Cache({ max: 1000 });
|
|
210
|
+
this._closeController = new AbortController();
|
|
132
211
|
this._canReplicate = options?.canReplicate;
|
|
133
|
-
this._sync = options?.sync;
|
|
134
212
|
this._logProperties = options;
|
|
135
|
-
this.
|
|
136
|
-
this._lastSubscriptionMessageId = 0;
|
|
213
|
+
this.setupRole(options?.role);
|
|
137
214
|
this._onSubscriptionFn = this._onSubscription.bind(this);
|
|
138
|
-
this.
|
|
215
|
+
this._totalParticipation = 0;
|
|
216
|
+
this._sortedPeersCache = yallist.create();
|
|
139
217
|
this._gidPeersHistory = new Map();
|
|
140
218
|
await this.node.services.pubsub.addEventListener("subscribe", this._onSubscriptionFn);
|
|
141
219
|
this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
|
|
142
220
|
await this.node.services.pubsub.addEventListener("unsubscribe", this._onUnsubscriptionFn);
|
|
143
|
-
|
|
144
|
-
|
|
221
|
+
const id = sha256Base64Sync(this.log.id);
|
|
222
|
+
const storage = await this.node.memory.sublevel(id);
|
|
223
|
+
const localBlocks = await new AnyBlockStore(await storage.sublevel("blocks"));
|
|
224
|
+
const cache = await storage.sublevel("cache");
|
|
225
|
+
this.remoteBlocks = new RemoteBlocks({
|
|
226
|
+
local: localBlocks,
|
|
227
|
+
publish: (message, options) => this.rpc.send(new BlocksMessage(message), {
|
|
228
|
+
to: options?.to
|
|
229
|
+
}),
|
|
230
|
+
waitFor: this.rpc.waitFor.bind(this.rpc)
|
|
231
|
+
});
|
|
232
|
+
await this.remoteBlocks.start();
|
|
233
|
+
await this.log.open(this.remoteBlocks, this.node.identity, {
|
|
234
|
+
keychain: this.node.services.keychain,
|
|
145
235
|
...this._logProperties,
|
|
146
236
|
onChange: (change) => {
|
|
147
237
|
if (this._pendingIHave.size > 0) {
|
|
@@ -149,7 +239,7 @@ let SharedLog = class SharedLog extends Program {
|
|
|
149
239
|
const ih = this._pendingIHave.get(added.hash);
|
|
150
240
|
if (ih) {
|
|
151
241
|
ih.clear();
|
|
152
|
-
ih.callback();
|
|
242
|
+
ih.callback(added);
|
|
153
243
|
}
|
|
154
244
|
}
|
|
155
245
|
}
|
|
@@ -181,33 +271,38 @@ let SharedLog = class SharedLog extends Program {
|
|
|
181
271
|
return this._logProperties?.canAppend?.(entry) ?? true;
|
|
182
272
|
},
|
|
183
273
|
trim: this._logProperties?.trim && {
|
|
184
|
-
...this._logProperties?.trim
|
|
185
|
-
filter: {
|
|
186
|
-
canTrim: async (entry) => !(await this.isLeader(entry.meta.gid, decodeReplicas(entry).getValue(this))),
|
|
187
|
-
cacheId: () => this._lastSubscriptionMessageId
|
|
188
|
-
}
|
|
274
|
+
...this._logProperties?.trim
|
|
189
275
|
},
|
|
190
|
-
cache:
|
|
191
|
-
(await this.node.memory.sublevel(sha256Base64Sync(this.log.id)))
|
|
192
|
-
});
|
|
193
|
-
await this.initializeWithRole();
|
|
194
|
-
// Take into account existing subscription
|
|
195
|
-
(await this.node.services.pubsub.getSubscribers(this.topic))?.forEach((v, k) => {
|
|
196
|
-
this.handleSubscriptionChange(v.publicKey, [{ topic: this.topic, data: v.data }], true);
|
|
276
|
+
cache: cache
|
|
197
277
|
});
|
|
198
278
|
// Open for communcation
|
|
199
279
|
await this.rpc.open({
|
|
200
280
|
queryType: TransportMessage,
|
|
201
281
|
responseType: TransportMessage,
|
|
202
282
|
responseHandler: this._onMessage.bind(this),
|
|
203
|
-
topic: this.topic
|
|
204
|
-
subscriptionData: serialize(this.role)
|
|
283
|
+
topic: this.topic
|
|
205
284
|
});
|
|
285
|
+
await this._updateRole();
|
|
286
|
+
await this.rebalanceParticipation();
|
|
287
|
+
// Take into account existing subscription
|
|
288
|
+
(await this.node.services.pubsub.getSubscribers(this.topic))?.forEach((v, k) => {
|
|
289
|
+
if (v.equals(this.node.identity.publicKey)) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.handleSubscriptionChange(v, [this.topic], true);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async getMemoryUsage() {
|
|
296
|
+
return (((await this.log.memory?.size()) || 0) + (await this.log.blocks.size()));
|
|
206
297
|
}
|
|
207
298
|
get topic() {
|
|
208
299
|
return this.log.idString;
|
|
209
300
|
}
|
|
210
301
|
async _close() {
|
|
302
|
+
this._closeController.abort();
|
|
303
|
+
this.node.services.pubsub.removeEventListener("subscribe", this._onSubscriptionFn);
|
|
304
|
+
this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
|
|
305
|
+
this.node.services.pubsub.removeEventListener("unsubscribe", this._onUnsubscriptionFn);
|
|
211
306
|
for (const [k, v] of this._pendingDeletes) {
|
|
212
307
|
v.clear();
|
|
213
308
|
v.promise.resolve(); // TODO or reject?
|
|
@@ -215,15 +310,14 @@ let SharedLog = class SharedLog extends Program {
|
|
|
215
310
|
for (const [k, v] of this._pendingIHave) {
|
|
216
311
|
v.clear();
|
|
217
312
|
}
|
|
313
|
+
await this.remoteBlocks.stop();
|
|
218
314
|
this._gidParentCache.clear();
|
|
219
315
|
this._pendingDeletes = new Map();
|
|
220
316
|
this._pendingIHave = new Map();
|
|
317
|
+
this.latestRoleMessages.clear();
|
|
221
318
|
this._gidPeersHistory = new Map();
|
|
222
319
|
this._sortedPeersCache = undefined;
|
|
223
320
|
this._loadedOnce = false;
|
|
224
|
-
this.node.services.pubsub.removeEventListener("subscribe", this._onSubscriptionFn);
|
|
225
|
-
this._onUnsubscriptionFn = this._onUnsubscription.bind(this);
|
|
226
|
-
this.node.services.pubsub.removeEventListener("unsubscribe", this._onUnsubscriptionFn);
|
|
227
321
|
}
|
|
228
322
|
async close(from) {
|
|
229
323
|
const superClosed = await super.close(from);
|
|
@@ -239,8 +333,8 @@ let SharedLog = class SharedLog extends Program {
|
|
|
239
333
|
if (!superDropped) {
|
|
240
334
|
return superDropped;
|
|
241
335
|
}
|
|
242
|
-
await this._close();
|
|
243
336
|
await this.log.drop();
|
|
337
|
+
await this._close();
|
|
244
338
|
return true;
|
|
245
339
|
}
|
|
246
340
|
async recover() {
|
|
@@ -255,7 +349,6 @@ let SharedLog = class SharedLog extends Program {
|
|
|
255
349
|
* I can use them to load associated logs and join/sync them with the data stores I own
|
|
256
350
|
*/
|
|
257
351
|
const { heads } = msg;
|
|
258
|
-
// replication topic === trustedNetwork address
|
|
259
352
|
logger.debug(`${this.node.identity.publicKey.hashcode()}: Recieved heads: ${heads.length === 1 ? heads[0].entry.hash : "#" + heads.length}, logId: ${this.log.idString}`);
|
|
260
353
|
if (heads) {
|
|
261
354
|
const filteredHeads = [];
|
|
@@ -269,20 +362,22 @@ let SharedLog = class SharedLog extends Program {
|
|
|
269
362
|
filteredHeads.push(head);
|
|
270
363
|
}
|
|
271
364
|
}
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
365
|
+
if (filteredHeads.length === 0) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const toMerge = [];
|
|
369
|
+
let toDelete = undefined;
|
|
370
|
+
let maybeDelete = undefined;
|
|
371
|
+
const groupedByGid = await groupByGid(filteredHeads);
|
|
372
|
+
const promises = [];
|
|
373
|
+
for (const [gid, entries] of groupedByGid) {
|
|
374
|
+
const fn = async () => {
|
|
278
375
|
const headsWithGid = this.log.headsIndex.gids.get(gid);
|
|
279
376
|
const maxReplicasFromHead = headsWithGid && headsWithGid.size > 0
|
|
280
377
|
? maxReplicas(this, [...headsWithGid.values()])
|
|
281
378
|
: this.replicas.min.getValue(this);
|
|
282
|
-
const maxReplicasFromNewEntries = maxReplicas(this,
|
|
283
|
-
|
|
284
|
-
]);
|
|
285
|
-
const isLeader = await this.isLeader(gid, Math.max(maxReplicasFromHead, maxReplicasFromNewEntries));
|
|
379
|
+
const maxReplicasFromNewEntries = maxReplicas(this, entries.map((x) => x.entry));
|
|
380
|
+
const isLeader = await this.waitForIsLeader(gid, Math.max(maxReplicasFromHead, maxReplicasFromNewEntries));
|
|
286
381
|
if (maxReplicasFromNewEntries < maxReplicasFromHead && isLeader) {
|
|
287
382
|
(maybeDelete || (maybeDelete = [])).push(entries);
|
|
288
383
|
}
|
|
@@ -302,34 +397,35 @@ let SharedLog = class SharedLog extends Program {
|
|
|
302
397
|
}
|
|
303
398
|
logger.debug(`${this.node.identity.publicKey.hashcode()}: Dropping heads with gid: ${entry.entry.gid}. Because not leader`);
|
|
304
399
|
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
400
|
+
};
|
|
401
|
+
promises.push(fn());
|
|
402
|
+
}
|
|
403
|
+
await Promise.all(promises);
|
|
404
|
+
if (this.closed) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (toMerge.length > 0) {
|
|
408
|
+
await this.log.join(toMerge);
|
|
409
|
+
toDelete &&
|
|
410
|
+
this.prune(toDelete).catch((e) => {
|
|
411
|
+
logger.error(e.toString());
|
|
412
|
+
});
|
|
413
|
+
this.rebalanceParticipationDebounced?.();
|
|
414
|
+
}
|
|
415
|
+
if (maybeDelete) {
|
|
416
|
+
for (const entries of maybeDelete) {
|
|
417
|
+
const headsWithGid = this.log.headsIndex.gids.get(entries[0].entry.meta.gid);
|
|
418
|
+
if (headsWithGid && headsWithGid.size > 0) {
|
|
419
|
+
const minReplicas = maxReplicas(this, headsWithGid.values());
|
|
420
|
+
const isLeader = await this.isLeader(entries[0].entry.meta.gid, minReplicas);
|
|
421
|
+
if (!isLeader) {
|
|
422
|
+
this.prune(entries.map((x) => x.entry)).catch((e) => {
|
|
423
|
+
logger.error(e.toString());
|
|
424
|
+
});
|
|
326
425
|
}
|
|
327
426
|
}
|
|
328
427
|
}
|
|
329
428
|
}
|
|
330
|
-
else {
|
|
331
|
-
await this.log.join(await Promise.all(filteredHeads.map((x) => this._sync(x.entry))).then((filter) => filteredHeads.filter((v, ix) => filter[ix])));
|
|
332
|
-
}
|
|
333
429
|
}
|
|
334
430
|
}
|
|
335
431
|
else if (msg instanceof RequestIHave) {
|
|
@@ -347,12 +443,14 @@ let SharedLog = class SharedLog extends Program {
|
|
|
347
443
|
clearTimeout(timeout);
|
|
348
444
|
prevPendingIHave?.clear();
|
|
349
445
|
},
|
|
350
|
-
callback: () => {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
446
|
+
callback: async (entry) => {
|
|
447
|
+
if (await this.isLeader(entry.meta.gid, decodeReplicas(entry).getValue(this))) {
|
|
448
|
+
this.rpc.send(new ResponseIHave({ hashes: [entry.hash] }), {
|
|
449
|
+
to: [context.from]
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
prevPendingIHave && prevPendingIHave.callback(entry);
|
|
453
|
+
this._pendingIHave.delete(entry.hash);
|
|
356
454
|
}
|
|
357
455
|
};
|
|
358
456
|
const timeout = setTimeout(() => {
|
|
@@ -364,7 +462,7 @@ let SharedLog = class SharedLog extends Program {
|
|
|
364
462
|
this._pendingIHave.set(hash, pendingIHave);
|
|
365
463
|
}
|
|
366
464
|
}
|
|
367
|
-
this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
|
|
465
|
+
await this.rpc.send(new ResponseIHave({ hashes: hasAndIsLeader }), {
|
|
368
466
|
to: [context.from]
|
|
369
467
|
});
|
|
370
468
|
}
|
|
@@ -373,11 +471,55 @@ let SharedLog = class SharedLog extends Program {
|
|
|
373
471
|
this._pendingDeletes.get(hash)?.callback(context.from.hashcode());
|
|
374
472
|
}
|
|
375
473
|
}
|
|
474
|
+
else if (msg instanceof BlocksMessage) {
|
|
475
|
+
await this.remoteBlocks.onMessage(msg.message);
|
|
476
|
+
}
|
|
477
|
+
else if (msg instanceof RequestRoleMessage) {
|
|
478
|
+
if (!context.from) {
|
|
479
|
+
throw new Error("Missing form in update role message");
|
|
480
|
+
}
|
|
481
|
+
if (context.from.equals(this.node.identity.publicKey)) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
await this.rpc.send(new ResponseRoleMessage({ role: this.role }), {
|
|
485
|
+
to: [context.from]
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
else if (msg instanceof ResponseRoleMessage) {
|
|
489
|
+
if (!context.from) {
|
|
490
|
+
throw new Error("Missing form in update role message");
|
|
491
|
+
}
|
|
492
|
+
if (context.from.equals(this.node.identity.publicKey)) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
this.waitFor(context.from, {
|
|
496
|
+
signal: this._closeController.signal,
|
|
497
|
+
timeout: WAIT_FOR_REPLICATOR_TIMEOUT
|
|
498
|
+
})
|
|
499
|
+
.then(async () => {
|
|
500
|
+
/* await delay(1000 * Math.random()) */
|
|
501
|
+
const prev = this.latestRoleMessages.get(context.from.hashcode());
|
|
502
|
+
if (prev && prev > context.timestamp) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
this.latestRoleMessages.set(context.from.hashcode(), context.timestamp);
|
|
506
|
+
await this.modifyReplicators(msg.role, context.from);
|
|
507
|
+
})
|
|
508
|
+
.catch((e) => {
|
|
509
|
+
if (e instanceof AbortError) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
logger.error("Failed to find peer who updated their role: " + e?.message);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
376
515
|
else {
|
|
377
516
|
throw new Error("Unexpected message");
|
|
378
517
|
}
|
|
379
518
|
}
|
|
380
519
|
catch (e) {
|
|
520
|
+
if (e instanceof AbortError) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
381
523
|
if (e instanceof BorshError) {
|
|
382
524
|
logger.trace(`${this.node.identity.publicKey.hashcode()}: Failed to handle message on topic: ${JSON.stringify(this.log.idString)}: Got message for a different namespace`);
|
|
383
525
|
return;
|
|
@@ -392,114 +534,327 @@ let SharedLog = class SharedLog extends Program {
|
|
|
392
534
|
getReplicatorsSorted() {
|
|
393
535
|
return this._sortedPeersCache;
|
|
394
536
|
}
|
|
395
|
-
async
|
|
396
|
-
const
|
|
537
|
+
async waitForReplicator(...keys) {
|
|
538
|
+
const check = () => {
|
|
539
|
+
for (const k of keys) {
|
|
540
|
+
if (!this.getReplicatorsSorted()
|
|
541
|
+
?.toArray()
|
|
542
|
+
?.find((x) => x.publicKey.equals(k))) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return true;
|
|
547
|
+
};
|
|
548
|
+
return waitFor(() => check(), { signal: this._closeController.signal });
|
|
549
|
+
}
|
|
550
|
+
async isLeader(slot, numberOfLeaders, options) {
|
|
551
|
+
const isLeader = (await this.findLeaders(slot, numberOfLeaders, options)).find((l) => l === this.node.identity.publicKey.hashcode());
|
|
397
552
|
return !!isLeader;
|
|
398
553
|
}
|
|
399
|
-
async
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
554
|
+
async waitForIsLeader(slot, numberOfLeaders, timeout = WAIT_FOR_REPLICATOR_TIMEOUT) {
|
|
555
|
+
return new Promise((res, rej) => {
|
|
556
|
+
const removeListeners = () => {
|
|
557
|
+
this.events.removeEventListener("role", roleListener);
|
|
558
|
+
this._closeController.signal.addEventListener("abort", abortListener);
|
|
559
|
+
};
|
|
560
|
+
const abortListener = () => {
|
|
561
|
+
removeListeners();
|
|
562
|
+
clearTimeout(timer);
|
|
563
|
+
res(false);
|
|
564
|
+
};
|
|
565
|
+
const timer = setTimeout(() => {
|
|
566
|
+
removeListeners();
|
|
567
|
+
res(false);
|
|
568
|
+
}, timeout);
|
|
569
|
+
const check = () => this.isLeader(slot, numberOfLeaders).then((isLeader) => {
|
|
570
|
+
if (isLeader) {
|
|
571
|
+
removeListeners();
|
|
572
|
+
clearTimeout(timer);
|
|
573
|
+
res(isLeader);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
const roleListener = () => {
|
|
577
|
+
check();
|
|
578
|
+
};
|
|
579
|
+
this.events.addEventListener("role", roleListener);
|
|
580
|
+
this._closeController.signal.addEventListener("abort", abortListener);
|
|
581
|
+
check();
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
async findLeaders(subject, numberOfLeaders, options) {
|
|
403
585
|
// For a fixed set or members, the choosen leaders will always be the same (address invariant)
|
|
404
586
|
// This allows for that same content is always chosen to be distributed to same peers, to remove unecessary copies
|
|
405
|
-
const peers = this.getReplicatorsSorted() || [];
|
|
406
|
-
if (peers.length === 0) {
|
|
407
|
-
return [];
|
|
408
|
-
}
|
|
409
|
-
numberOfLeaders = Math.min(numberOfLeaders, peers.length);
|
|
410
587
|
// Convert this thing we wan't to distribute to 8 bytes so we get can convert it into a u64
|
|
411
588
|
// modulus into an index
|
|
412
589
|
const utf8writer = new BinaryWriter();
|
|
413
590
|
utf8writer.string(subject.toString());
|
|
414
591
|
const seed = await sha256(utf8writer.finalize());
|
|
415
592
|
// convert hash of slot to a number
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const
|
|
593
|
+
const cursor = hashToUniformNumber(seed); // bounded between 0 and 1
|
|
594
|
+
return this.findLeadersFromUniformNumber(cursor, numberOfLeaders, options);
|
|
595
|
+
}
|
|
596
|
+
findLeadersFromUniformNumber(cursor, numberOfLeaders, options) {
|
|
597
|
+
const leaders = new Set();
|
|
598
|
+
const width = 1; // this.getParticipationSum(roleAge);
|
|
599
|
+
const peers = this.getReplicatorsSorted();
|
|
600
|
+
if (!peers || peers?.length === 0) {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
numberOfLeaders = Math.min(numberOfLeaders, peers.length);
|
|
604
|
+
const t = +new Date();
|
|
605
|
+
const roleAge = options?.roleAge ??
|
|
606
|
+
Math.min(WAIT_FOR_ROLE_MATURITY, +new Date() - this.openTime);
|
|
422
607
|
for (let i = 0; i < numberOfLeaders; i++) {
|
|
423
|
-
|
|
608
|
+
let matured = 0;
|
|
609
|
+
const maybeIncrementMatured = (role) => {
|
|
610
|
+
if (t - Number(role.timestamp) > roleAge) {
|
|
611
|
+
matured++;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
const x = ((cursor + i / numberOfLeaders) % 1) * width;
|
|
615
|
+
let currentNode = peers.head;
|
|
616
|
+
const diffs = [];
|
|
617
|
+
while (currentNode) {
|
|
618
|
+
const start = currentNode.value.offset % width;
|
|
619
|
+
const absDelta = Math.abs(start - x);
|
|
620
|
+
const diff = Math.min(absDelta, width - absDelta);
|
|
621
|
+
if (diff < currentNode.value.role.factor / 2 + 0.00001) {
|
|
622
|
+
leaders.add(currentNode.value.publicKey.hashcode());
|
|
623
|
+
maybeIncrementMatured(currentNode.value.role);
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
diffs.push({
|
|
627
|
+
diff: currentNode.value.role.factor > 0
|
|
628
|
+
? diff / currentNode.value.role.factor
|
|
629
|
+
: Number.MAX_SAFE_INTEGER,
|
|
630
|
+
rect: currentNode.value
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
currentNode = currentNode.next;
|
|
634
|
+
}
|
|
635
|
+
if (matured === 0) {
|
|
636
|
+
diffs.sort((x, y) => x.diff - y.diff);
|
|
637
|
+
for (const node of diffs) {
|
|
638
|
+
leaders.add(node.rect.publicKey.hashcode());
|
|
639
|
+
maybeIncrementMatured(node.rect.role);
|
|
640
|
+
if (matured > 0) {
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
424
645
|
}
|
|
425
|
-
return leaders;
|
|
646
|
+
return [...leaders];
|
|
426
647
|
}
|
|
427
|
-
|
|
428
|
-
|
|
648
|
+
/**
|
|
649
|
+
*
|
|
650
|
+
* @returns groups where at least one in any group will have the entry you are looking for
|
|
651
|
+
*/
|
|
652
|
+
getReplicatorUnion(roleAge = WAIT_FOR_ROLE_MATURITY) {
|
|
653
|
+
// Total replication "width"
|
|
654
|
+
const width = 1; //this.getParticipationSum(roleAge);
|
|
655
|
+
// How much width you need to "query" to
|
|
656
|
+
const peers = this.getReplicatorsSorted(); // TODO types
|
|
657
|
+
const minReplicas = Math.min(peers.length, this.replicas.min.getValue(this));
|
|
658
|
+
const coveringWidth = width / minReplicas;
|
|
659
|
+
let walker = peers.head;
|
|
660
|
+
if (this.role instanceof Replicator) {
|
|
661
|
+
// start at our node (local first)
|
|
662
|
+
while (walker) {
|
|
663
|
+
if (walker.value.publicKey.equals(this.node.identity.publicKey)) {
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
walker = walker.next;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
const seed = Math.round(peers.length * Math.random()); // start at a random point
|
|
671
|
+
for (let i = 0; i < seed - 1; i++) {
|
|
672
|
+
if (walker?.next == null) {
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
walker = walker.next;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const set = [];
|
|
679
|
+
let distance = 0;
|
|
680
|
+
const startNode = walker;
|
|
681
|
+
if (!startNode) {
|
|
682
|
+
return [];
|
|
683
|
+
}
|
|
684
|
+
let nextPoint = startNode.value.offset;
|
|
685
|
+
const t = +new Date();
|
|
686
|
+
while (walker && distance < coveringWidth) {
|
|
687
|
+
const absDelta = Math.abs(walker.value.offset - nextPoint);
|
|
688
|
+
const diff = Math.min(absDelta, width - absDelta);
|
|
689
|
+
if (diff < walker.value.role.factor / 2 + 0.00001) {
|
|
690
|
+
set.push(walker.value.publicKey.hashcode());
|
|
691
|
+
if (t - Number(walker.value.role.timestamp) >
|
|
692
|
+
roleAge /* ||
|
|
693
|
+
walker!.value.publicKey.equals(this.node.identity.publicKey)) */) {
|
|
694
|
+
nextPoint = (nextPoint + walker.value.role.factor) % 1;
|
|
695
|
+
distance += walker.value.role.factor;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
walker = walker.next || peers.head;
|
|
699
|
+
if (walker?.value.publicKey &&
|
|
700
|
+
startNode?.value.publicKey.equals(walker?.value.publicKey)) {
|
|
701
|
+
break; // TODO throw error for failing to fetch ffull width
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return set;
|
|
705
|
+
}
|
|
706
|
+
async replicator(entry, options) {
|
|
707
|
+
return this.isLeader(entry.gid, decodeReplicas(entry).getValue(this), options);
|
|
708
|
+
}
|
|
709
|
+
onRoleChange(prev, role, publicKey) {
|
|
710
|
+
if (this.closed) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
this.distribute();
|
|
714
|
+
if (role instanceof Replicator) {
|
|
715
|
+
const timer = setTimeout(async () => {
|
|
716
|
+
this._closeController.signal.removeEventListener("abort", listener);
|
|
717
|
+
await this.rebalanceParticipationDebounced?.();
|
|
718
|
+
this.distribute();
|
|
719
|
+
}, WAIT_FOR_ROLE_MATURITY + 2000);
|
|
720
|
+
const listener = () => {
|
|
721
|
+
clearTimeout(timer);
|
|
722
|
+
};
|
|
723
|
+
this._closeController.signal.addEventListener("abort", listener);
|
|
724
|
+
}
|
|
725
|
+
this.events.dispatchEvent(new CustomEvent("role", {
|
|
726
|
+
detail: { publicKey, role }
|
|
727
|
+
}));
|
|
728
|
+
}
|
|
729
|
+
async modifyReplicators(role, publicKey) {
|
|
730
|
+
const { prev, changed } = await this._modifyReplicators(role, publicKey);
|
|
731
|
+
if (changed) {
|
|
732
|
+
await this.rebalanceParticipationDebounced?.(); // await this.rebalanceParticipation(false);
|
|
733
|
+
this.onRoleChange(prev, role, publicKey);
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
async _modifyReplicators(role, publicKey) {
|
|
739
|
+
if (role instanceof Replicator &&
|
|
429
740
|
this._canReplicate &&
|
|
430
|
-
!(await this._canReplicate(publicKey))) {
|
|
431
|
-
return false;
|
|
741
|
+
!(await this._canReplicate(publicKey, role))) {
|
|
742
|
+
return { changed: false };
|
|
432
743
|
}
|
|
433
744
|
const sortedPeer = this._sortedPeersCache;
|
|
434
745
|
if (!sortedPeer) {
|
|
435
746
|
if (this.closed === false) {
|
|
436
747
|
throw new Error("Unexpected, sortedPeersCache is undefined");
|
|
437
748
|
}
|
|
438
|
-
return false;
|
|
749
|
+
return { changed: false };
|
|
439
750
|
}
|
|
440
|
-
|
|
441
|
-
if (subscribed) {
|
|
751
|
+
if (role instanceof Replicator && role.factor > 0) {
|
|
442
752
|
// TODO use Set + list for fast lookup
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
753
|
+
// check also that peer is online
|
|
754
|
+
const isOnline = this.node.identity.publicKey.equals(publicKey) ||
|
|
755
|
+
(await this.waitFor(publicKey, { signal: this._closeController.signal })
|
|
756
|
+
.then(() => true)
|
|
757
|
+
.catch(() => false));
|
|
758
|
+
if (isOnline) {
|
|
759
|
+
// insert or if already there do nothing
|
|
760
|
+
const code = hashToUniformNumber(publicKey.bytes);
|
|
761
|
+
const rect = {
|
|
762
|
+
publicKey,
|
|
763
|
+
offset: code,
|
|
764
|
+
role
|
|
765
|
+
};
|
|
766
|
+
let currentNode = sortedPeer.head;
|
|
767
|
+
if (!currentNode) {
|
|
768
|
+
sortedPeer.push(rect);
|
|
769
|
+
this._totalParticipation += rect.role.factor;
|
|
770
|
+
return { changed: true };
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
while (currentNode) {
|
|
774
|
+
if (currentNode.value.publicKey.equals(publicKey)) {
|
|
775
|
+
// update the value
|
|
776
|
+
// rect.timestamp = currentNode.value.timestamp;
|
|
777
|
+
const prev = currentNode.value;
|
|
778
|
+
currentNode.value = rect;
|
|
779
|
+
this._totalParticipation += rect.role.factor;
|
|
780
|
+
this._totalParticipation -= prev.role.factor;
|
|
781
|
+
// TODO change detection and only do change stuff if diff?
|
|
782
|
+
return { prev: prev.role, changed: true };
|
|
783
|
+
}
|
|
784
|
+
if (code > currentNode.value.offset) {
|
|
785
|
+
const next = currentNode?.next;
|
|
786
|
+
if (next) {
|
|
787
|
+
currentNode = next;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
currentNode = currentNode.prev;
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const prev = currentNode;
|
|
800
|
+
if (!prev?.next?.value.publicKey.equals(publicKey)) {
|
|
801
|
+
this._totalParticipation += rect.role.factor;
|
|
802
|
+
_insertAfter(sortedPeer, prev || undefined, rect);
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
throw new Error("Unexpected");
|
|
806
|
+
}
|
|
807
|
+
return { changed: true };
|
|
808
|
+
}
|
|
447
809
|
}
|
|
448
810
|
else {
|
|
449
|
-
return false;
|
|
811
|
+
return { changed: false };
|
|
450
812
|
}
|
|
451
813
|
}
|
|
452
814
|
else {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
815
|
+
let currentNode = sortedPeer.head;
|
|
816
|
+
while (currentNode) {
|
|
817
|
+
if (currentNode.value.publicKey.equals(publicKey)) {
|
|
818
|
+
sortedPeer.removeNode(currentNode);
|
|
819
|
+
this._totalParticipation -= currentNode.value.role.factor;
|
|
820
|
+
return { prev: currentNode.value.role, changed: true };
|
|
821
|
+
}
|
|
822
|
+
currentNode = currentNode.next;
|
|
460
823
|
}
|
|
824
|
+
return { changed: false };
|
|
461
825
|
}
|
|
462
826
|
}
|
|
463
827
|
async handleSubscriptionChange(publicKey, changes, subscribed) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
if (!subscription.data ||
|
|
471
|
-
!startsWith(subscription.data, REPLICATOR_TYPE_VARIANT)) {
|
|
472
|
-
prev.push(await this.modifySortedSubscriptionCache(false, publicKey));
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
this._lastSubscriptionMessageId += 1;
|
|
477
|
-
prev.push(await this.modifySortedSubscriptionCache(subscribed, publicKey));
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
// TODO don't do this i fnot is replicator?
|
|
481
|
-
for (const [i, subscription] of changes.entries()) {
|
|
482
|
-
if (this.log.idString !== subscription.topic) {
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
if (subscription.data) {
|
|
486
|
-
try {
|
|
487
|
-
const type = deserialize(subscription.data, Role);
|
|
488
|
-
// Reorganize if the new subscriber is a replicator, or observers AND was replicator
|
|
489
|
-
if (type instanceof Replicator || prev[i]) {
|
|
490
|
-
await this.replicationReorganization();
|
|
828
|
+
if (subscribed) {
|
|
829
|
+
if (this.role instanceof Replicator) {
|
|
830
|
+
for (const subscription of changes) {
|
|
831
|
+
if (this.log.idString !== subscription) {
|
|
832
|
+
continue;
|
|
491
833
|
}
|
|
834
|
+
this.rpc
|
|
835
|
+
.send(new ResponseRoleMessage(this.role), {
|
|
836
|
+
mode: new AcknowledgeDelivery({ redundancy: 1, to: [publicKey] })
|
|
837
|
+
})
|
|
838
|
+
.catch((e) => logger.error(e.toString()));
|
|
492
839
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
840
|
+
}
|
|
841
|
+
//if(evt.detail.subscriptions.map((x) => x.topic).includes())
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
for (const topic of changes) {
|
|
845
|
+
if (this.log.idString !== topic) {
|
|
846
|
+
continue;
|
|
498
847
|
}
|
|
848
|
+
await this.modifyReplicators(new Observer(), publicKey);
|
|
499
849
|
}
|
|
500
850
|
}
|
|
501
851
|
}
|
|
502
|
-
|
|
852
|
+
async prune(entries, options) {
|
|
853
|
+
if (options?.unchecked) {
|
|
854
|
+
return Promise.all(entries.map((x) => this.log.remove(x, {
|
|
855
|
+
recursively: true
|
|
856
|
+
})));
|
|
857
|
+
}
|
|
503
858
|
// ask network if they have they entry,
|
|
504
859
|
// so I can delete it
|
|
505
860
|
// There is a few reasons why we might end up here
|
|
@@ -510,12 +865,16 @@ let SharedLog = class SharedLog extends Program {
|
|
|
510
865
|
const filteredEntries = [];
|
|
511
866
|
for (const entry of entries) {
|
|
512
867
|
const pendingPrev = this._pendingDeletes.get(entry.hash);
|
|
868
|
+
if (pendingPrev) {
|
|
869
|
+
promises.push(pendingPrev.promise.promise);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
513
872
|
filteredEntries.push(entry);
|
|
514
873
|
const existCounter = new Set();
|
|
515
874
|
const minReplicas = decodeReplicas(entry);
|
|
516
875
|
const deferredPromise = pDefer();
|
|
517
876
|
const clear = () => {
|
|
518
|
-
pendingPrev?.clear();
|
|
877
|
+
//pendingPrev?.clear();
|
|
519
878
|
const pending = this._pendingDeletes.get(entry.hash);
|
|
520
879
|
if (pending?.promise == deferredPromise) {
|
|
521
880
|
this._pendingDeletes.delete(entry.hash);
|
|
@@ -531,7 +890,7 @@ let SharedLog = class SharedLog extends Program {
|
|
|
531
890
|
deferredPromise.reject(e);
|
|
532
891
|
};
|
|
533
892
|
const timeout = setTimeout(() => {
|
|
534
|
-
reject(new Error("Timeout"));
|
|
893
|
+
reject(new Error("Timeout for checked pruning"));
|
|
535
894
|
}, options?.timeout ?? 10 * 1000);
|
|
536
895
|
this._pendingDeletes.set(entry.hash, {
|
|
537
896
|
promise: deferredPromise,
|
|
@@ -540,10 +899,17 @@ let SharedLog = class SharedLog extends Program {
|
|
|
540
899
|
},
|
|
541
900
|
callback: async (publicKeyHash) => {
|
|
542
901
|
const minReplicasValue = minReplicas.getValue(this);
|
|
543
|
-
const
|
|
544
|
-
|
|
902
|
+
const minMinReplicasValue = this.replicas.max
|
|
903
|
+
? Math.min(minReplicasValue, this.replicas.max.getValue(this))
|
|
904
|
+
: minReplicasValue;
|
|
905
|
+
const leaders = await this.findLeaders(entry.gid, minMinReplicasValue);
|
|
906
|
+
if (leaders.find((x) => x === this.node.identity.publicKey.hashcode())) {
|
|
907
|
+
reject(new Error("Failed to delete, is leader"));
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (leaders.find((x) => x === publicKeyHash)) {
|
|
545
911
|
existCounter.add(publicKeyHash);
|
|
546
|
-
if (
|
|
912
|
+
if (minMinReplicasValue <= existCounter.size) {
|
|
547
913
|
this.log
|
|
548
914
|
.remove(entry, {
|
|
549
915
|
recursively: true
|
|
@@ -560,118 +926,173 @@ let SharedLog = class SharedLog extends Program {
|
|
|
560
926
|
});
|
|
561
927
|
promises.push(deferredPromise.promise);
|
|
562
928
|
}
|
|
563
|
-
if (filteredEntries.length
|
|
564
|
-
|
|
929
|
+
if (filteredEntries.length == 0) {
|
|
930
|
+
return;
|
|
565
931
|
}
|
|
566
|
-
|
|
932
|
+
this.rpc.send(new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) }));
|
|
933
|
+
const onNewPeer = async (e) => {
|
|
934
|
+
if (e.detail.role instanceof Replicator) {
|
|
935
|
+
await this.rpc.send(new RequestIHave({ hashes: filteredEntries.map((x) => x.hash) }), {
|
|
936
|
+
to: [e.detail.publicKey.hashcode()]
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
// check joining peers
|
|
941
|
+
this.events.addEventListener("role", onNewPeer);
|
|
942
|
+
return Promise.all(promises).finally(() => this.events.removeEventListener("role", onNewPeer));
|
|
567
943
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
944
|
+
async distribute() {
|
|
945
|
+
/**
|
|
946
|
+
* TODO use information of new joined/leaving peer to create a subset of heads
|
|
947
|
+
* that we potentially need to share with other peers
|
|
948
|
+
*/
|
|
949
|
+
if (this.closed) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
574
952
|
const changed = false;
|
|
953
|
+
await this.log.trim();
|
|
575
954
|
const heads = await this.log.getHeads();
|
|
576
955
|
const groupedByGid = await groupByGid(heads);
|
|
577
|
-
|
|
956
|
+
const toDeliver = new Map();
|
|
957
|
+
const allEntriesToDelete = [];
|
|
578
958
|
for (const [gid, entries] of groupedByGid) {
|
|
579
|
-
|
|
580
|
-
|
|
959
|
+
if (this.closed) {
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
581
962
|
if (entries.length === 0) {
|
|
582
963
|
continue; // TODO maybe close store?
|
|
583
964
|
}
|
|
584
965
|
const oldPeersSet = this._gidPeersHistory.get(gid);
|
|
585
966
|
const currentPeers = await this.findLeaders(gid, maxReplicas(this, entries) // pick max replication policy of all entries, so all information is treated equally important as the most important
|
|
586
967
|
);
|
|
968
|
+
const currentPeersSet = new Set(currentPeers);
|
|
969
|
+
this._gidPeersHistory.set(gid, currentPeersSet);
|
|
587
970
|
for (const currentPeer of currentPeers) {
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
|
|
971
|
+
if (currentPeer == this.node.identity.publicKey.hashcode()) {
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
if (!oldPeersSet?.has(currentPeer)) {
|
|
591
975
|
// second condition means that if the new peer is us, we should not do anything, since we are expecting to receive heads, not send
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
logger.debug(`${this.node.identity.publicKey.hashcode()}: Exchange heads ${entries.length === 1 ? entries[0].hash : "#" + entries.length} on rebalance`);
|
|
597
|
-
for (const entry of entries) {
|
|
598
|
-
toSend.set(entry.hash, entry);
|
|
599
|
-
}
|
|
976
|
+
let arr = toDeliver.get(currentPeer);
|
|
977
|
+
if (!arr) {
|
|
978
|
+
arr = [];
|
|
979
|
+
toDeliver.set(currentPeer, arr);
|
|
600
980
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
logger.error("Missing channel when reorg to peer: " + currentPeer.toString());
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
throw error;
|
|
981
|
+
for (const entry of entries) {
|
|
982
|
+
arr.push(entry);
|
|
607
983
|
}
|
|
608
984
|
}
|
|
609
985
|
}
|
|
610
|
-
// We don't need this clause anymore because we got the trim option!
|
|
611
986
|
if (!currentPeers.find((x) => x === this.node.identity.publicKey.hashcode())) {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
//
|
|
615
|
-
entriesToDelete =
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
Promise.all(this.pruneSafely(entriesToDelete)).catch((e) => {
|
|
621
|
-
logger.error(e.toString());
|
|
622
|
-
});
|
|
987
|
+
if (currentPeers.length > 0) {
|
|
988
|
+
// If we are observer, never prune locally created entries, since we dont really know who can store them
|
|
989
|
+
// if we are replicator, we will always persist entries that we need to so filtering on createdLocally will not make a difference
|
|
990
|
+
const entriesToDelete = this._role instanceof Observer
|
|
991
|
+
? entries.filter((e) => !e.createdLocally)
|
|
992
|
+
: entries;
|
|
993
|
+
entriesToDelete.map((x) => this._gidPeersHistory.delete(x.meta.gid));
|
|
994
|
+
allEntriesToDelete.push(...entriesToDelete);
|
|
623
995
|
}
|
|
624
|
-
// TODO if length === 0 maybe close store?
|
|
625
996
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
997
|
+
else {
|
|
998
|
+
for (const entry of entries) {
|
|
999
|
+
this._pendingDeletes
|
|
1000
|
+
.get(entry.hash)
|
|
1001
|
+
?.promise.reject(new Error("Failed to delete, is leader: " +
|
|
1002
|
+
this.role.constructor.name +
|
|
1003
|
+
". " +
|
|
1004
|
+
this.node.identity.publicKey.hashcode()));
|
|
1005
|
+
}
|
|
629
1006
|
}
|
|
630
|
-
|
|
1007
|
+
}
|
|
1008
|
+
for (const [target, entries] of toDeliver) {
|
|
1009
|
+
const message = await createExchangeHeadsMessage(this.log, entries, // TODO send to peers directly
|
|
631
1010
|
this._gidParentCache);
|
|
632
1011
|
// TODO perhaps send less messages to more receivers for performance reasons?
|
|
633
1012
|
await this.rpc.send(message, {
|
|
634
|
-
to:
|
|
635
|
-
strict: true
|
|
1013
|
+
to: [target]
|
|
636
1014
|
});
|
|
637
1015
|
}
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
/**
|
|
644
|
-
*
|
|
645
|
-
* @returns groups where at least one in any group will have the entry you are looking for
|
|
646
|
-
*/
|
|
647
|
-
getDiscoveryGroups() {
|
|
648
|
-
// TODO Optimize this so we don't have to recreate the array all the time!
|
|
649
|
-
const minReplicas = this.replicas.min.getValue(this);
|
|
650
|
-
const replicators = this.getReplicatorsSorted();
|
|
651
|
-
if (!replicators) {
|
|
652
|
-
return []; // No subscribers and we are not replicating
|
|
653
|
-
}
|
|
654
|
-
const numberOfGroups = Math.min(Math.ceil(replicators.length / minReplicas));
|
|
655
|
-
const groups = new Array(numberOfGroups);
|
|
656
|
-
for (let i = 0; i < groups.length; i++) {
|
|
657
|
-
groups[i] = [];
|
|
658
|
-
}
|
|
659
|
-
for (let i = 0; i < replicators.length; i++) {
|
|
660
|
-
groups[i % numberOfGroups].push(replicators[i]);
|
|
1016
|
+
if (allEntriesToDelete.length > 0) {
|
|
1017
|
+
this.prune(allEntriesToDelete).catch((e) => {
|
|
1018
|
+
logger.error(e.toString());
|
|
1019
|
+
});
|
|
661
1020
|
}
|
|
662
|
-
return
|
|
663
|
-
}
|
|
664
|
-
async replicator(entry) {
|
|
665
|
-
return this.isLeader(entry.gid, decodeReplicas(entry).getValue(this));
|
|
1021
|
+
return changed;
|
|
666
1022
|
}
|
|
667
1023
|
async _onUnsubscription(evt) {
|
|
668
|
-
logger.debug(`Peer disconnected '${evt.detail.from.hashcode()}' from '${JSON.stringify(evt.detail.unsubscriptions.map((x) => x
|
|
1024
|
+
logger.debug(`Peer disconnected '${evt.detail.from.hashcode()}' from '${JSON.stringify(evt.detail.unsubscriptions.map((x) => x))}'`);
|
|
1025
|
+
this.latestRoleMessages.delete(evt.detail.from.hashcode());
|
|
1026
|
+
this.events.dispatchEvent(new CustomEvent("role", {
|
|
1027
|
+
detail: { publicKey: evt.detail.from, role: new Observer() }
|
|
1028
|
+
}));
|
|
669
1029
|
return this.handleSubscriptionChange(evt.detail.from, evt.detail.unsubscriptions, false);
|
|
670
1030
|
}
|
|
671
1031
|
async _onSubscription(evt) {
|
|
672
|
-
logger.debug(`New peer '${evt.detail.from.hashcode()}' connected to '${JSON.stringify(evt.detail.subscriptions.map((x) => x
|
|
1032
|
+
logger.debug(`New peer '${evt.detail.from.hashcode()}' connected to '${JSON.stringify(evt.detail.subscriptions.map((x) => x))}'`);
|
|
1033
|
+
this.remoteBlocks.onReachable(evt.detail.from);
|
|
673
1034
|
return this.handleSubscriptionChange(evt.detail.from, evt.detail.subscriptions, true);
|
|
674
1035
|
}
|
|
1036
|
+
replicationController;
|
|
1037
|
+
history;
|
|
1038
|
+
async addToHistory(usedMemory, factor) {
|
|
1039
|
+
(this.history || (this.history = [])).push({ usedMemory, factor });
|
|
1040
|
+
// Keep only the last N entries in the history array (you can adjust N based on your needs)
|
|
1041
|
+
const maxHistoryLength = 10;
|
|
1042
|
+
if (this.history.length > maxHistoryLength) {
|
|
1043
|
+
this.history.shift();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
async calculateTrend() {
|
|
1047
|
+
// Calculate the average change in factor per unit change in memory usage
|
|
1048
|
+
const factorChanges = this.history.map((entry, index) => {
|
|
1049
|
+
if (index > 0) {
|
|
1050
|
+
const memoryChange = entry.usedMemory - this.history[index - 1].usedMemory;
|
|
1051
|
+
if (memoryChange !== 0) {
|
|
1052
|
+
const factorChange = entry.factor - this.history[index - 1].factor;
|
|
1053
|
+
return factorChange / memoryChange;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return 0;
|
|
1057
|
+
});
|
|
1058
|
+
// Return the average factor change per unit memory change
|
|
1059
|
+
return (factorChanges.reduce((sum, change) => sum + change, 0) /
|
|
1060
|
+
factorChanges.length);
|
|
1061
|
+
}
|
|
1062
|
+
async rebalanceParticipation(onRoleChange = true) {
|
|
1063
|
+
// update more participation rate to converge to the average expected rate or bounded by
|
|
1064
|
+
// resources such as memory and or cpu
|
|
1065
|
+
if (this.closed) {
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
// The role is fixed (no changes depending on memory usage or peer count etc)
|
|
1069
|
+
if (this._roleOptions instanceof Role) {
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
// TODO second condition: what if the current role is Observer?
|
|
1073
|
+
if (this._roleOptions.type == "replicator" &&
|
|
1074
|
+
this._role instanceof Replicator) {
|
|
1075
|
+
const peers = this.getReplicatorsSorted();
|
|
1076
|
+
const usedMemory = await this.getMemoryUsage();
|
|
1077
|
+
const newFactor = await this.replicationController.adjustReplicationFactor(usedMemory, this._role.factor, this._totalParticipation, peers?.length || 1);
|
|
1078
|
+
const newRole = new Replicator({
|
|
1079
|
+
factor: newFactor,
|
|
1080
|
+
timestamp: this._role.timestamp
|
|
1081
|
+
});
|
|
1082
|
+
const relativeDifference = Math.abs(this._role.factor - newRole.factor) / this._role.factor;
|
|
1083
|
+
if (relativeDifference > 0.0001) {
|
|
1084
|
+
const canReplicate = !this._canReplicate ||
|
|
1085
|
+
(await this._canReplicate(this.node.identity.publicKey, newRole));
|
|
1086
|
+
if (!canReplicate) {
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
await this._updateRole(newRole, onRoleChange);
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
675
1096
|
};
|
|
676
1097
|
__decorate([
|
|
677
1098
|
field({ type: Log }),
|
|
@@ -686,4 +1107,19 @@ SharedLog = __decorate([
|
|
|
686
1107
|
__metadata("design:paramtypes", [Object])
|
|
687
1108
|
], SharedLog);
|
|
688
1109
|
export { SharedLog };
|
|
1110
|
+
function _insertAfter(self, node, value) {
|
|
1111
|
+
const inserted = !node
|
|
1112
|
+
? new yallist.Node(value, null, self.head, self)
|
|
1113
|
+
: new yallist.Node(value, node, node.next, self);
|
|
1114
|
+
// is tail
|
|
1115
|
+
if (inserted.next === null) {
|
|
1116
|
+
self.tail = inserted;
|
|
1117
|
+
}
|
|
1118
|
+
// is head
|
|
1119
|
+
if (inserted.prev === null) {
|
|
1120
|
+
self.head = inserted;
|
|
1121
|
+
}
|
|
1122
|
+
self.length++;
|
|
1123
|
+
return inserted;
|
|
1124
|
+
}
|
|
689
1125
|
//# sourceMappingURL=index.js.map
|