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