@yz-social/kdht 0.1.0

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/spec/bots.js ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ import cluster from 'node:cluster';
3
+ import process from 'node:process';
4
+ import { launchWriteRead } from './writes.js';
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ import { WebContact, Node } from '../index.js';
7
+ import yargs from 'yargs';
8
+ import { hideBin } from 'yargs/helpers';
9
+
10
+ // Todo: Allow a remote portal to be specified (passing a host to WebContact.create/ensureRemoteContact).
11
+ const argv = yargs(hideBin(process.argv))
12
+ .usage("Launch nBots that connect to the network through the local portal. A bot is just an ordinary node that can only be contacted through another node. They provide either continuity or churn-testing, depend on whether or not they are told to 'thrash'.")
13
+ .option('nBots', {
14
+ alias: 'n',
15
+ alias: 'nbots',
16
+ type: 'number',
17
+ default: 20,
18
+ description: "The number of bots, which can only be reached through the network."
19
+ })
20
+ .option('baseURL', {
21
+ type: 'string',
22
+ default: 'http://localhost:3000/kdht',
23
+ description: "The base URL of the portal server through which to bootstrap."
24
+ })
25
+ .option('thrash', {
26
+ type: 'boolean',
27
+ default: false,
28
+ description: "Do bots randomly disconnect and reconnect with no memory of previous data?"
29
+ })
30
+ .option('nWrites', {
31
+ alias: 'w',
32
+ alias: "nwrites",
33
+ type: 'number',
34
+ default: 0,
35
+ description: "The number of test writes to check."
36
+ })
37
+ .option('verbose', {
38
+ alias: 'v',
39
+ type: 'boolean',
40
+ description: "Run with verbose logging."
41
+ })
42
+ .parse();
43
+
44
+ const host = uuidv4();
45
+
46
+ if (cluster.isPrimary) {
47
+ for (let i = 1; i < argv.nBots; i++) { // The cluster primary becomes bot 0.
48
+ cluster.fork();
49
+ }
50
+ if (argv.nWrites) {
51
+ console.log(new Date(), 'Waiting a refresh interval while bots get randomly created before write/read test');
52
+ await Node.delay(2 * Node.refreshTimeIntervalMS);
53
+ launchWriteRead(argv.nWrites, argv.baseURL, Node.refreshTimeIntervalMS, argv.verbose);
54
+ }
55
+ }
56
+ process.title = 'kdht-bot-' + host;
57
+
58
+ await Node.delay(Node.randomInteger(Node.refreshTimeIntervalMS));
59
+ console.log(cluster.worker?.id || 0, host);
60
+ let contact = await WebContact.create({name: host, debug: argv.verbose});
61
+ let bootstrapName = await contact.fetchBootstrap(argv.baseURL);
62
+ let bootstrapContact = await contact.ensureRemoteContact(bootstrapName, argv.baseURL);
63
+ await contact.join(bootstrapContact);
64
+
65
+ while (argv.thrash) {
66
+ await Node.delay(contact.host.fuzzyInterval(Node.refreshTimeIntervalMS));
67
+ const old = contact;
68
+ const next = uuidv4();
69
+ contact.host.xlog('disconnecting');
70
+ contact.disconnect();
71
+ await Node.delay(1e3); // TODO: remove?
72
+
73
+ contact = await WebContact.create({name: next, debug: argv.verbose});
74
+ bootstrapName = await contact.fetchBootstrap(argv.baseURL);
75
+ bootstrapContact = await contact.ensureRemoteContact(bootstrapName, argv.baseURL);
76
+ await contact.join(bootstrapContact);
77
+ old.host.xlog('rejoined as', next);
78
+ }
79
+
@@ -0,0 +1,227 @@
1
+ // Defines a repeatable suite of tests that confirm DHT operations and log performance data.
2
+
3
+ // Defined by the generic test framework. See https://jasmine.github.io/
4
+ // One does not import these definitions from a file, but rather they are
5
+ // defined globally by the jasmine program or browser page that runs the tests.
6
+ const { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach} = globalThis; // For linters.
7
+
8
+ // The file dhtImplementation.js exports functions that perform setup operations whose
9
+ // implementation changes for different DHTs.
10
+ import { setupServerNodes, shutdownServerNodes,
11
+ start1, setupClientsByTime, shutdownClientNodes,
12
+ getContacts, getRandomLiveContact,
13
+ startThrashing, write1, read1, Node } from './dhtImplementation.js';
14
+
15
+ // Some definitions:
16
+ //
17
+ // CONTACTS: an array whose elements can be passed to write1/read1.
18
+ // The elements might be node names or keys, or some wrapper around a node instance.
19
+ //
20
+ // THRASH: to run a client node for a random amount of time averaging the refresh interval,
21
+ // disconnect, and then immediately reconnect with no memory of any stored key/value pairs.
22
+ // It is not specified whether the node must come back with the same node name or node key,
23
+ // but it is required that contacts must be updated with any new values.
24
+ //
25
+ // CLIENT NODE: A node representing an application user, that is subject to thrashing.
26
+ //
27
+ // SERVER NODE: A node managed by the server that does NOT thrash.
28
+ // 1. The server nodes provide persistence of stored values when all the client nodes have disconnected,
29
+ // and thus ARE included in contacts. (AKA as "persistence nodes" or "community nodes".)
30
+ // 2. In addition, the implementation-specific function setupServerNodes also ensures
31
+ // that client nodes have nodes that they can join through (AKA "bootstrap nodes"),
32
+ // but it is not specified whether this function is performed by the server nodes in the
33
+ // contacts array, or by other nodes managed by the server.
34
+ //
35
+ // READY: A node is ready when write1/read1 operations may be performed through it without loss.
36
+ // It is not specified whether this condition is an arbitrary amount of time or the result of
37
+ // having positively completed completed some probing operation with the network. The
38
+ // implementation-specific asynchronous operations to set up, thrash, write, and read must
39
+ // not resolve until the node is "ready" for more operations. For example, a write that has
40
+ // resolved, followed by nodes thrashing (averaging the refresh interval), followed by
41
+ // a read sould produce the written value.
42
+
43
+ async function timed(operation, logString) {
44
+ // Time the execution of await operation(startTime), and
45
+ // then log the result of await logString(elapsedSeconds).
46
+ // Promises the elapsed time in milliseconds.
47
+ const startTime = Date.now();
48
+ await operation(startTime);
49
+ const endTime = Date.now();
50
+ const elapsed = endTime - startTime;
51
+ console.log(await logString(elapsed/1e3));
52
+ return elapsed;
53
+ }
54
+
55
+ async function getContactsLength() {
56
+ // Promise current length of contacts. (Convenience for sanity checks.)
57
+ const contacts = await getContacts();
58
+ return contacts.length;
59
+ }
60
+ function delay(ms, label = '') {
61
+ // Promise to resolve in the given milliseconds.
62
+ // Logs what it is doing if given a label.
63
+ // Reports any non-trivial lagging.
64
+ // NOTE: this is measuring lag on the machine running the tests:
65
+ // 1. It does not (currently) check remote systems.
66
+ // 2. It does not automatically fail the tests if there is lag, although it likely
67
+ // that things will fail. So check the logs.
68
+ if (label && ms) console.log(`(${(ms/1e3).toFixed(3)}s ${label})`);
69
+ const start = Date.now();
70
+ return new Promise(resolve => setTimeout(() => {
71
+ const lag = Date.now() - start - ms;
72
+ if (lag > 250) console.log(`** System is overloaded by ${lag.toLocaleString()} ms. **`);
73
+ resolve();
74
+ }, ms));
75
+ }
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
+ async function parallelWriteAll() {
92
+ // Persist a unique string through each contact all at once, but not resolving until all are ready.
93
+ const contacts = await getContacts();
94
+ // 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
+ }));
106
+ return writes.length;
107
+ }
108
+ async function serialWriteAll() { // One-at-atime alternative to above, useful for debugging.
109
+ const contacts = await getContacts();
110
+ for (let index = 0; index < contacts.length; index++) {
111
+ const ok = await write1(await awaitNonNullContact(contacts, index), index, index);
112
+ expect(ok).toBeTruthy();
113
+ }
114
+ return contacts.length;
115
+ }
116
+ async function parallelReadAll(start = 0) {
117
+ // Reads from a random contact, confirming the value, for each key written by writeAll.
118
+ const contacts = await getContacts();
119
+ const readPromises = await Promise.all(contacts.map(async (_, index) => {
120
+ if (index < start) return;
121
+ const value = await read1(await getRandomLiveContact(), index);
122
+ expect(value).toBe(index);
123
+ }));
124
+ return readPromises.length - start;
125
+ }
126
+ async function serialReadAll() { // One-at-a-time alternative of above, useful for debugging.
127
+ const contacts = await getContacts();
128
+ for (let index = 0; index < contacts.length; index++) {
129
+ const value = await read1(await getRandomLiveContact(), index);
130
+ expect(value).toBe(index);
131
+ }
132
+ return contacts.length;
133
+ }
134
+
135
+ describe("DHT", function () {
136
+ function test(parameters = {}) {
137
+ // Define a suite of tests with the given parameters.
138
+ const {nServerNodes = 10,
139
+ pingTimeMS = 40, // Round-trip network time. Implementation should pad network calls to achieve what is specified here.
140
+ maxTransports = 152, // How many direct connections are allowed per node?
141
+ maxClientNodes = Infinity, // If zero, will try to make as many as it can in refreshTimeIntervalMS.
142
+ refreshTimeIntervalMS = 15e3, // How long on average does a client stay up?
143
+ setupTimeMS = Math.max(2e3, refreshTimeIntervalMS), // How long to create the test nodes.
144
+ runtimeBeforeWriteMS = 3 * refreshTimeIntervalMS, // How long to probe (and thrash) before writing starts.
145
+ runtimeBeforeReadMS = runtimeBeforeWriteMS, // How long to probe (and thrash) before reading starts.
146
+ startThrashingBefore = 'creation', // When to start thrashing clients: before creation|writing|reading. Anything else is no thrashing.
147
+ notes = ''
148
+ } = parameters;
149
+ const suiteLabel = `Server nodes: ${nServerNodes}, setup time: ${setupTimeMS}, max client nodes: ${maxClientNodes ?? Infinity}, ping: ${pingTimeMS}ms, max connections: ${maxTransports}, refresh: ${refreshTimeIntervalMS.toFixed(3)}ms, pause before write: ${runtimeBeforeWriteMS.toFixed(3)}ms, pause before read: ${runtimeBeforeReadMS.toFixed(3)}ms, thrash before: ${startThrashingBefore}`;
150
+
151
+ describe(suiteLabel, function () {
152
+ beforeAll(async function () {
153
+ console.log('\n' + suiteLabel);
154
+ if (notes) console.log(notes);
155
+ await delay(3e3); // For gc
156
+ await timed(_ => setupServerNodes(nServerNodes, refreshTimeIntervalMS, pingTimeMS, maxTransports),
157
+ elapsed => `Server setup ${nServerNodes} / ${elapsed} = ${Math.round(nServerNodes/elapsed)} nodes/second.`);
158
+ expect(await getContactsLength()).toBe(nServerNodes); // sanity check
159
+ //console.log('end server setup');
160
+ });
161
+ afterAll(async function () {
162
+ //console.log('start server shutdown');
163
+ await shutdownServerNodes(nServerNodes);
164
+ expect(await getContactsLength()).toBe(0); // sanity check
165
+ //console.log('end server shutdown');
166
+ }, 20e3);
167
+
168
+ describe("joins within a refresh interval", function () {
169
+ let nJoined = 0, nWritten = 0;
170
+ beforeAll(async function () {
171
+ //console.log('start client setup');
172
+ if (startThrashingBefore === 'creation') await startThrashing(nServerNodes, refreshTimeIntervalMS);
173
+ let elapsed = await timed(async _ => nJoined = await setupClientsByTime(refreshTimeIntervalMS, nServerNodes, maxClientNodes, setupTimeMS),
174
+ elapsed => `Created ${nJoined} / ${elapsed} = ${(elapsed/nJoined).toFixed(3)} client nodes/second.`);
175
+ expect(await getContactsLength()).toBe(nJoined + nServerNodes); // Sanity check
176
+ if (maxClientNodes < Infinity) expect(nJoined).toBe(maxClientNodes); // Sanity check
177
+ if (startThrashingBefore === 'writing') await startThrashing(nServerNodes, refreshTimeIntervalMS);
178
+ await delay(runtimeBeforeWriteMS, 'pause before writing');
179
+ elapsed = await timed(async _ => nWritten = await parallelWriteAll(), // Alt: serialWriteAll
180
+ elapsed => `Wrote ${nWritten} / ${elapsed} = ${Math.round(nWritten/elapsed)} nodes/second.`);
181
+ //console.log('end client setup');
182
+ }, setupTimeMS + runtimeBeforeWriteMS + runtimeBeforeWriteMS + 3 * setupTimeMS);
183
+ afterAll(async function () {
184
+ //console.log('start client shutdown');
185
+ //await Node.reportAll();
186
+ await shutdownClientNodes(nServerNodes, nJoined);
187
+ expect(await getContactsLength()).toBe(nServerNodes); // Sanity check.
188
+ //console.log('end client shutdown');
189
+ }, 20e3);
190
+ it("produces.", async function () {
191
+ const total = await getContactsLength();
192
+ expect(total).toBe(nJoined + nServerNodes); // Sanity check
193
+ expect(nWritten).toBe(total);
194
+ });
195
+ it("can be read.", async function () {
196
+ if (startThrashingBefore === 'reading') await startThrashing(nServerNodes, refreshTimeIntervalMS);
197
+ await delay(runtimeBeforeReadMS, 'pause before reading');
198
+ let nRead = 0;
199
+ await timed(async _ => nRead = await parallelReadAll(), // alt: serialReadAll
200
+ elapsed => `Read ${nRead} / ${elapsed} = ${Math.round(nRead/elapsed)} values/second.`);
201
+ expect(nRead).toBe(nWritten);
202
+ }, 10 * setupTimeMS + 5 * runtimeBeforeReadMS);
203
+ });
204
+ });
205
+ }
206
+
207
+ // Each call here sets up a full suite of tests with the given parameters, which can be useful for development and debugging.
208
+ // For example:
209
+ test({pingTimeMS: 0, refreshTimeIntervalMS: 0, startThrashingBefore: 'never', notes: "Runs flat out if probing and disconnects turned off."});
210
+ test({setupTimeMS: 1e3, pingTimeMS: 0, startThrashingBefore: 'never', notes: "Probing on, but no disconnects or network delay."});
211
+ test({maxClientNodes: 30, pingTimeMS: 0, refreshTimeIntervalMS: 5e3, notes: "Small networks allow faster smoke-testing."});
212
+ test({maxTransports: 85, maxClientNodes: 90, pingTimeMS: 10, setupTimeMS: 20e3, notes: "Limit number of transports enough to exercise the reconnect logic."});
213
+ test({maxClientNodes: 140, setupTimeMS: 60e3, pingTimeMS: 10, notes: "Relatively larger network size."});
214
+
215
+ //test({maxTransports: 95, maxClientNodes: 100, refreshTimeIntervalMS: 0, startThrashingBefore: 'never', notes: 'dev: no refresh, no thrashing'});
216
+ //test({maxTransports: 95, maxClientNodes: 100, startThrashingBefore: 'never', notes: 'dev: no thrashing'});
217
+
218
+ //test({maxClientNodes: 7, nServerNodes: 5, refreshTimeIntervalMS: 3e3, runtimeBeforeWriteMS: 0e3, runtimeBeforeReadMS: 0e3, startThrashingBefore: 'never'});
219
+ //test({maxClientNodes: 3, nServerNodes: 3, refreshTimeIntervalMS: 3e3, runtimeBeforeWriteMS: 6e3, runtimeBeforeReadMS: 6e3});
220
+
221
+
222
+ // TODO:
223
+ // Persistence Test that joins+writes one at a time until period, runs 3xperiod, then quits one a time until gone, then one node join and reads all
224
+ // collect and confirm data from each node on shutdown.
225
+ // pub/sub
226
+ // 1k nodes
227
+ });
@@ -0,0 +1,191 @@
1
+ // An example of the control functions needed for testing.
2
+
3
+ // For running a server
4
+ // import express from 'express';
5
+ // import http from 'http';
6
+ //import { router } from './routes/index.js';
7
+
8
+ ////////////////////////////////////////////////////////////////////////////////////////////////
9
+ // This section certainly needs to be modified for any given implementation.
10
+ //
11
+
12
+ // In the present case, these manipulate a Contact that directly contains a
13
+ // DHT node with simulated networking.
14
+ //import { InProcessWebContact as Contact, Node } from '../index.js';
15
+ //import { SimulatedContact as Contact, Node } from '../index.js';
16
+ import { SimulatedConnectionContact as Contact, Node } from '../index.js';
17
+ export { Node };
18
+
19
+
20
+ export async function start1(name, bootstrapContact, refreshTimeIntervalMS, isServerNode = false) {
21
+ const contact = await Contact.create({name, refreshTimeIntervalMS, isServerNode});
22
+ if (bootstrapContact) await contact.join(bootstrapContact);
23
+ return contact;
24
+ }
25
+
26
+ export async function startServerNode(name, bootstrapContact, refreshTimeIntervalMS) {
27
+ return await start1(name, bootstrapContact, refreshTimeIntervalMS, true);
28
+ }
29
+
30
+ export async function stop1(contact) {
31
+
32
+ // For debugging: Report if we're killing the last holder of our data.
33
+ // This is fine for simulations, but some decentralized reporting would be needed for real systems.
34
+ // if (contact && isThrashing) {
35
+ // for (const key of contact.node.storage.keys()) {
36
+ // let remaining = [];
37
+ // for (const contact of contacts) {
38
+ // if (contact?.isConnected && contact.node.storage.has(key)) remaining.push(contact.node.name);
39
+ // }
40
+ // if (!remaining.length) console.log(`Disconnecting ${contact.node.name}, last holder of ${key}: ${contact.node.storage.get(key)}.`);
41
+ // }
42
+ // }
43
+
44
+ return await contact?.disconnect();
45
+ }
46
+
47
+ export async function write1(contact, key, value) {
48
+ // Make a request through contact to store value under key in the DHT
49
+ // resolving when ready. (See test suite definitions.)
50
+ return await contact.node.storeValue(key, value);
51
+ }
52
+ export async function read1(contact, key) {
53
+ // Promise the result of requesting key from the DHT through contact.
54
+ return await contact.node.locateValue(key);
55
+ }
56
+
57
+
58
+
59
+ ////////////////////////////////////////////////////////////////////////////////////////////////
60
+ // Given the above, the following might not need to be redefined on a per-implemmentation basis,
61
+ // but see pingTimeMS in setupServerNodes().
62
+
63
+ var contacts = [];
64
+ export async function getContacts() {
65
+ // Return a list of contact information for all the nodes.
66
+ // For real implementations this might be a list of node identifiers.
67
+ // It is async because it might be collecting from some monitor/control service.
68
+
69
+ // For a simulator in one Javascript instance, it is just the list of Contacts.
70
+ return contacts;
71
+ }
72
+ function randomInteger(max = contacts.length) {
73
+ // Return a random number between 0 (inclusive) and max (exclusive), defaulting to the number of contacts made.
74
+ return Math.floor(Math.random() * max);
75
+ }
76
+ export async function getRandomLiveContact() {
77
+ // Answer a randomly selected contact (including those for server nodes) that is
78
+ // is not in the process of reconnecting.
79
+ return contacts[randomInteger()] || await getRandomLiveContact();
80
+ }
81
+ export async function getBootstrapContact(nServerNodes) {
82
+ return contacts[randomInteger(nServerNodes)];
83
+ }
84
+
85
+ var isThrashing = false;
86
+ const thrashers = [];
87
+ function thrash(i, nServerNodes, refreshTimeIntervalMS) { // Start disconnect/reconnect timer on contact i.
88
+ // If we are asked to thrash with a zero refreshTimeIntervalMS, average one second anyway.
89
+ const average = Math.max(refreshTimeIntervalMS, 2e3);
90
+ const min = Math.min(average / 2, 2e3);
91
+ const runtimeMS = randomInteger(2 * (average - min)) + min;
92
+ thrashers[i] = setTimeout(async () => {
93
+ if (!isThrashing) return;
94
+ const contact = contacts[i];
95
+ contacts[i] = null;
96
+ await stop1(contact);
97
+ if (!isThrashing) return;
98
+ const bootstrapContact = await getBootstrapContact(nServerNodes);
99
+ const started = await start1(i, bootstrapContact, refreshTimeIntervalMS);
100
+ if (!isThrashing) return;
101
+ contacts[i] = started;
102
+ thrash(i, nServerNodes, refreshTimeIntervalMS);
103
+ }, runtimeMS);
104
+ }
105
+ export async function startThrashing(nServerNodes, refreshTimeIntervalMS) {
106
+ console.log('Start thrashing');
107
+ isThrashing = true;
108
+ for (let i = nServerNodes; i < contacts.length; i++) {
109
+ thrash(i, nServerNodes, refreshTimeIntervalMS);
110
+ }
111
+ }
112
+ export async function stopThrashing() {
113
+ isThrashing = false;
114
+ for (const thrasher of thrashers) clearTimeout(thrasher);
115
+ }
116
+
117
+ async function shutdown(startIndex, stopIndex) { // Internal
118
+ // Shutdown n nodes.
119
+ for (let i = startIndex; i < stopIndex; i++) {
120
+ await stop1(contacts.pop());
121
+ }
122
+ }
123
+
124
+
125
+ // const app = express();
126
+ // const port = 3000;
127
+ // app.set('port', port);
128
+ // app.use(express.json());
129
+ // //app.use('/test', router);
130
+ // app.listen(port);
131
+
132
+ let ping, transports;
133
+ export async function setupServerNodes(nServerNodes, refreshTimeIntervalMS, pingTimeMS, maxTransports) {
134
+ // Set up nServerNodes, returning a promise that resolves when they are ready to use.
135
+ // See definitions in test suite.
136
+
137
+ Node.contacts = contacts = []; // Quirk of simulation code.
138
+ transports = Node.maxTransports;
139
+ Node.maxTransports = maxTransports;
140
+ ping = Contact.pingTimeMS;
141
+ Contact.pingTimeMS = pingTimeMS;
142
+
143
+ for (let i = 0; i < nServerNodes; i++) {
144
+ const node = await startServerNode(i, contacts[i - 1], refreshTimeIntervalMS);
145
+ contacts.push(node);
146
+ }
147
+ }
148
+ export async function shutdownServerNodes(nServerNodes) {
149
+ // Shut down the specified number of server nodes, resolving when complete.
150
+ // The nServerNodes will match that of the preceding setupServerNodes.
151
+ // The purpose here is to kill any persisted data so that the next call
152
+ // to setupServerNodes will start fresh.
153
+ await shutdown(0, nServerNodes);
154
+ Contact.pingTimeMS = ping;
155
+ Node.maxTransports = transports;
156
+ Node.contacts = [];
157
+ }
158
+
159
+ export async function setupClientsByTime(...rest) {
160
+ // Create as many ready-to-use client nodes as possible in the specified milliseconds.
161
+ // Returns a promise that resolves to the number of clients that are now ready to use.
162
+ return await serialSetupClientsByTime(...rest);
163
+ // Alternatively, one could launch batches of clients in parallel, and then
164
+ // wait for each to complete its probes.
165
+ }
166
+ async function serialSetupClientsByTime(refreshTimeIntervalMS, nServerNodes, maxClientNodes, runtimeMS, op=start1) {
167
+ // Do setupClientsByTime one client node at a time.
168
+ // It takes longer and longer as the number of existing nodes gets larger.
169
+ return await new Promise(async resolve => {
170
+ const nBootstraps = contacts.length;
171
+ let done = false, index = nBootstraps, count = 0;
172
+ setTimeout(() => done = true, runtimeMS);
173
+ while (!done && (count++ < maxClientNodes)) {
174
+ const bootstrapContact = await getBootstrapContact(nBootstraps);
175
+ const contact = await op(index, bootstrapContact, refreshTimeIntervalMS);
176
+ if (!done) { // Don't include it if we're now over time.
177
+ contacts.push(contact);
178
+ if (isThrashing) thrash(index, nServerNodes, refreshTimeIntervalMS);
179
+ index++;
180
+ } else {
181
+ await stop1(contact);
182
+ }
183
+ }
184
+ resolve(contacts.length - nBootstraps);
185
+ });
186
+ }
187
+ export async function shutdownClientNodes(nServerNodes, nClientNodes) {
188
+ await stopThrashing();
189
+ await new Promise(resolve => setTimeout(resolve, 5e3));
190
+ await shutdown(nServerNodes, nClientNodes + nServerNodes);
191
+ }