@yz-social/kdht 0.1.7 → 0.1.9
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/dht/kbucket.js +2 -2
- package/dht/node.js +1 -2
- package/dht/nodeContacts.js +20 -24
- package/dht/nodeMessages.js +53 -19
- package/dht/nodeProbe.js +6 -5
- package/dht/nodeRefresh.js +5 -11
- package/dht/nodeStorage.js +2 -1
- package/dht/nodeTransports.js +5 -4
- package/dht/nodeUtilities.js +1 -1
- package/package.json +3 -3
- package/portals/node.js +1 -1
- package/spec/bots.js +3 -2
- package/spec/dhtAcceptanceSpec.js +20 -49
- package/spec/dhtImplementation.js +81 -32
- package/spec/dhtInternalsSpec.js +15 -14
- package/spec/{dhtWriteReadSpec.js → webrtcTests.js} +3 -3
- package/transports/contact.js +93 -12
- package/transports/simulations.js +51 -82
- package/transports/webrtc.js +8 -57
- package/transports/inProcessWebrtc.js +0 -70
package/dht/kbucket.js
CHANGED
|
@@ -59,7 +59,7 @@ export class KBucket {
|
|
|
59
59
|
return false;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
addContact(contact) { // Returns 'present' or 'added' if it was added to end within capacity, else false.
|
|
63
63
|
// Resets refresh timer.
|
|
64
64
|
this.node.constructor.assert(contact.node.key !== this.node.key, 'attempt to add self contact to bucket');
|
|
65
65
|
let added = this.removeKey(contact.key, false) || 'added';
|
|
@@ -67,7 +67,7 @@ export class KBucket {
|
|
|
67
67
|
if (this.isFull) {
|
|
68
68
|
if (added === 'present') this.node.looseTransports.push(contact); // So no findContact will fail during ping. Should we instead serialize findContact?
|
|
69
69
|
const head = this.contacts[0];
|
|
70
|
-
if (
|
|
70
|
+
if (head.connection) { // still alive
|
|
71
71
|
added = false; // New contact will not be added.
|
|
72
72
|
contact = head; // Add head back, below.
|
|
73
73
|
}
|
package/dht/node.js
CHANGED
|
@@ -109,12 +109,11 @@ export class Node extends NodeProbe {
|
|
|
109
109
|
this.ilog('joining', contact.sname);
|
|
110
110
|
contact = this.ensureContact(contact);
|
|
111
111
|
await contact.connect();
|
|
112
|
-
|
|
112
|
+
this.addToRoutingTable(contact);
|
|
113
113
|
await this.locateNodes(this.key); // Discovers between us and otherNode.
|
|
114
114
|
|
|
115
115
|
// Refresh every bucket farther out than our closest neighbor.
|
|
116
116
|
// I think(?) that this can be done by refreshing "just" the farthest bucket:
|
|
117
|
-
//this.ensureBucket(this.constructor.keySize - 1).resetRefresh();
|
|
118
117
|
await this.ensureBucket(this.constructor.keySize - 1).refresh();
|
|
119
118
|
// But if it turns out to be necessary to explicitly refresh each next bucket in turn, this is how:
|
|
120
119
|
// let started = false;
|
package/dht/nodeContacts.js
CHANGED
|
@@ -56,6 +56,10 @@ export class NodeContacts extends NodeTransports {
|
|
|
56
56
|
});
|
|
57
57
|
return contacts;
|
|
58
58
|
}
|
|
59
|
+
get connections() {
|
|
60
|
+
return this.contacts.filter(contact => contact.connection)
|
|
61
|
+
.concat(this.looseTransports.filter(contact => contact.connection));
|
|
62
|
+
}
|
|
59
63
|
contactDictionary = {}; // maps name => contact for lifetime of Node instance until removeContact.
|
|
60
64
|
existingContact(name) { // Returns contact with the given name for this node, without searching buckets or looseTransports.
|
|
61
65
|
return this.contactDictionary[name];
|
|
@@ -80,20 +84,14 @@ export class NodeContacts extends NodeTransports {
|
|
|
80
84
|
if (sponsor) contact.noteSponsor(sponsor);
|
|
81
85
|
return contact;
|
|
82
86
|
}
|
|
83
|
-
routingTableSerializer = Promise.resolve();
|
|
84
|
-
queueRoutingTableChange(thunk) { // Promise to resolve thunk() -- after all previous queued thunks have resolved.
|
|
85
|
-
return this.routingTableSerializer = this.routingTableSerializer.then(thunk);
|
|
86
|
-
}
|
|
87
87
|
removeContact(contact) { // Removes from node entirely if present, from looseTransports or bucket as necessary, returning bucket if that's where it was, else null.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return bucket?.removeKey(key) ? bucket : null;
|
|
96
|
-
});
|
|
88
|
+
delete this.contactDictionary[contact.name];
|
|
89
|
+
const key = contact.key;
|
|
90
|
+
if (this.removeLooseTransport(key)) return null;
|
|
91
|
+
const bucketIndex = this.getBucketIndex(key);
|
|
92
|
+
const bucket = this.routingTable.get(bucketIndex);
|
|
93
|
+
// Host might not yet have added node or anyone else as contact for that bucket yet, so maybe no bucket.
|
|
94
|
+
return bucket?.removeKey(key) ? bucket : null;
|
|
97
95
|
}
|
|
98
96
|
addToRoutingTable(contact) { // Promise contact, and add it to the routing table if room.
|
|
99
97
|
if (contact.key === this.key) return null; // Do not add self.
|
|
@@ -102,18 +100,16 @@ export class NodeContacts extends NodeTransports {
|
|
|
102
100
|
// we have already dropped the connection.
|
|
103
101
|
//this.constructor.assert(contact.connection, 'Adding contact without connection', contact.report, 'in', this.contact.report);
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const bucket = this.ensureBucket(bucketIndex);
|
|
103
|
+
const bucketIndex = this.getBucketIndex(contact.key);
|
|
104
|
+
const bucket = this.ensureBucket(bucketIndex);
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
});
|
|
106
|
+
// Try to add to bucket
|
|
107
|
+
const added = bucket.addContact(contact);
|
|
108
|
+
if (added !== 'present') { // Not already tracked in bucket.
|
|
109
|
+
this.removeLooseTransport(contact.key); // Can't be in two places.
|
|
110
|
+
this.replicateCloserStorage(contact); // Asynchronous, but don't wait for it here.
|
|
111
|
+
}
|
|
112
|
+
return added;
|
|
117
113
|
}
|
|
118
114
|
findClosestHelpers(targetKey, count = this.constructor.k) { // Answer count closest Helpers to targetKey, including ourself.
|
|
119
115
|
if (!this.contact) return []; // Can happen while we are shutting down during a probe.
|
package/dht/nodeMessages.js
CHANGED
|
@@ -25,30 +25,65 @@ export class NodeMessages extends NodeContacts {
|
|
|
25
25
|
if (value !== undefined) return {value};
|
|
26
26
|
return this.findClosestHelpers(key);
|
|
27
27
|
}
|
|
28
|
-
async signals(key, signals, forwardingExclusions =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
async signals(key, signals, forwardingExclusions = null, targetNameForDebugging) {
|
|
29
|
+
// Handle an exchange of signals, with a response that may include {result, forwardingExclusions}. See code.
|
|
30
|
+
|
|
31
|
+
if (!this.isRunning) { // In case it happens in simulations.
|
|
32
|
+
//this.xlog('\n*** not running ***');
|
|
33
|
+
return null; //{forwardingExclusions}; // FIXME
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// If the key is us, pass the signals to our home contact and respond with the WebRTC signals from the contact.
|
|
37
|
+
// (Subtle: the signals will contain the sender name. The handler in our home contact will create
|
|
38
|
+
// a new specific contact if necessary, and set up the WebRTC through that.)
|
|
39
|
+
// The forwardingExclusions are passed back, in case the sender wants to see the steps involved.
|
|
40
|
+
if (this.key === key) return {result: await this.contact.signals(...signals), forwardingExclusions};
|
|
41
|
+
|
|
42
|
+
// If we have a direct connection to the key, pass it on and answer what it tells us.
|
|
43
|
+
// (E.g., if we sponsored target for sender, we will have a direct connection that will answer as above.)
|
|
44
|
+
let contact = this.findContactByKey(key);
|
|
45
|
+
if (contact && contact.connection) {
|
|
46
|
+
forwardingExclusions?.push(this.name); // Keeps stats accurate if sender is examining paths.
|
|
47
|
+
const response = await contact.sendRPC('signals', key, signals, forwardingExclusions, targetNameForDebugging);
|
|
48
|
+
if (response) return response;
|
|
49
|
+
return {forwardingExclusions}; // Subtle: If it fails, return a definitive failure instead of just null.
|
|
50
|
+
}
|
|
34
51
|
|
|
35
|
-
let contact = this.findContactByKey(key); // If we have the target as a contact, use it directly.
|
|
36
|
-
if (!contact && !forwardingExclusions) return null;
|
|
37
|
-
if (contact) return await contact.sendRPC('signals', key, signals, forwardingExclusions);
|
|
38
52
|
// Forward recursively.
|
|
39
|
-
|
|
53
|
+
if (forwardingExclusions) return await this.recursiveSignals(key, signals, forwardingExclusions, Contact.forwardingTimeoutMS, targetNameForDebugging);
|
|
54
|
+
|
|
55
|
+
// We were a sponsor but for a contact has since disconnected. We do not know if they are still connected to others.
|
|
56
|
+
//this.xlog('\n*** sponsored disconnected ***');
|
|
57
|
+
return {forwardingExclusions}; // FIXME: Is this definitively right, or should we answer null here?
|
|
58
|
+
}
|
|
59
|
+
static maxTries = Math.pow(this.alpha, 3); // alpha tries at each of three deep, or equivalent.
|
|
60
|
+
async recursiveSignals(key, signals, forwardingExclusions, expiration, targetNameForDebugging) { // Forward recursively.
|
|
61
|
+
// The target key may not be reachable from here (and might not even still be running).
|
|
62
|
+
// So bound our branching.
|
|
63
|
+
if (Date.now() > expiration) return null;
|
|
64
|
+
let remainingThisNode = this.constructor.alpha; // If it's good enough for probing, then it's good enough here.
|
|
65
|
+
if (forwardingExclusions.length > this.constructor.maxTries) {
|
|
66
|
+
this.xlog('abandoning wandering path towards', targetNameForDebugging, 'through', forwardingExclusions.join(', '));
|
|
67
|
+
return {forwardingExclusions};
|
|
68
|
+
}
|
|
69
|
+
const helpers = this.findClosestHelpers(key);
|
|
70
|
+
const contacts = helpers.map(helper => helper.contact);
|
|
40
71
|
forwardingExclusions.push(this.name);
|
|
41
|
-
|
|
72
|
+
|
|
42
73
|
for (const contact of contacts) {
|
|
43
|
-
|
|
44
|
-
// contact.connection ? 'connected': 'unconnected',
|
|
45
|
-
// forwardingExclusions.includes(contact.name) ? 'excluded' : 'allowed');
|
|
74
|
+
if (!remainingThisNode--) break;
|
|
46
75
|
if (!contact.isRunning) continue;
|
|
47
76
|
if (!contact.connection) continue;
|
|
48
77
|
if (forwardingExclusions.includes(contact.name)) continue;
|
|
49
|
-
|
|
50
|
-
//this.xlog('
|
|
51
|
-
|
|
78
|
+
this.constructor.assert(contact.key !== this.key, 'forwarding through self');
|
|
79
|
+
//this.xlog('forwarding through', contact.sname);
|
|
80
|
+
const response = await contact.sendRPC('signals', key, signals, forwardingExclusions, targetNameForDebugging);
|
|
81
|
+
if (response) {
|
|
82
|
+
return response;
|
|
83
|
+
} else { // No response at all: continue with further calls that exclude contact.
|
|
84
|
+
//this.xlog('No forwarding response from', contact.sname, );
|
|
85
|
+
forwardingExclusions.push(contact.name);
|
|
86
|
+
}
|
|
52
87
|
}
|
|
53
88
|
return null;
|
|
54
89
|
}
|
|
@@ -58,8 +93,7 @@ export class NodeMessages extends NodeContacts {
|
|
|
58
93
|
this.constructor.assert(typeof(method)==='string', 'no method', method, sender, rest);
|
|
59
94
|
this.constructor.assert(sender instanceof Contact, 'no sender', method, sender, rest);
|
|
60
95
|
this.constructor.assert(sender.host.key === this.key, 'sender', sender.host.name, 'not on receiver', this.name);
|
|
61
|
-
//
|
|
62
|
-
this.addToRoutingTable(sender);
|
|
96
|
+
this.addToRoutingTable(sender); // sender exists, so add it to the routing table.
|
|
63
97
|
if (!(method in this)) {
|
|
64
98
|
this.xlog('Does not handle method', method);
|
|
65
99
|
return null;
|
package/dht/nodeProbe.js
CHANGED
|
@@ -18,10 +18,10 @@ export class NodeProbe extends NodeMessages {
|
|
|
18
18
|
if (!results) { // disconnected
|
|
19
19
|
if (trace) this.log(helper.name, '=> disconnected');
|
|
20
20
|
this.log('removing unconnected contact', contact.sname);
|
|
21
|
-
|
|
21
|
+
this.removeContact(contact);
|
|
22
22
|
return null; // signal that there is *no* response from this contact - to distinguish from a response that confirms that the contact is alive, even if there are (after filtering) no new contacts to try.
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
this.addToRoutingTable(contact); // Live node, so update bucket.
|
|
25
25
|
// this.log('step added contact', contact.sname);
|
|
26
26
|
if (this.constructor.isContactsResult(results)) { // Keep only those that we have not seen, and note the new ones we have.
|
|
27
27
|
const rawResults = results;
|
|
@@ -55,6 +55,7 @@ export class NodeProbe extends NodeMessages {
|
|
|
55
55
|
if (trace) this.log(`iterate: key=${targetKey}, finder=${finder}, k=${k}`);
|
|
56
56
|
|
|
57
57
|
if (targetKey !== this.key) {
|
|
58
|
+
// Schedule a refresh for the targetKey's bucket.
|
|
58
59
|
const bucketIndex = this.getBucketIndex(targetKey);
|
|
59
60
|
const bucket = this.routingTable.get(bucketIndex);
|
|
60
61
|
// Subtle: if we don't have one now, but will after, refreshes will be rescheduled by KBucket constructor.
|
|
@@ -118,7 +119,7 @@ export class NodeProbe extends NodeMessages {
|
|
|
118
119
|
return null;
|
|
119
120
|
};
|
|
120
121
|
|
|
121
|
-
// Handler for when a request completes. result is only expected if status='responded'.
|
|
122
|
+
// Handler for when a request completes. a non-null result is only expected if status='responded'.
|
|
122
123
|
const handleCompletion = (helper, status, result) => {
|
|
123
124
|
if (iterationFinished) return; // too late
|
|
124
125
|
|
|
@@ -166,7 +167,7 @@ export class NodeProbe extends NodeMessages {
|
|
|
166
167
|
|
|
167
168
|
// Result is array of Helpers (may be empty if node had no new contacts)
|
|
168
169
|
// Merge new helpers into allNodesSeen and track progress
|
|
169
|
-
if (result
|
|
170
|
+
if (result.length > 0) {
|
|
170
171
|
allNodesSeen.push(...result);
|
|
171
172
|
allNodesSeen.sort(Helper.compare); // Keep sorted by distance (best-first).
|
|
172
173
|
responsesWithoutNewNodes = 0; // reset counter
|
|
@@ -226,7 +227,7 @@ export class NodeProbe extends NodeMessages {
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
this.step(targetKey, finder, helper, keysSeen, trace)
|
|
229
|
-
.then(result => handleCompletion(helper, 'responded', result))
|
|
230
|
+
.then(result => handleCompletion(helper, result ? 'responded' : 'disconnected', result))
|
|
230
231
|
.catch(err => {
|
|
231
232
|
// Handle errors - treat as disconnected
|
|
232
233
|
handleCompletion(helper, 'disconnected');
|
package/dht/nodeRefresh.js
CHANGED
|
@@ -30,10 +30,6 @@ export class NodeRefresh extends NodeKeys {
|
|
|
30
30
|
const adjustment = this.randomInteger(margin);
|
|
31
31
|
return Math.floor(target + margin/2 - adjustment);
|
|
32
32
|
}
|
|
33
|
-
workQueue = Promise.resolve();
|
|
34
|
-
queueWork(thunk) { // Promise to resolve thunk() -- after all previous queued thunks have resolved.
|
|
35
|
-
return this.workQueue = this.workQueue.then(thunk);
|
|
36
|
-
}
|
|
37
33
|
timers = new Map();
|
|
38
34
|
schedule(timerKey, statisticsKey, thunk, timeout = this.fuzzyInterval()) {
|
|
39
35
|
// Schedule thunk() to occur at a fuzzyInterval from now, cancelling any
|
|
@@ -46,16 +42,14 @@ export class NodeRefresh extends NodeKeys {
|
|
|
46
42
|
const start = Date.now();
|
|
47
43
|
clearInterval(this.timers.get(timerKey));
|
|
48
44
|
this.timers.set(timerKey, setTimeout(async () => {
|
|
49
|
-
const
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const elapsed = now - start;
|
|
47
|
+
const lag = elapsed - timeout;
|
|
50
48
|
this.timers.delete(timerKey);
|
|
51
49
|
if (this.isStopped()) return;
|
|
50
|
+
this.ilog('refresh', statisticsKey, timerKey, 'last/lag ms:', elapsed.toLocaleString(), lag.toLocaleString());
|
|
52
51
|
if (lag > 250) console.log(`** System is overloaded by ${lag.toLocaleString()} ms. **`);
|
|
53
|
-
|
|
54
|
-
// one at a time. This prevents a node from self-DoS'ing, but of course it does not coordinate across
|
|
55
|
-
// nodes. If the system is bogged down for any reason, then the timeout spacing will get smaller
|
|
56
|
-
// until finally the node is just running flat out.
|
|
57
|
-
// this.log('queue', statisticsKey, timerKey, timeout);
|
|
58
|
-
await this.queueWork(thunk);
|
|
52
|
+
await thunk();
|
|
59
53
|
}, timeout));
|
|
60
54
|
}
|
|
61
55
|
}
|
package/dht/nodeStorage.js
CHANGED
|
@@ -14,6 +14,7 @@ export class NodeStorage extends NodeRefresh {
|
|
|
14
14
|
// Claude.ai suggests just writing to the next in line, but that doesn't work.
|
|
15
15
|
this.schedule(key, 'storage', () => {
|
|
16
16
|
this.ilog('refresh value', value, 'at key', key);
|
|
17
|
+
// IF storeValue determines we are one of the nodes to store, then it will get scheduled again.
|
|
17
18
|
this.storeValue(key, value);
|
|
18
19
|
});
|
|
19
20
|
}
|
|
@@ -22,7 +23,7 @@ export class NodeStorage extends NodeRefresh {
|
|
|
22
23
|
}
|
|
23
24
|
async replicateCloserStorage(contact) { // Replicate to new contact any of our data for which contact is closer than us.
|
|
24
25
|
for (const key in this.storage.keys()) {
|
|
25
|
-
if (contact.distance(key) <= this.distance(key)) {
|
|
26
|
+
if (contact.distance(key) <= this.distance(key)) {
|
|
26
27
|
await contact.store(key, this.retrieveLocally(key));
|
|
27
28
|
}
|
|
28
29
|
}
|
package/dht/nodeTransports.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NodeStorage } from './nodeStorage.js';
|
|
2
|
+
import { WebRTC } from '@yz-social/webrtc';
|
|
2
3
|
|
|
3
4
|
// Management of Contacts that have a limited number of connections that can transport messages.
|
|
4
5
|
export class NodeTransports extends NodeStorage {
|
|
@@ -16,7 +17,7 @@ export class NodeTransports extends NodeStorage {
|
|
|
16
17
|
}
|
|
17
18
|
return false;
|
|
18
19
|
}
|
|
19
|
-
static maxTransports =
|
|
20
|
+
static maxTransports = WebRTC.suggestedInstancesLimit;
|
|
20
21
|
noteContactForTransport(contact) { // We're about to use this contact for a message, so keep track of it.
|
|
21
22
|
// Requires: if we later addToRoutingTable successfully, it should be removed from looseTransports.
|
|
22
23
|
// Requires: if we later remove contact because of a failed send, it should be removed from looseTransports.
|
|
@@ -36,7 +37,7 @@ export class NodeTransports extends NodeStorage {
|
|
|
36
37
|
}
|
|
37
38
|
let dropped = removeLast(this.looseTransports);
|
|
38
39
|
if (dropped) {
|
|
39
|
-
|
|
40
|
+
this.xlog('dropping loose transport', dropped.name);
|
|
40
41
|
} else { // Find the bucket with the most connections.
|
|
41
42
|
let bestBucket = null, bestCount = 0;
|
|
42
43
|
this.forEachBucket(bucket => {
|
|
@@ -47,8 +48,8 @@ export class NodeTransports extends NodeStorage {
|
|
|
47
48
|
return true;
|
|
48
49
|
});
|
|
49
50
|
dropped = removeLast(bestBucket.contacts);
|
|
50
|
-
if (!dropped)
|
|
51
|
-
|
|
51
|
+
if (!dropped) this.xlog('Unable to find something to drop in', this.report(null));
|
|
52
|
+
else this.xlog('dropping transport', dropped.name, 'in bucket', bestBucket.index, 'among', bestCount, 'contacts.');
|
|
52
53
|
}
|
|
53
54
|
dropped.disconnectTransport();
|
|
54
55
|
}
|
package/dht/nodeUtilities.js
CHANGED
|
@@ -72,7 +72,7 @@ export class NodeUtilities {
|
|
|
72
72
|
for (let index = 0; index < this.constructor.keySize; index++) {
|
|
73
73
|
const bucket = this.routingTable.get(index);
|
|
74
74
|
if (!bucket) continue;
|
|
75
|
-
report += `\n ${index}: ` + (contactsString(bucket.contacts) || '-');
|
|
75
|
+
report += `\n ${index} (${bucket.contacts.length}): ` + (contactsString(bucket.contacts) || '-');
|
|
76
76
|
}
|
|
77
77
|
return logger ? logger(report) : report;
|
|
78
78
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yz-social/kdht",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Pure Kademlia base, for testing variations.",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./index.js",
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"background": "npm stop; (npm start 1>server.log 2>&1 &); sleep 1",
|
|
15
15
|
"bots": "node spec/bots.js",
|
|
16
16
|
"thrashbots": "node spec/bots.js --thrash",
|
|
17
|
-
"test": "npx jasmine && echo '-- SUCCESS --' || echo '**** FAIL ****'"
|
|
17
|
+
"test": "npx jasmine && npx jasmine spec/webrtcTests.js && echo '-- SUCCESS --' || echo '**** FAIL ****'"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@yz-social/webrtc": "^0.1.
|
|
20
|
+
"@yz-social/webrtc": "^0.1.2",
|
|
21
21
|
"uuid": "^13.0.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
package/portals/node.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function setup({baseURL, externalBaseURL = '', verbose, fixedSpacin
|
|
|
10
10
|
// process.on('uncaughtException', error => console.error(hostName, 'Global uncaught exception:', error));
|
|
11
11
|
// process.on('unhandledRejection', error => console.error(hostName, 'Global unhandled promise rejection:', error));
|
|
12
12
|
|
|
13
|
-
const contact = await WebContact.create({name: hostName, isServerNode: true, debug: verbose});
|
|
13
|
+
const contact = await WebContact.create({name: hostName, isServerNode: true, info: false, debug: verbose});
|
|
14
14
|
// Handle signaling that comes as a message from the server.
|
|
15
15
|
process.on('message', async ([senderSname, ...incomingSignals]) => { // Signals from a sender through the server.
|
|
16
16
|
const response = await contact.signals(senderSname, ...incomingSignals);
|
package/spec/bots.js
CHANGED
|
@@ -59,9 +59,10 @@ if (cluster.isPrimary) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
const info = false;
|
|
62
63
|
await Node.delay(Node.randomInteger(Node.refreshTimeIntervalMS));
|
|
63
64
|
console.log(cluster.worker?.id || 0, host);
|
|
64
|
-
let contact = await WebContact.create({name: host, debug: argv.verbose});
|
|
65
|
+
let contact = await WebContact.create({name: host, info, debug: argv.verbose});
|
|
65
66
|
let bootstrapName = await contact.fetchBootstrap(argv.baseURL);
|
|
66
67
|
let bootstrapContact = await contact.ensureRemoteContact(bootstrapName, argv.baseURL);
|
|
67
68
|
await contact.join(bootstrapContact);
|
|
@@ -79,7 +80,7 @@ while (argv.thrash) {
|
|
|
79
80
|
await contact.disconnect();
|
|
80
81
|
await Node.delay(1e3); // TODO: remove?
|
|
81
82
|
|
|
82
|
-
contact = await WebContact.create({name: next, debug: argv.verbose});
|
|
83
|
+
contact = await WebContact.create({name: next, info, debug: argv.verbose});
|
|
83
84
|
bootstrapName = await contact.fetchBootstrap(argv.baseURL);
|
|
84
85
|
bootstrapContact = await contact.ensureRemoteContact(bootstrapName, argv.baseURL);
|
|
85
86
|
await contact.join(bootstrapContact);
|
|
@@ -9,7 +9,7 @@ const { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach} = glob
|
|
|
9
9
|
// implementation changes for different DHTs.
|
|
10
10
|
import { setupServerNodes, shutdownServerNodes,
|
|
11
11
|
start1, setupClientsByTime, shutdownClientNodes,
|
|
12
|
-
getContacts,
|
|
12
|
+
getContacts, readThroughRandom,
|
|
13
13
|
startThrashing, write1, read1, Node, Contact } from './dhtImplementation.js';
|
|
14
14
|
|
|
15
15
|
// Some definitions:
|
|
@@ -48,7 +48,7 @@ async function timed(operation, logString) {
|
|
|
48
48
|
await operation(startTime);
|
|
49
49
|
const endTime = Date.now();
|
|
50
50
|
const elapsed = endTime - startTime;
|
|
51
|
-
console.log(await logString(elapsed/1e3));
|
|
51
|
+
console.log(new Date(), await logString(elapsed/1e3));
|
|
52
52
|
return elapsed;
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -74,41 +74,17 @@ function delay(ms, label = '') {
|
|
|
74
74
|
}, ms));
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export async function startWrite1(name, bootstrapContact, refreshTimeIntervalMS, index) {
|
|
78
|
-
return await start1(name, bootstrapContact, refreshTimeIntervalMS)
|
|
79
|
-
.then(contact => write1(contact, name, name));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function awaitNonNullContact(contacts, i) { // Kind of stupid...
|
|
83
|
-
// When a contact thrashes, its contact[i] is null for a moment.
|
|
84
|
-
// TODO Alt: When thrashing, set slot to a promise and await it in all references, rather than this.
|
|
85
|
-
let contact = contacts[i];
|
|
86
|
-
if (contact) return contact;
|
|
87
|
-
await delay(50);
|
|
88
|
-
return await awaitNonNullContact(contacts, i);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
77
|
async function parallelWriteAll() {
|
|
92
78
|
// Persist a unique string through each contact all at once, but not resolving until all are ready.
|
|
93
79
|
const contacts = await getContacts();
|
|
94
80
|
// The key and value are the same, to facilitate read confirmation.
|
|
95
|
-
const writes = await Promise.all(contacts.map(async (contact, index) =>
|
|
96
|
-
let ok;
|
|
97
|
-
for (let i = 0; i < 10; i++) {
|
|
98
|
-
contact ||= await awaitNonNullContact(contacts, index);
|
|
99
|
-
ok = await write1(contact, index, index);
|
|
100
|
-
if (ok) break;
|
|
101
|
-
contact = contacts[index];
|
|
102
|
-
}
|
|
103
|
-
// It's ok if the contact disconnect right after, as long the write succeeded.
|
|
104
|
-
expect(ok).toBeTruthy();
|
|
105
|
-
}));
|
|
81
|
+
const writes = await Promise.all(contacts.map(async (contact, index) => write1(contact, index, index)));
|
|
106
82
|
return writes.length;
|
|
107
83
|
}
|
|
108
84
|
async function serialWriteAll() { // One-at-atime alternative to above, useful for debugging.
|
|
109
85
|
const contacts = await getContacts();
|
|
110
86
|
for (let index = 0; index < contacts.length; index++) {
|
|
111
|
-
const ok = await write1(
|
|
87
|
+
const ok = await write1(contacts[index], index, index);
|
|
112
88
|
expect(ok).toBeTruthy();
|
|
113
89
|
}
|
|
114
90
|
return contacts.length;
|
|
@@ -117,14 +93,7 @@ async function parallelReadAll(start = 0) {
|
|
|
117
93
|
// Reads from a random contact, confirming the value, for each key written by writeAll.
|
|
118
94
|
const contacts = await getContacts();
|
|
119
95
|
const readPromises = await Promise.all(contacts.map(async (_, index) => {
|
|
120
|
-
|
|
121
|
-
let value;
|
|
122
|
-
for (let liveTry = 0; liveTry < 4; liveTry++) {
|
|
123
|
-
const contact = await getRandomLiveContact();
|
|
124
|
-
value = await read1(contact, index);
|
|
125
|
-
if (contact.isRunning) break;
|
|
126
|
-
console.log('\n\n*** read from dead contact. retrying', liveTry, '***\n');
|
|
127
|
-
}
|
|
96
|
+
const value = await readThroughRandom(index);
|
|
128
97
|
expect(value).toBe(index);
|
|
129
98
|
}));
|
|
130
99
|
return readPromises.length - start;
|
|
@@ -132,7 +101,7 @@ async function parallelReadAll(start = 0) {
|
|
|
132
101
|
async function serialReadAll() { // One-at-a-time alternative of above, useful for debugging.
|
|
133
102
|
const contacts = await getContacts();
|
|
134
103
|
for (let index = 0; index < contacts.length; index++) {
|
|
135
|
-
const value = await
|
|
104
|
+
const value = await readThroughRandom(index);
|
|
136
105
|
expect(value).toBe(index);
|
|
137
106
|
}
|
|
138
107
|
return contacts.length;
|
|
@@ -156,25 +125,25 @@ describe("DHT", function () {
|
|
|
156
125
|
|
|
157
126
|
describe(notes || suiteLabel, function () {
|
|
158
127
|
beforeAll(async function () {
|
|
159
|
-
console.log('\n'
|
|
128
|
+
console.log('\n', new Date(), suiteLabel);
|
|
160
129
|
if (notes) console.log(notes);
|
|
161
130
|
await delay(3e3); // For gc
|
|
162
131
|
await timed(_ => setupServerNodes(nServerNodes, refreshTimeIntervalMS, pingTimeMS, maxTransports),
|
|
163
132
|
elapsed => `Server setup ${nServerNodes} / ${elapsed} = ${Math.round(nServerNodes/elapsed)} nodes/second.`);
|
|
164
133
|
expect(await getContactsLength()).toBe(nServerNodes); // sanity check
|
|
165
|
-
|
|
166
|
-
});
|
|
134
|
+
console.log(new Date(), 'end server setup');
|
|
135
|
+
}, 10e3);
|
|
167
136
|
afterAll(async function () {
|
|
168
|
-
|
|
137
|
+
console.log(new Date(), 'start server shutdown');
|
|
169
138
|
await shutdownServerNodes(nServerNodes);
|
|
170
139
|
expect(await getContactsLength()).toBe(0); // sanity check
|
|
171
|
-
|
|
140
|
+
console.log(new Date(), 'end server shutdown');
|
|
172
141
|
}, 20e3);
|
|
173
142
|
|
|
174
143
|
describe("joins within a refresh interval", function () {
|
|
175
144
|
let nJoined = 0, nWritten = 0;
|
|
176
145
|
beforeAll(async function () {
|
|
177
|
-
|
|
146
|
+
console.log(new Date(), 'start client setup');
|
|
178
147
|
if (startThrashingBefore === 'creation') await startThrashing(nServerNodes, refreshTimeIntervalMS);
|
|
179
148
|
let elapsed = await timed(async _ => nJoined = await setupClientsByTime(refreshTimeIntervalMS, nServerNodes, maxClientNodes, setupTimeMS),
|
|
180
149
|
elapsed => `Created ${nJoined} / ${elapsed} = ${(elapsed/nJoined).toFixed(3)} client nodes/second.`);
|
|
@@ -182,16 +151,16 @@ describe("DHT", function () {
|
|
|
182
151
|
if (maxClientNodes < Infinity) expect(nJoined).toBe(maxClientNodes); // Sanity check
|
|
183
152
|
if (startThrashingBefore === 'writing') await startThrashing(nServerNodes, refreshTimeIntervalMS);
|
|
184
153
|
await delay(runtimeBeforeWriteMS, 'pause before writing');
|
|
185
|
-
|
|
154
|
+
console.log(new Date(), 'writing');
|
|
155
|
+
elapsed = await timed(async _ => nWritten = await serialWriteAll(), // Alt: serial/parallelWriteAll
|
|
186
156
|
elapsed => `Wrote ${nWritten} / ${elapsed} = ${Math.round(nWritten/elapsed)} nodes/second.`);
|
|
187
|
-
|
|
188
|
-
}, setupTimeMS + runtimeBeforeWriteMS + runtimeBeforeWriteMS + 3 * setupTimeMS);
|
|
157
|
+
}, setupTimeMS + runtimeBeforeWriteMS + runtimeBeforeWriteMS + 5 * setupTimeMS);
|
|
189
158
|
afterAll(async function () {
|
|
190
|
-
|
|
159
|
+
console.log(new Date(), 'start client shutdown');
|
|
191
160
|
//await Node.reportAll();
|
|
192
161
|
await shutdownClientNodes(nServerNodes, nJoined);
|
|
193
162
|
expect(await getContactsLength()).toBe(nServerNodes); // Sanity check.
|
|
194
|
-
|
|
163
|
+
console.log(new Date(), 'end client shutdown');
|
|
195
164
|
}, 20e3);
|
|
196
165
|
it("produces.", async function () {
|
|
197
166
|
const total = await getContactsLength();
|
|
@@ -201,8 +170,9 @@ describe("DHT", function () {
|
|
|
201
170
|
it("can be read.", async function () {
|
|
202
171
|
if (startThrashingBefore === 'reading') await startThrashing(nServerNodes, refreshTimeIntervalMS);
|
|
203
172
|
await delay(runtimeBeforeReadMS, 'pause before reading');
|
|
173
|
+
console.log(new Date(), 'reading');
|
|
204
174
|
let nRead = 0;
|
|
205
|
-
await timed(async _ => nRead = await
|
|
175
|
+
await timed(async _ => nRead = await serialReadAll(), // alt: serial/parallelReadAll
|
|
206
176
|
elapsed => `Read ${nRead} / ${elapsed} = ${Math.round(nRead/elapsed)} values/second.`);
|
|
207
177
|
expect(nRead).toBe(nWritten);
|
|
208
178
|
}, 10 * setupTimeMS + 5 * runtimeBeforeReadMS);
|
|
@@ -217,6 +187,7 @@ describe("DHT", function () {
|
|
|
217
187
|
test({setupTimeMS: 1e3, pingTimeMS: 0, startThrashingBefore: 'never', notes: "Probing on, but no disconnects or network delay."});
|
|
218
188
|
test({pingTimeMS: 0, refreshTimeIntervalMS: 5e3, notes: "Small networks allow faster thrash smoke-testing."});
|
|
219
189
|
test({notes: "Normal ops"});
|
|
190
|
+
test({setupTimeMS: 40e3, notes: "Bigger network overflowing bucket."});
|
|
220
191
|
|
|
221
192
|
// test({maxClientNodes: 55, setupTimeMS: 240e3, pingTimeMS: 40, maxTransports: 62,
|
|
222
193
|
// //startThrashingBefore: 'never', runtimeBeforeWriteMS: 0, runtimeBeforeReadMS: 0,
|