@mark1russell7/harness 0.5.0 → 0.7.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/README.md +63 -4
- package/build/index.d.ts +537 -23
- package/build/index.js +1497 -94
- package/build/index.js.map +1 -1
- package/image/go-http/Dockerfile +14 -0
- package/image/go-http/go.mod +3 -0
- package/image/go-http/main.go +324 -0
- package/image/mongo/Dockerfile +11 -0
- package/image/mongo/client-mongo.mjs +273 -0
- package/image/mongo/npm-lock.json +163 -0
- package/image/mongo/package.json +8 -0
- package/image/mongo/stepdown-mongo.mjs +37 -0
- package/image/node-http/Dockerfile +12 -0
- package/image/node-http/client-agent.mjs +163 -0
- package/image/node-http/client-undici.mjs +141 -0
- package/image/node-http/lines.mjs +59 -0
- package/image/node-http/npm-lock.json +22 -0
- package/image/node-http/package.json +8 -0
- package/package.json +16 -2
package/build/index.js
CHANGED
|
@@ -10,6 +10,8 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
const isHopPlace = (place) => typeof place === "object" && "hop" in place;
|
|
11
11
|
//#endregion
|
|
12
12
|
//#region src/verdict.ts
|
|
13
|
+
/** This function gives the words of the outcome of a second request: its connection, a retry, and its end. */
|
|
14
|
+
const requestWords = (outcome) => `${outcome.connection}${outcome.retried ? ", a retry" : ""}, ${outcome.pending === true ? "pending" : outcome.error ?? "a reply"}`;
|
|
13
15
|
/**
|
|
14
16
|
* This amount is the noise of the pipeline itself, δ_abs, as the calibration measured it. It is
|
|
15
17
|
* the 99.9th percentile of the whole latency of a reset, at the concurrency cap or less. Its
|
|
@@ -115,14 +117,32 @@ const middle = (sorted) => {
|
|
|
115
117
|
* which does not exist at this time.
|
|
116
118
|
*/
|
|
117
119
|
const verdictOf = (prediction, runs) => {
|
|
118
|
-
const
|
|
120
|
+
const band = verdictOfBand(prediction, runs);
|
|
119
121
|
const counted = runs.filter((run) => run.void === null);
|
|
122
|
+
const verdict = counted.some((run) => run.request !== void 0) ? withRequests(band, counted) : band;
|
|
120
123
|
if (prediction.group === void 0 && !counted.some((run) => run.group !== void 0)) return verdict;
|
|
121
124
|
return {
|
|
122
125
|
...verdict,
|
|
123
126
|
group: groupVerdictOf(prediction, counted)
|
|
124
127
|
};
|
|
125
128
|
};
|
|
129
|
+
/**
|
|
130
|
+
* This function adds the outcomes of the second request of an HTTP client to a verdict. A cell
|
|
131
|
+
* whose outcome is a distribution gets them in its reason too.
|
|
132
|
+
*/
|
|
133
|
+
const withRequests = (verdict, counted) => {
|
|
134
|
+
const requests = {};
|
|
135
|
+
for (const run of counted) {
|
|
136
|
+
const words = run.request === null || run.request === void 0 ? "no second request" : requestWords(run.request);
|
|
137
|
+
requests[words] = (requests[words] ?? 0) + 1;
|
|
138
|
+
}
|
|
139
|
+
const listed = Object.entries(requests).map(([words, n]) => `${n} ${words}`).join("; ");
|
|
140
|
+
return {
|
|
141
|
+
...verdict,
|
|
142
|
+
requests,
|
|
143
|
+
why: verdict.decision === "measured" ? `${verdict.why} Second requests: ${listed}.` : verdict.why
|
|
144
|
+
};
|
|
145
|
+
};
|
|
126
146
|
/** This function gives the median of some counts, or null for none. */
|
|
127
147
|
const medianCount = (values) => values.length === 0 ? null : middle([...values].sort((a, b) => a - b));
|
|
128
148
|
/** This function sums what the group of a group cell did in its counted runs, and tests each stage that has a band. */
|
|
@@ -139,9 +159,10 @@ const groupVerdictOf = (prediction, counted) => {
|
|
|
139
159
|
for (const run of counted) {
|
|
140
160
|
const at = run.group?.stages[band.stage];
|
|
141
161
|
if (at === void 0) {
|
|
162
|
+
const who = band.stage.startsWith("+") ? "no Sentinel announced" : "no member or client marked";
|
|
142
163
|
outside.push({
|
|
143
164
|
run: name(run),
|
|
144
|
-
why:
|
|
165
|
+
why: `${who} ${band.stage}`
|
|
145
166
|
});
|
|
146
167
|
continue;
|
|
147
168
|
}
|
|
@@ -362,13 +383,367 @@ const verdictOfBand = (prediction, runs) => {
|
|
|
362
383
|
};
|
|
363
384
|
};
|
|
364
385
|
//#endregion
|
|
386
|
+
//#region src/driver.ts
|
|
387
|
+
/** These are the errno numbers that the kernel reports to a socket, by the names that predictions use. */
|
|
388
|
+
const ERRNO$1 = {
|
|
389
|
+
32: "EPIPE",
|
|
390
|
+
101: "ENETUNREACH",
|
|
391
|
+
103: "ECONNABORTED",
|
|
392
|
+
104: "ECONNRESET",
|
|
393
|
+
110: "ETIMEDOUT",
|
|
394
|
+
111: "ECONNREFUSED",
|
|
395
|
+
113: "EHOSTUNREACH"
|
|
396
|
+
};
|
|
397
|
+
const between$2 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
|
|
398
|
+
const round = (seconds) => Math.round(seconds * 1e6) / 1e6;
|
|
399
|
+
/** This function gives the value of a field of a line by its name. A dotted name is a field in a field. */
|
|
400
|
+
const fieldOf = (line, path) => path.split(".").reduce((value, key) => value !== null && typeof value === "object" ? value[key] : void 0, line);
|
|
401
|
+
/** This function tells if a line of the client matches a line match: its `ev`, and each field that the match names. */
|
|
402
|
+
const matches = (line, match) => line.ev === match.ev && Object.entries(match.where ?? {}).every(([path, value]) => fieldOf(line, path) === value);
|
|
403
|
+
/** This function says a line match in words, for a finding. */
|
|
404
|
+
const wordsOf = (match) => {
|
|
405
|
+
const where = Object.entries(match.where ?? {}).map(([path, value]) => `${path} ${String(value)}`);
|
|
406
|
+
return where.length === 0 ? `a ${match.ev} line` : `a ${match.ev} line with ${where.join(", ")}`;
|
|
407
|
+
};
|
|
408
|
+
/** This function gives the sockets of the driver in a run, from the `socket` and `socket-end` lines of the client. */
|
|
409
|
+
const socketsOf = (events) => events.filter((event) => event.ev === "socket").map((event) => {
|
|
410
|
+
const local = String(event["local"]);
|
|
411
|
+
const end = events.find((each) => each.ev === "socket-end" && each["local"] === local && each.t >= event.t);
|
|
412
|
+
return {
|
|
413
|
+
role: String(event["role"]),
|
|
414
|
+
local,
|
|
415
|
+
server: String(event["server"]),
|
|
416
|
+
opened: event.t,
|
|
417
|
+
ended: end?.t ?? null
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
/**
|
|
421
|
+
* This function finds the socket of a role at an instant. The pool's socket is the first one, which
|
|
422
|
+
* the client names in its `connected` line. Each other role has the latest socket that opened at
|
|
423
|
+
* the instant or before it.
|
|
424
|
+
*/
|
|
425
|
+
const socketAt = (events, role, at) => {
|
|
426
|
+
const sockets = socketsOf(events);
|
|
427
|
+
if (role === "pool") {
|
|
428
|
+
const local = events.find((event) => event.ev === "connected")?.["local"];
|
|
429
|
+
return sockets.find((socket) => socket.local === local);
|
|
430
|
+
}
|
|
431
|
+
return sockets.filter((socket) => socket.role === role && socket.opened <= at).at(-1);
|
|
432
|
+
};
|
|
433
|
+
const placeOf = (run, netns, find) => {
|
|
434
|
+
const cookie = run.kernel.find((record) => record.netns === netns && find(record))?.cookie;
|
|
435
|
+
if (cookie === void 0) return void 0;
|
|
436
|
+
return {
|
|
437
|
+
netns,
|
|
438
|
+
cookie,
|
|
439
|
+
records: run.kernel.filter((record) => record.netns === netns && record.cookie === cookie)
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
const portOf = (address) => address.split(":").at(-1) ?? "";
|
|
443
|
+
/**
|
|
444
|
+
* This function gives the socket of the target for a socket of the client. In a direct pair, its
|
|
445
|
+
* peer is the address of the client socket. Behind a router, its peer is the address of the router
|
|
446
|
+
* with the port of the client socket. The router keeps the port when it is free.
|
|
447
|
+
*/
|
|
448
|
+
const peerPlace = (run, local) => {
|
|
449
|
+
const peer = run.topology === void 0 ? local : `${run.topology.targetPeer}:${portOf(local)}`;
|
|
450
|
+
return placeOf(run, run.targetNetns, (record) => record.dst === peer);
|
|
451
|
+
};
|
|
452
|
+
/** This function gives the kernel place of a socket of a role at an instant. The role `server` is a socket of the target. */
|
|
453
|
+
const kernelOf = (run, role, at) => {
|
|
454
|
+
if (role === "server") {
|
|
455
|
+
const pool = socketAt(run.events, "pool", at);
|
|
456
|
+
return {
|
|
457
|
+
socket: pool,
|
|
458
|
+
place: pool === void 0 ? void 0 : peerPlace(run, pool.local)
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const socket = socketAt(run.events, role, at);
|
|
462
|
+
return {
|
|
463
|
+
socket,
|
|
464
|
+
place: socket === void 0 ? void 0 : placeOf(run, run.netns, (record) => record.src === socket.local)
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
const newState$1 = (record) => record.new;
|
|
468
|
+
const oldState$1 = (record) => record.old;
|
|
469
|
+
/** This function gives the instant that a run starts to look for its outcome. It is the fault, the reply before it, or the start of the client. */
|
|
470
|
+
const outsetOf = (run) => {
|
|
471
|
+
const ready = run.events.find((event) => event.ev === "ready");
|
|
472
|
+
return run.fault?.t0 ?? ready?.t ?? run.events[0]?.t ?? 0;
|
|
473
|
+
};
|
|
474
|
+
/**
|
|
475
|
+
* This function gives the instant of a reference in a run, on a socket of the driver:
|
|
476
|
+
* - `last-received`: the last segment that the socket received before the fault held. A band of
|
|
477
|
+
* the kernel counts from the last segment of any kind. A band that a line ends is a timer of
|
|
478
|
+
* the driver. It counts from the last segment with data that the socket received or sent.
|
|
479
|
+
* - `request-sent`: the send of the operation into the fault, the second `sent` line.
|
|
480
|
+
* - `first-retransmission`: the first retransmission of the RTO timer on the socket.
|
|
481
|
+
* - `peer-closed`: the change of state of the socket of the target, when its process dies.
|
|
482
|
+
* - `fault`: the start of the commit of the fault. `client-start`: the `connecting` line.
|
|
483
|
+
*/
|
|
484
|
+
const referenceOf = (run, from, role, data) => {
|
|
485
|
+
const ready = run.events.find((event) => event.ev === "ready");
|
|
486
|
+
const before = run.fault?.t1 ?? ready?.t ?? Infinity;
|
|
487
|
+
if (from === "fault") return run.fault?.t0;
|
|
488
|
+
if (from === "client-start") return (run.events.find((event) => event.ev === "connecting") ?? run.events[0])?.t;
|
|
489
|
+
if (from === "request-sent") return run.events.filter((event) => event.ev === "sent")[1]?.t;
|
|
490
|
+
const { socket, place } = kernelOf(run, role, before);
|
|
491
|
+
if (place === void 0) return void 0;
|
|
492
|
+
if (from === "last-received") {
|
|
493
|
+
const counts = (record) => data ? (record.ev === "rx" || record.ev === "tx") && (record.seg?.len.value ?? 0) > 0 : record.ev === "rx";
|
|
494
|
+
return place.records.filter((record) => record.mono <= before && counts(record)).at(-1)?.mono;
|
|
495
|
+
}
|
|
496
|
+
if (from === "first-retransmission") {
|
|
497
|
+
const request = run.events.filter((event) => event.ev === "sent")[1]?.t ?? before;
|
|
498
|
+
return place.records.find((record) => record.ev === "retrans" && record.why === "rto" && record.mono > request)?.mono;
|
|
499
|
+
}
|
|
500
|
+
return (socket === void 0 ? void 0 : peerPlace(run, socket.local))?.records.find((record) => record.ev === "state" && oldState$1(record) === 1)?.mono;
|
|
501
|
+
};
|
|
502
|
+
/** This function gives the first line of the client that matches, after an instant. */
|
|
503
|
+
const lineAfter = (events, match, after) => events.find((event) => event.t > after && matches(event, match));
|
|
504
|
+
/** This function gives the name of the error of a line: the field that the match names, or `name`. */
|
|
505
|
+
const nameOf = (line, match) => String(fieldOf(line, match.name ?? "name") ?? line.ev);
|
|
506
|
+
/** This function gives the report of the kernel on a socket after an instant: an error, or the end of the file (CLOSE_WAIT). */
|
|
507
|
+
const reportOf = (place, after) => {
|
|
508
|
+
const report = place.records.find((record) => record.mono > after && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && newState$1(record) === 8));
|
|
509
|
+
if (report === void 0) return void 0;
|
|
510
|
+
return report.ev === "error" ? {
|
|
511
|
+
at: report.mono,
|
|
512
|
+
outcome: {
|
|
513
|
+
kind: "error",
|
|
514
|
+
errno: ERRNO$1[report.err] ?? `E${report.err}`
|
|
515
|
+
}
|
|
516
|
+
} : {
|
|
517
|
+
at: report.mono,
|
|
518
|
+
outcome: { kind: "eof" }
|
|
519
|
+
};
|
|
520
|
+
};
|
|
521
|
+
/** This function gives the latest instant of the bands of a driver prediction, in seconds from their references. */
|
|
522
|
+
const longestOf = (cell) => {
|
|
523
|
+
const { prediction } = cell;
|
|
524
|
+
if (prediction.detects === null) return valueIn(prediction.window, "s");
|
|
525
|
+
const steps = (prediction.driver?.then ?? []).map((step) => valueIn(step.high, "s"));
|
|
526
|
+
const own = prediction.detects ? valueIn(prediction.high, "s") : valueIn(prediction.window, "s");
|
|
527
|
+
return Math.max(own, ...steps);
|
|
528
|
+
};
|
|
529
|
+
/**
|
|
530
|
+
* This function tells if a run of a driver cell has its outcome, so that its wait can stop. For a
|
|
531
|
+
* detection, the outcome is the end of its band and of each later band. A silent prediction waits
|
|
532
|
+
* for its window.
|
|
533
|
+
*/
|
|
534
|
+
const settled = (cell, run) => {
|
|
535
|
+
const { prediction } = cell;
|
|
536
|
+
if (prediction.detects !== true || prediction.driver === void 0) return false;
|
|
537
|
+
const driver = prediction.driver;
|
|
538
|
+
const after = outsetOf(run);
|
|
539
|
+
return (driver.ends === void 0 ? (() => {
|
|
540
|
+
const { place } = kernelOf(run, driver.socket, run.fault?.t1 ?? after);
|
|
541
|
+
return place !== void 0 && reportOf(place, after) !== void 0;
|
|
542
|
+
})() : lineAfter(run.events, driver.ends, after) !== void 0) && (driver.then ?? []).every((step) => lineAfter(run.events, step.ends, after) !== void 0);
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* This function examines the keepalive timer of a socket between two instants. A firing two
|
|
546
|
+
* jiffies or more behind its wheel slot was delayed by the host, which stopped a vCPU. A firing
|
|
547
|
+
* that found the socket held by its owner put itself off by HZ/20. Either way, the run measured
|
|
548
|
+
* something other than the timer.
|
|
549
|
+
*/
|
|
550
|
+
const delayed = (place, from, to) => {
|
|
551
|
+
const firings = place.records.filter((record) => record.ev === "timer" && record.what === "keepalive" && record.mono >= from && record.mono <= to);
|
|
552
|
+
const late = firings.find((record) => (record.behind?.value ?? 0) >= 2);
|
|
553
|
+
if (late !== void 0) return `its keepalive timer ran ${late.behind.value} jiffies behind its wheel slot: the VM delayed it, not the kernel`;
|
|
554
|
+
return firings.find((record) => record.owned === true) === void 0 ? null : "its keepalive timer found the socket held by its owner and put itself off by HZ/20: the run measured the owner, not the timer";
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* This function reads one run of a driver cell.
|
|
558
|
+
* - The band of the cell counts from its reference on its socket. It ends at its line, or at the
|
|
559
|
+
* report of the kernel on that socket.
|
|
560
|
+
* - Each later band (`then`) gets the same test. A later band outside its band, or absent, is a finding.
|
|
561
|
+
* - A line that the prediction names as quiet, after the fault and in its window, is a finding.
|
|
562
|
+
* So is a flow of an open socket of the driver that a router forgot, for a silent prediction.
|
|
563
|
+
* - A silent prediction can time a reply (`reply`), and the verdict tests it against `replies`.
|
|
564
|
+
*
|
|
565
|
+
* A run is void when the prober lost records, or when the client did not get its first reply. It
|
|
566
|
+
* is also void when the operation went out before the fault held. A late lift of a hold and a
|
|
567
|
+
* keepalive probe before the fault also make it void. So do a delayed timer and a round trip
|
|
568
|
+
* above the assumption.
|
|
569
|
+
*/
|
|
570
|
+
const measureDriver = (cell, run) => {
|
|
571
|
+
const { prediction } = cell;
|
|
572
|
+
if (prediction.detects === null || prediction.driver === void 0) throw new Error(`${cell.name} has no driver band`);
|
|
573
|
+
const driver = prediction.driver;
|
|
574
|
+
const { events } = run;
|
|
575
|
+
const ready = events.find((event) => event.ev === "ready");
|
|
576
|
+
const request = events.filter((event) => event.ev === "sent")[1];
|
|
577
|
+
const after = outsetOf(run);
|
|
578
|
+
const before = run.fault?.t1 ?? ready?.t ?? after;
|
|
579
|
+
const { place } = kernelOf(run, driver.socket, before);
|
|
580
|
+
const records = place?.records ?? [];
|
|
581
|
+
const srtt = kernelOf(run, "pool", before).place?.records.find((record) => record.tcp?.srtt !== void 0)?.tcp?.srtt;
|
|
582
|
+
const rtt = srtt === void 0 ? null : quantity(valueIn(srtt, "us") / 1e3, "ms");
|
|
583
|
+
const base = {
|
|
584
|
+
environment: run.environment,
|
|
585
|
+
index: run.index,
|
|
586
|
+
probes: records.filter((record) => record.ev === "probe" && record.what === "keepalive").length,
|
|
587
|
+
retransmissions: records.filter((record) => record.ev === "retrans").length,
|
|
588
|
+
acks: request === void 0 ? 0 : records.filter((record) => record.ev === "rx" && record.mono > request.t).length,
|
|
589
|
+
rtt
|
|
590
|
+
};
|
|
591
|
+
const nothing = {
|
|
592
|
+
outcome: { kind: "silent" },
|
|
593
|
+
elapsed: null,
|
|
594
|
+
socket: null,
|
|
595
|
+
noticed: null,
|
|
596
|
+
steps: [],
|
|
597
|
+
finding: null
|
|
598
|
+
};
|
|
599
|
+
if (cell.before !== true && ready === void 0) return {
|
|
600
|
+
...base,
|
|
601
|
+
...nothing,
|
|
602
|
+
void: "the client never had its reply"
|
|
603
|
+
};
|
|
604
|
+
if (run.lost === null) return {
|
|
605
|
+
...base,
|
|
606
|
+
...nothing,
|
|
607
|
+
void: "the prober wrote no clock line after the run, so nothing says what it dropped"
|
|
608
|
+
};
|
|
609
|
+
if (run.lost > 0) return {
|
|
610
|
+
...base,
|
|
611
|
+
...nothing,
|
|
612
|
+
void: `the prober dropped ${run.lost} records`
|
|
613
|
+
};
|
|
614
|
+
let voided = null;
|
|
615
|
+
if (cell.probe.mode !== "idle" && cell.before !== true) {
|
|
616
|
+
if (request === void 0) voided = "the operation was never sent into the fault";
|
|
617
|
+
else if (run.fault !== null && run.fault.t1 > request.t) voided = "the operation went out before the fault held";
|
|
618
|
+
}
|
|
619
|
+
const keepalive = records.find((record) => record.ev === "probe" && record.what === "keepalive");
|
|
620
|
+
if (voided === null && run.fault !== null && keepalive !== void 0 && keepalive.mono < run.fault.t1) voided = "a keepalive probe went out before the fault held";
|
|
621
|
+
if (voided === null && cell.hold !== void 0 && run.fault !== null) {
|
|
622
|
+
const heal = run.heal ?? null;
|
|
623
|
+
if (heal === null) voided = "the fault never lifted";
|
|
624
|
+
else {
|
|
625
|
+
const late = (heal.t0 - run.fault.t0) / 1e9 - valueIn(cell.hold, "s");
|
|
626
|
+
if (late > valueIn(NOISE, "s")) voided = `the lift came ${Math.round(late * 1e4) / 10} ms after its hold: the injector was late, not the client`;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const assumed = prediction.rtt;
|
|
630
|
+
if (voided === null && assumed !== void 0 && rtt !== null && valueIn(rtt, "ms") > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt.value} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
|
|
631
|
+
const findings = [];
|
|
632
|
+
const noise = valueIn(NOISE, "s");
|
|
633
|
+
const until = prediction.detects ? Infinity : after + valueIn(prediction.window, "s") * 1e9;
|
|
634
|
+
for (const match of driver.quiet ?? []) {
|
|
635
|
+
const line = events.find((event) => event.t > after && event.t <= until && matches(event, match));
|
|
636
|
+
if (line !== void 0) findings.push(`${wordsOf(match)} came ${between$2(after, line.t).value} s after the fault, where none should`);
|
|
637
|
+
}
|
|
638
|
+
if (!prediction.detects && run.middlebox !== void 0) for (const socket of socketsOf(events)) {
|
|
639
|
+
const forgot = run.middlebox.find((line) => line.ev === "ct" && line["type"] === "DESTROY" && String(line["orig"] ?? "").startsWith(`${socket.local}>`) && line.t > socket.opened && (socket.ended === null || line.t < socket.ended) && line.t <= until);
|
|
640
|
+
if (forgot !== void 0) findings.push(`a router forgot the flow of the ${socket.role} socket ${between$2(after, forgot.t).value} s after the fault, where it should keep it`);
|
|
641
|
+
}
|
|
642
|
+
const stepTimes = (driver.then ?? []).map((step) => {
|
|
643
|
+
const line = lineAfter(events, step.ends, after);
|
|
644
|
+
const from = referenceOf(run, step.from, step.socket ?? driver.socket, true);
|
|
645
|
+
if (line === void 0 || from === void 0) {
|
|
646
|
+
findings.push(line === void 0 ? `${step.what}: ${wordsOf(step.ends)} never came` : `${step.what}: its reference (${step.from}) never came`);
|
|
647
|
+
return {
|
|
648
|
+
what: step.what,
|
|
649
|
+
name: line === void 0 ? null : nameOf(line, step.ends),
|
|
650
|
+
elapsed: null
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
const elapsed = between$2(from, line.t);
|
|
654
|
+
const name = nameOf(line, step.ends);
|
|
655
|
+
if (step.name !== "OK" && name !== step.name) findings.push(`${step.what}: it ended with ${name}, not ${step.name}`);
|
|
656
|
+
const [low, high] = [round(valueIn(step.low, "s") - noise), round(valueIn(step.high, "s") + noise)];
|
|
657
|
+
if (elapsed.value < low || elapsed.value > high) findings.push(`${step.what}: ${elapsed.value} s is outside [${low}, ${high}] s`);
|
|
658
|
+
return {
|
|
659
|
+
what: step.what,
|
|
660
|
+
name,
|
|
661
|
+
elapsed
|
|
662
|
+
};
|
|
663
|
+
});
|
|
664
|
+
const stepped = stepTimes.length === 0 ? {} : { driver: { steps: stepTimes } };
|
|
665
|
+
if (!prediction.detects) {
|
|
666
|
+
if (driver.reply === void 0 || prediction.replies === void 0) return {
|
|
667
|
+
...base,
|
|
668
|
+
...nothing,
|
|
669
|
+
...stepped,
|
|
670
|
+
void: voided,
|
|
671
|
+
finding: findings[0] ?? null
|
|
672
|
+
};
|
|
673
|
+
const line = lineAfter(events, driver.reply.ends, after);
|
|
674
|
+
const from = referenceOf(run, driver.reply.from, driver.socket, true);
|
|
675
|
+
if (line === void 0 || from === void 0) return {
|
|
676
|
+
...base,
|
|
677
|
+
...nothing,
|
|
678
|
+
...stepped,
|
|
679
|
+
void: voided,
|
|
680
|
+
finding: findings[0] ?? `the reply never came: ${wordsOf(driver.reply.ends)}`
|
|
681
|
+
};
|
|
682
|
+
return {
|
|
683
|
+
...base,
|
|
684
|
+
...nothing,
|
|
685
|
+
...stepped,
|
|
686
|
+
outcome: { kind: "answered" },
|
|
687
|
+
elapsed: between$2(from, line.t),
|
|
688
|
+
void: voided,
|
|
689
|
+
finding: findings[0] ?? null
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
const line = driver.ends === void 0 ? void 0 : lineAfter(events, driver.ends, after);
|
|
693
|
+
const report = driver.ends === void 0 && place !== void 0 ? reportOf(place, after) : void 0;
|
|
694
|
+
if (driver.ends === void 0 && place === void 0) return {
|
|
695
|
+
...base,
|
|
696
|
+
...nothing,
|
|
697
|
+
...stepped,
|
|
698
|
+
void: voided ?? `the prober saw nothing of the ${driver.socket} socket`,
|
|
699
|
+
finding: findings[0] ?? null
|
|
700
|
+
};
|
|
701
|
+
const ended = line?.t ?? report?.at;
|
|
702
|
+
const outcome = line !== void 0 && driver.ends !== void 0 ? {
|
|
703
|
+
kind: "error",
|
|
704
|
+
errno: nameOf(line, driver.ends)
|
|
705
|
+
} : report?.outcome ?? { kind: "silent" };
|
|
706
|
+
if (ended === void 0) return {
|
|
707
|
+
...base,
|
|
708
|
+
...nothing,
|
|
709
|
+
...stepped,
|
|
710
|
+
void: voided,
|
|
711
|
+
finding: findings[0] ?? `nothing detected it: ${prediction.timer} never fired`
|
|
712
|
+
};
|
|
713
|
+
const reference = referenceOf(run, prediction.from, driver.socket, driver.ends !== void 0);
|
|
714
|
+
if (reference === void 0) return {
|
|
715
|
+
...base,
|
|
716
|
+
...nothing,
|
|
717
|
+
...stepped,
|
|
718
|
+
outcome,
|
|
719
|
+
void: voided,
|
|
720
|
+
finding: `it ended without its reference event (${prediction.from} on the ${driver.socket} socket)`
|
|
721
|
+
};
|
|
722
|
+
if (driver.ends === void 0 && prediction.from === "last-received" && place !== void 0) voided ??= delayed(place, reference, ended);
|
|
723
|
+
const name = outcome.kind === "error" ? outcome.errno : outcome.kind === "eof" ? "EOF" : "silent";
|
|
724
|
+
if (prediction.errno !== "OK" && name !== prediction.errno) findings.unshift(`it ended with ${name}, not ${prediction.errno}`);
|
|
725
|
+
const seen = report === void 0 ? void 0 : events.find((event) => event.t >= report.at && (event.ev === "error" || event.ev === "socket-error"));
|
|
726
|
+
return {
|
|
727
|
+
...base,
|
|
728
|
+
...stepped,
|
|
729
|
+
outcome,
|
|
730
|
+
elapsed: between$2(reference, ended),
|
|
731
|
+
socket: seen === void 0 ? null : between$2(reference, seen.t),
|
|
732
|
+
noticed: report === void 0 || seen === void 0 ? null : between$2(report.at, seen.t),
|
|
733
|
+
steps: (prediction.from === "last-received" ? records.filter((record) => record.ev === "probe" && record.what === "keepalive" && record.mono > reference) : []).map((record) => between$2(reference, record.mono)),
|
|
734
|
+
void: voided,
|
|
735
|
+
finding: findings[0] ?? null
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
//#endregion
|
|
365
739
|
//#region src/timeline.ts
|
|
366
740
|
/**
|
|
367
741
|
* This module measures a run of a group cell. It gives the first write that a primary accepted
|
|
368
|
-
* after the fault, and the timeline of the group
|
|
742
|
+
* after the fault, and the timeline of the group. The Sentinels of a group announce its timeline.
|
|
743
|
+
* The logs of the members of a replica set and its client tell it. The module also gives what
|
|
369
744
|
* became of each write.
|
|
370
745
|
*/
|
|
371
|
-
/** These are the stages of a timeline, in their sequence after a fault. */
|
|
746
|
+
/** These are the stages of a timeline under Sentinels, in their sequence after a fault. */
|
|
372
747
|
const STAGES = [
|
|
373
748
|
"+sdown",
|
|
374
749
|
"+odown",
|
|
@@ -398,6 +773,105 @@ const stagesOf = (timeline, from) => {
|
|
|
398
773
|
}
|
|
399
774
|
return out;
|
|
400
775
|
};
|
|
776
|
+
/** These are the stages of the timeline of a replica set, in their usual sequence after a fault. */
|
|
777
|
+
const REPLICA_SET_STAGES = [
|
|
778
|
+
"member-down",
|
|
779
|
+
"election-start",
|
|
780
|
+
"elected",
|
|
781
|
+
"caught-up",
|
|
782
|
+
"writable",
|
|
783
|
+
"old-primary-steps-down",
|
|
784
|
+
"discovered",
|
|
785
|
+
"written-off",
|
|
786
|
+
"first-write"
|
|
787
|
+
];
|
|
788
|
+
/** These are the log lines of mongod that mark each stage of a member. */
|
|
789
|
+
const MEMBER_MARKS = {
|
|
790
|
+
"member-down": {
|
|
791
|
+
ids: [21216],
|
|
792
|
+
message: /now in state DOWN/,
|
|
793
|
+
at: "other"
|
|
794
|
+
},
|
|
795
|
+
"election-start": {
|
|
796
|
+
ids: [4615652, 21437],
|
|
797
|
+
message: /^Starting an election|Skipping dry run|Conducting a dry run/,
|
|
798
|
+
at: "other"
|
|
799
|
+
},
|
|
800
|
+
elected: {
|
|
801
|
+
ids: [21450],
|
|
802
|
+
message: /^Election succeeded/,
|
|
803
|
+
at: "other"
|
|
804
|
+
},
|
|
805
|
+
"caught-up": {
|
|
806
|
+
ids: [21363],
|
|
807
|
+
message: /^Exited primary catch-up mode/,
|
|
808
|
+
at: "other"
|
|
809
|
+
},
|
|
810
|
+
writable: {
|
|
811
|
+
ids: [21331],
|
|
812
|
+
message: /^Transition to primary complete/,
|
|
813
|
+
at: "other"
|
|
814
|
+
},
|
|
815
|
+
"old-primary-steps-down": {
|
|
816
|
+
ids: [
|
|
817
|
+
21809,
|
|
818
|
+
21475,
|
|
819
|
+
962660
|
|
820
|
+
],
|
|
821
|
+
message: /relinquishing primary|Stepping down from primary/,
|
|
822
|
+
at: "old"
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
/** This function tells if a log line of a member marks a stage, with the old primary of the run. */
|
|
826
|
+
const marks = (line, stage, old, oldAddress) => {
|
|
827
|
+
const mark = MEMBER_MARKS[stage];
|
|
828
|
+
const member = String(line["member"] ?? "");
|
|
829
|
+
if (mark.at === "old" !== (member === old)) return false;
|
|
830
|
+
const attr = line["attr"] ?? {};
|
|
831
|
+
if (stage === "member-down" && !JSON.stringify(attr).includes(oldAddress)) return false;
|
|
832
|
+
if (stage === "old-primary-steps-down" && Number(line["id"]) === 21358 && attr["oldState"] === "PRIMARY") return true;
|
|
833
|
+
return mark.ids.includes(Number(line["id"])) || mark.message.test(String(line["msg"] ?? ""));
|
|
834
|
+
};
|
|
835
|
+
/**
|
|
836
|
+
* This function gives the time of each stage of a replica set from an instant, in a run. The stages
|
|
837
|
+
* of the members come from their log lines (`ev` "mongod"), whose times the lab puts on the
|
|
838
|
+
* monotonic clock. The stages of the client come from its lines:
|
|
839
|
+
* - `discovered`: a member other than the old primary becomes RSPrimary in the view of the client.
|
|
840
|
+
* - `written-off`: the first heartbeat of the client to the old primary that fails.
|
|
841
|
+
* - `first-write`: the first write that a primary accepts, of a write that the client sent after
|
|
842
|
+
* the commit of the fault (`sentAfter`). It is on a new primary when `lands` is `new`.
|
|
843
|
+
*/
|
|
844
|
+
const replicaSetStagesOf = (run, from, sentAfter, lands) => {
|
|
845
|
+
const out = {};
|
|
846
|
+
const old = run.group?.primary ?? "";
|
|
847
|
+
const oldAddress = `${run.group?.members[old]?.ip ?? ""}:`;
|
|
848
|
+
for (const line of run.group?.timeline ?? []) {
|
|
849
|
+
if (line.ev !== "mongod" || line.t < from) continue;
|
|
850
|
+
for (const stage of Object.keys(MEMBER_MARKS)) if (out[stage] === void 0 && marks(line, stage, old, oldAddress)) out[stage] = between$1(from, line.t);
|
|
851
|
+
}
|
|
852
|
+
const isOld = (address) => typeof address === "string" && address.startsWith(oldAddress);
|
|
853
|
+
for (const event of run.events) {
|
|
854
|
+
if (event.t < from) continue;
|
|
855
|
+
if (out.discovered === void 0 && event.ev === "sdam" && event["event"] === "server-changed" && !isOld(event["address"])) {
|
|
856
|
+
if (event["next"]?.type === "RSPrimary") out.discovered = between$1(from, event.t);
|
|
857
|
+
}
|
|
858
|
+
if (out["written-off"] === void 0 && event.ev === "sdam" && event["event"] === "heartbeat-failed" && isOld(event["address"])) out["written-off"] = between$1(from, event.t);
|
|
859
|
+
}
|
|
860
|
+
const accepted = acceptedAfter(run, sentAfter, lands);
|
|
861
|
+
if (accepted !== void 0) out["first-write"] = between$1(from, accepted.t);
|
|
862
|
+
return out;
|
|
863
|
+
};
|
|
864
|
+
/** This function gives the first write that a primary accepted, of a write sent after an instant. It is on a new primary for `new`. */
|
|
865
|
+
const acceptedAfter = (run, sentAfter, lands) => {
|
|
866
|
+
const sent = /* @__PURE__ */ new Map();
|
|
867
|
+
for (const event of run.events) if (event.ev === "sent") sent.set(Number(event["n"]), event.t);
|
|
868
|
+
const primary = run.group?.primary ?? "";
|
|
869
|
+
return run.events.find((event) => {
|
|
870
|
+
if (event.ev !== "ack" || (sent.get(Number(event["n"])) ?? -Infinity) <= sentAfter) return false;
|
|
871
|
+
const member = memberAt(run, event["at"]);
|
|
872
|
+
return member !== null && (lands === "new" ? member !== primary : true);
|
|
873
|
+
});
|
|
874
|
+
};
|
|
401
875
|
/** This function gives what became of each write of a run, from the lines of the client and the keys that the group kept. */
|
|
402
876
|
const writesOf = (run, after, primary) => {
|
|
403
877
|
const { events } = run;
|
|
@@ -470,7 +944,10 @@ const lagsOf = (run, kernel) => {
|
|
|
470
944
|
* - The reference is the start of the commit of the fault, as the injector stamped it. For a
|
|
471
945
|
* stall, it is the first write into the fault, as for the stall of a direct pair.
|
|
472
946
|
* - The outcome is the first write that a primary accepted after the fault. The prediction says
|
|
473
|
-
* which primary: a new one, or the same one after the lift.
|
|
947
|
+
* which primary: a new one, or the same one after the lift. In a replica set, it is the first
|
|
948
|
+
* accepted write of a write that the client sent after the commit of the fault.
|
|
949
|
+
* - The stages come from the Sentinels, or from the logs of the members of a replica set and the
|
|
950
|
+
* lines of its client (`replicaSetStagesOf`).
|
|
474
951
|
* - Its time is the last segment with data that the client's socket to that primary received
|
|
475
952
|
* before the client saw the acceptance. The stamp of the client is the fallback.
|
|
476
953
|
* - A run is void when the prober lost records, or when the client did not become ready. It is
|
|
@@ -526,16 +1003,25 @@ const measureGroup = (cell, run) => {
|
|
|
526
1003
|
group: empty
|
|
527
1004
|
};
|
|
528
1005
|
const timeline = run.group?.timeline ?? [];
|
|
529
|
-
const stages = stagesOf(timeline, t0);
|
|
530
1006
|
const heal = run.heal ?? null;
|
|
531
1007
|
const lands = expected?.lands ?? "new";
|
|
532
|
-
const
|
|
1008
|
+
const replica = cell.replicaSet !== void 0;
|
|
1009
|
+
const stall = prediction.detects === false && prediction.recovers !== void 0;
|
|
1010
|
+
const stages = replica ? replicaSetStagesOf(run, t0, run.fault.t1, lands) : stagesOf(timeline, t0);
|
|
1011
|
+
const after = lands === "same" && heal !== null && (!replica || stall) ? heal.t0 : t0;
|
|
1012
|
+
const sentAt = /* @__PURE__ */ new Map();
|
|
1013
|
+
for (const event of events) if (event.ev === "sent") sentAt.set(Number(event["n"]), event.t);
|
|
533
1014
|
const accepted = events.find((event) => {
|
|
534
1015
|
if (event.ev !== "ack" || event.t <= after) return false;
|
|
1016
|
+
if (replica && (sentAt.get(Number(event["n"])) ?? -Infinity) <= run.fault.t1) return false;
|
|
535
1017
|
const member = memberAt(run, event["at"]);
|
|
536
1018
|
return member !== null && (lands === "new" ? member !== primary : true);
|
|
537
1019
|
});
|
|
538
1020
|
const to = accepted === void 0 ? null : memberAt(run, accepted["at"]);
|
|
1021
|
+
if (replica) {
|
|
1022
|
+
delete stages["first-write"];
|
|
1023
|
+
if (accepted !== void 0) stages["first-write"] = between$1(t0, accepted.t);
|
|
1024
|
+
}
|
|
539
1025
|
const group = {
|
|
540
1026
|
from: primary,
|
|
541
1027
|
to,
|
|
@@ -560,7 +1046,7 @@ const measureGroup = (cell, run) => {
|
|
|
560
1046
|
}
|
|
561
1047
|
}
|
|
562
1048
|
const assumed = prediction.detects === null ? void 0 : prediction.rtt;
|
|
563
|
-
const first = kernel.find((record) => record.netns === run.netns && record.dst === `${run.group?.members[primary]?.ip}:${cell.group?.port}` && record.tcp?.srtt !== void 0);
|
|
1049
|
+
const first = kernel.find((record) => record.netns === run.netns && record.dst === `${run.group?.members[primary]?.ip}:${(cell.group ?? cell.replicaSet)?.port}` && record.tcp?.srtt !== void 0);
|
|
564
1050
|
const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
|
|
565
1051
|
if (voided === null && assumed !== void 0 && rtt !== null && rtt > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
|
|
566
1052
|
if (voided === null && prediction.detects === true && prediction.from === "fault" && (run.fault.t1 - run.fault.t0) / 1e6 > valueIn(NOISE, "ms")) voided = `the commit of the fault took ${Math.round((run.fault.t1 - run.fault.t0) / 1e5) / 10} ms, longer than the noise of the pipeline: its start is not known`;
|
|
@@ -576,8 +1062,12 @@ const measureGroup = (cell, run) => {
|
|
|
576
1062
|
}
|
|
577
1063
|
}
|
|
578
1064
|
const findings = [];
|
|
579
|
-
const switched = stages["+switch-master"];
|
|
1065
|
+
const switched = replica ? stages["elected"] : stages["+switch-master"];
|
|
580
1066
|
if (expected?.failover === false && switched !== void 0) findings.push(`the group failed over ${switched.value} s after the fault, where it should not`);
|
|
1067
|
+
for (const stage of expected?.quiet ?? []) {
|
|
1068
|
+
const at = stages[stage];
|
|
1069
|
+
if (at !== void 0) findings.push(`${stage} came ${at.value} s after the fault, where it should not`);
|
|
1070
|
+
}
|
|
581
1071
|
if (accepted !== void 0 && lands === "same" && to !== primary) findings.push(`the client wrote to ${to ?? String(accepted["at"])}, where it should stay on ${primary}`);
|
|
582
1072
|
for (const error of expected?.errors ?? []) {
|
|
583
1073
|
const met = group.errors.find((each) => each.name === error.name);
|
|
@@ -595,7 +1085,8 @@ const measureGroup = (cell, run) => {
|
|
|
595
1085
|
};
|
|
596
1086
|
}
|
|
597
1087
|
const address = String(accepted["at"]);
|
|
598
|
-
const
|
|
1088
|
+
const others = new Set(socketsOf(events).filter((socket) => socket.role !== "pool").map((socket) => socket.local));
|
|
1089
|
+
const segment = kernel.filter((record) => record.netns === run.netns && record.ev === "rx" && record.dst === address && !others.has(String(record.src)) && (record.seg?.len.value ?? 0) > 0 && record.mono <= accepted.t && record.mono > after).at(-1);
|
|
599
1090
|
const at = segment?.mono ?? accepted.t;
|
|
600
1091
|
return {
|
|
601
1092
|
outcome: { kind: "answered" },
|
|
@@ -761,9 +1252,61 @@ const crossingsOf = (run, hop, toClient, toTarget) => {
|
|
|
761
1252
|
* it, and before the first keepalive probe. A live peer answers that probe, and the answer
|
|
762
1253
|
* starts the idle time again. A run where the fault was late is void. A run whose records the
|
|
763
1254
|
* prober lost is also void.
|
|
1255
|
+
* - For an HTTP client, it also reads what the second request met (`requestOf`). An outcome other
|
|
1256
|
+
* than the one that the prediction gives is a finding.
|
|
764
1257
|
*/
|
|
765
1258
|
const measure = (cell, run) => {
|
|
766
|
-
|
|
1259
|
+
const read = measureSocket(cell, run);
|
|
1260
|
+
const expected = cell.prediction.request;
|
|
1261
|
+
if (cell.probe.http === void 0 && expected === void 0) return read;
|
|
1262
|
+
const request = requestOf(run.events);
|
|
1263
|
+
if (expected === void 0 || read.void !== null) return {
|
|
1264
|
+
...read,
|
|
1265
|
+
request
|
|
1266
|
+
};
|
|
1267
|
+
const differs = requestFinding(expected, request);
|
|
1268
|
+
const finding = read.finding === null ? differs : differs === null ? read.finding : `${read.finding}. ${differs}`;
|
|
1269
|
+
return {
|
|
1270
|
+
...read,
|
|
1271
|
+
request,
|
|
1272
|
+
finding
|
|
1273
|
+
};
|
|
1274
|
+
};
|
|
1275
|
+
/** This function tells how a request reading differs from its prediction, or gives null. */
|
|
1276
|
+
const requestFinding = (expected, read) => {
|
|
1277
|
+
if (read === null) return `the second request never went out, where it should meet: ${requestWords(expected)}`;
|
|
1278
|
+
return read.connection === expected.connection && read.retried === expected.retried && read.error === expected.error && read.pending === (expected.pending === true) ? null : `the second request met: ${requestWords(read)}, not ${requestWords(expected)}`;
|
|
1279
|
+
};
|
|
1280
|
+
/**
|
|
1281
|
+
* This function reads the second request of an HTTP client from its lines. Its first try is its
|
|
1282
|
+
* first `request` line, and each `retry` line is a new try. Its end is its `error` line, or its
|
|
1283
|
+
* `replied` line. A request with neither is pending. It gives null when the client wrote no try.
|
|
1284
|
+
*
|
|
1285
|
+
* A new try that gets no connection writes no `retry` line. Its dial writes a `dial-error` line.
|
|
1286
|
+
* Thus a `dial-error` line after the first try and before the end is also a new try.
|
|
1287
|
+
*/
|
|
1288
|
+
const requestOf = (events) => {
|
|
1289
|
+
const of = (event) => Number(event["i"]) === 2;
|
|
1290
|
+
const tries = events.filter((event) => (event.ev === "request" || event.ev === "retry") && of(event));
|
|
1291
|
+
const first = tries[0];
|
|
1292
|
+
if (first === void 0) return null;
|
|
1293
|
+
const failed = events.find((event) => event.ev === "error" && of(event));
|
|
1294
|
+
const replied = events.some((event) => event.ev === "replied" && of(event));
|
|
1295
|
+
const ready = events.find((event) => event.ev === "ready");
|
|
1296
|
+
const sent = events.find((event) => event.ev === "sent" && of(event));
|
|
1297
|
+
const end = failed ?? events.find((event) => event.ev === "done" && of(event));
|
|
1298
|
+
const dialed = events.some((event) => event.ev === "dial-error" && event.t > first.t && (end === void 0 || event.t <= end.t));
|
|
1299
|
+
return {
|
|
1300
|
+
connection: first["reused"] === true ? "reused" : "new",
|
|
1301
|
+
retried: dialed || tries.some((each) => each.ev === "retry"),
|
|
1302
|
+
error: failed === void 0 ? null : String(failed["errno"]),
|
|
1303
|
+
pending: failed === void 0 && !replied,
|
|
1304
|
+
gap: ready === void 0 || sent === void 0 ? null : between(ready.t, sent.t)
|
|
1305
|
+
};
|
|
1306
|
+
};
|
|
1307
|
+
/** This function reads the socket of one run against its cell, as `measure` does without the second request of an HTTP client. */
|
|
1308
|
+
const measureSocket = (cell, run) => {
|
|
1309
|
+
if (cell.group !== void 0 || cell.replicaSet !== void 0) {
|
|
767
1310
|
const read = measureGroup(cell, run);
|
|
768
1311
|
return {
|
|
769
1312
|
environment: run.environment,
|
|
@@ -781,10 +1324,11 @@ const measure = (cell, run) => {
|
|
|
781
1324
|
group: read.group
|
|
782
1325
|
};
|
|
783
1326
|
}
|
|
1327
|
+
if (cell.prediction.detects !== null && cell.prediction.driver !== void 0) return measureDriver(cell, run);
|
|
784
1328
|
const { events, kernel } = run;
|
|
785
1329
|
const ready = events.find((event) => event.ev === "ready");
|
|
786
1330
|
const local = events.find((event) => event.ev === "connected")?.["local"];
|
|
787
|
-
const inflight = cell.probe.mode !== "idle";
|
|
1331
|
+
const inflight = cell.probe.mode !== "idle" && cell.probe.mode !== "gap";
|
|
788
1332
|
const request = inflight ? events.filter((event) => event.ev === "sent")[1] : ready;
|
|
789
1333
|
const server = cell.observer === "server";
|
|
790
1334
|
const place = socketOf(cell, run);
|
|
@@ -873,20 +1417,22 @@ const measure = (cell, run) => {
|
|
|
873
1417
|
const reported = socket.find((record) => record.ev === "error" && (record.err ?? 0) !== 0);
|
|
874
1418
|
const seen = remote ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
|
|
875
1419
|
const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8);
|
|
1420
|
+
const own = cell.prediction.detects === true && cell.prediction.errno === "CLOSED" ? socket.find((record) => record.ev === "state" && oldState(record) === 1 && newState(record) === 4) : void 0;
|
|
876
1421
|
const outcome = reported !== void 0 ? {
|
|
877
1422
|
kind: "error",
|
|
878
1423
|
errno: ERRNO[reported.err] ?? `E${reported.err}`
|
|
879
|
-
} : closeWait !== void 0 || seen?.ev === "eof" ? { kind: "eof" } : seen !== void 0 ? {
|
|
1424
|
+
} : closeWait !== void 0 || seen?.ev === "eof" ? { kind: "eof" } : own !== void 0 ? { kind: "closed" } : seen !== void 0 ? {
|
|
880
1425
|
kind: "error",
|
|
881
1426
|
errno: String(seen["errno"])
|
|
882
1427
|
} : { kind: "silent" };
|
|
883
|
-
const ended = reported?.mono ?? closeWait?.mono ?? seen?.t;
|
|
1428
|
+
const ended = reported?.mono ?? closeWait?.mono ?? own?.mono ?? seen?.t;
|
|
884
1429
|
const noticed = reported !== void 0 && seen !== void 0 ? between(reported.mono, seen.t) : null;
|
|
885
1430
|
const { prediction } = cell;
|
|
886
1431
|
const before = run.fault?.t1 ?? ready.t;
|
|
887
1432
|
const referenceAt = (from) => {
|
|
888
1433
|
if (from === "request-sent") return request.t;
|
|
889
1434
|
if (from === "fault") return run.fault?.t0;
|
|
1435
|
+
if (from === "client-start") return (events.find((event) => event.ev === "connecting") ?? events[0])?.t;
|
|
890
1436
|
return (from === "last-received" ? socket.filter((each) => each.ev === "rx" && each.mono <= before).at(-1) : from === "first-retransmission" ? retransmitted.find((each) => each.why === "rto") : kernel.find((each) => each.netns === peerNetns && each.cookie === peerCookie && each.ev === "state" && oldState(each) === 1))?.mono;
|
|
891
1437
|
};
|
|
892
1438
|
const lastRx = kernel.filter((each) => each.netns === run.netns && each.src === local && each.ev === "rx" && each.mono <= before).at(-1)?.mono ?? ready.t;
|
|
@@ -1056,8 +1602,9 @@ const measure = (cell, run) => {
|
|
|
1056
1602
|
finding: `it ended without its reference event (${prediction.from})`
|
|
1057
1603
|
};
|
|
1058
1604
|
const steps = (prediction.from === "last-received" ? keepalives : prediction.from === "first-retransmission" ? retransmitted.filter((record) => record.why === "rto" && record.mono > reference) : []).map((record) => between(reference, record.mono));
|
|
1059
|
-
const finding = (prediction.errno === "EOF" ? outcome.kind === "eof" : outcome.kind === "error" && outcome.errno === prediction.errno) ? null : `it ended with ${outcome.kind === "error" ? outcome.errno : "end of file"}, not ${prediction.errno}`;
|
|
1060
|
-
|
|
1605
|
+
const finding = (prediction.errno === "EOF" ? outcome.kind === "eof" : prediction.errno === "CLOSED" ? outcome.kind === "closed" : outcome.kind === "error" && outcome.errno === prediction.errno) ? null : `it ended with ${outcome.kind === "error" ? outcome.errno : outcome.kind === "closed" ? "its own close" : "end of file"}, not ${prediction.errno}`;
|
|
1606
|
+
const programTimer = cell.probe.http !== void 0 && prediction.from === "last-received" && prediction.timer !== "keepalive";
|
|
1607
|
+
if (prediction.from === "last-received" && !programTimer || prediction.from === "first-retransmission") voided ??= delayed(reference, ended, prediction.from === "last-received" ? "keepalive" : "retransmit");
|
|
1061
1608
|
const lag = (valueIn(prediction.high, "s") - valueIn(prediction.low, "s")) * 1e9;
|
|
1062
1609
|
const slow = prediction.timer.startsWith("expire_") ? (run.middlebox ?? []).find((line) => line.ev === "pump-slow" && line.t <= ended && line.t + Number(line["took"]) * 1e9 >= ended - lag) : void 0;
|
|
1063
1610
|
if (slow !== void 0) voided ??= `a listing of the router took ${Math.round(Number(slow["took"]) * 1e4) / 10} ms, longer than the bound of its prediction`;
|
|
@@ -1114,6 +1661,24 @@ const NODE = {
|
|
|
1114
1661
|
dockerfile: "harness/image/node/Dockerfile",
|
|
1115
1662
|
from: ["harness/image/node"]
|
|
1116
1663
|
};
|
|
1664
|
+
/** This image holds the Go HTTP client: the http.Transport of Go 1.27.2. */
|
|
1665
|
+
const GO_HTTP = {
|
|
1666
|
+
name: "loom-harness-go-http",
|
|
1667
|
+
dockerfile: "harness/image/go-http/Dockerfile",
|
|
1668
|
+
from: ["harness/image/go-http"]
|
|
1669
|
+
};
|
|
1670
|
+
/** This image holds the Node HTTP clients: the http.Agent of Node 24.21.0, and undici 8.12.2. */
|
|
1671
|
+
const NODE_HTTP = {
|
|
1672
|
+
name: "loom-harness-node-http",
|
|
1673
|
+
dockerfile: "harness/image/node-http/Dockerfile",
|
|
1674
|
+
from: ["harness/image/node-http"]
|
|
1675
|
+
};
|
|
1676
|
+
/** This image holds the MongoDB client: the driver mongodb 7.7.0 on Node 24.21.0, and its step-down script. */
|
|
1677
|
+
const MONGO = {
|
|
1678
|
+
name: "loom-harness-mongo",
|
|
1679
|
+
dockerfile: "harness/image/mongo/Dockerfile",
|
|
1680
|
+
from: ["harness/image/mongo"]
|
|
1681
|
+
};
|
|
1117
1682
|
/** This image is the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow. */
|
|
1118
1683
|
const ROUTER = {
|
|
1119
1684
|
name: "loom-harness-router",
|
|
@@ -1180,6 +1745,12 @@ const harnessImage = () => build(HARNESS);
|
|
|
1180
1745
|
const nodeClientImage = () => build(NODE);
|
|
1181
1746
|
/** This function gives the image of the router. */
|
|
1182
1747
|
const routerImage = () => build(ROUTER);
|
|
1748
|
+
/** This function gives the image of the Go HTTP client. */
|
|
1749
|
+
const goHttpImage = () => build(GO_HTTP);
|
|
1750
|
+
/** This function gives the image of the Node HTTP clients. */
|
|
1751
|
+
const nodeHttpImage = () => build(NODE_HTTP);
|
|
1752
|
+
/** This function gives the image of the MongoDB client. */
|
|
1753
|
+
const mongoClientImage = () => build(MONGO);
|
|
1183
1754
|
/**
|
|
1184
1755
|
* This time is the interval of the listing of a router, when its hop does not set one. A listing
|
|
1185
1756
|
* evicts each expired flow that it walks past, and the DESTROY event of that flow follows.
|
|
@@ -1322,6 +1893,21 @@ const clientOf = async (kind, image) => {
|
|
|
1322
1893
|
command: ["node", "/opt/harness/node/client-ioredis.mjs"],
|
|
1323
1894
|
clientKind: kind
|
|
1324
1895
|
};
|
|
1896
|
+
if (kind === "go-http") return {
|
|
1897
|
+
clientImage: await goHttpImage(),
|
|
1898
|
+
command: ["/opt/harness/client-go"],
|
|
1899
|
+
clientKind: kind
|
|
1900
|
+
};
|
|
1901
|
+
if (kind === "http-agent" || kind === "undici") return {
|
|
1902
|
+
clientImage: await nodeHttpImage(),
|
|
1903
|
+
command: ["node", `/opt/harness/node-http/client-${kind === "undici" ? "undici" : "agent"}.mjs`],
|
|
1904
|
+
clientKind: kind
|
|
1905
|
+
};
|
|
1906
|
+
if (kind === "mongodb") return {
|
|
1907
|
+
clientImage: await mongoClientImage(),
|
|
1908
|
+
command: ["node", "/opt/harness/mongo/client-mongo.mjs"],
|
|
1909
|
+
clientKind: kind
|
|
1910
|
+
};
|
|
1325
1911
|
return {
|
|
1326
1912
|
clientImage: image,
|
|
1327
1913
|
command: ["/opt/harness/client"],
|
|
@@ -1362,6 +1948,7 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1362
1948
|
const runtime = await getContainerRuntimeClient();
|
|
1363
1949
|
const opened = [];
|
|
1364
1950
|
const networks = [];
|
|
1951
|
+
const serverLines = lines();
|
|
1365
1952
|
const close = async () => {
|
|
1366
1953
|
for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
|
|
1367
1954
|
for (const network of networks.reverse()) await network.stop().catch(() => void 0);
|
|
@@ -1381,7 +1968,12 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1381
1968
|
try {
|
|
1382
1969
|
for (let at = 0; at <= hops.length; at += 1) networks.push(await new Network().start());
|
|
1383
1970
|
const last = networks[hops.length];
|
|
1384
|
-
|
|
1971
|
+
let stock = new GenericContainer(target.image).withNetwork(last).withWaitStrategy(Wait.forLogMessage(target.ready));
|
|
1972
|
+
if (target.files !== void 0) stock = stock.withCopyContentToContainer(Object.entries(target.files).map(([path, content]) => ({
|
|
1973
|
+
content,
|
|
1974
|
+
target: path
|
|
1975
|
+
})));
|
|
1976
|
+
if (target.lines === true) stock = stock.withLogConsumer(serverLines.consume);
|
|
1385
1977
|
const server = await (target.command === void 0 ? stock : stock.withCommand([...target.command])).start();
|
|
1386
1978
|
opened.push(server);
|
|
1387
1979
|
const serverIp = await ipOn(server, last);
|
|
@@ -1569,6 +2161,7 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1569
2161
|
};
|
|
1570
2162
|
const paused = runtime.container.getById(server.getId());
|
|
1571
2163
|
let targetAdmin;
|
|
2164
|
+
let signaller;
|
|
1572
2165
|
const hopNamed = (name) => {
|
|
1573
2166
|
const found = inChain.find((hop) => hop.hop.name === name);
|
|
1574
2167
|
if (found === void 0) throw new Error(`the topology has no hop ${name}`);
|
|
@@ -1633,6 +2226,7 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1633
2226
|
netns,
|
|
1634
2227
|
topology: placed,
|
|
1635
2228
|
middlebox,
|
|
2229
|
+
server: serverLines.seen,
|
|
1636
2230
|
kernel: records.seen,
|
|
1637
2231
|
async sysctl(values) {
|
|
1638
2232
|
const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
|
|
@@ -1830,6 +2424,19 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1830
2424
|
},
|
|
1831
2425
|
freeze: () => paused.pause().then(() => void 0),
|
|
1832
2426
|
thaw: () => paused.unpause().then(() => void 0),
|
|
2427
|
+
async signal(signal, at) {
|
|
2428
|
+
if (target.program === void 0) throw new Error(`${target.image} names no server process to signal`);
|
|
2429
|
+
signaller ??= await withPid(new GenericContainer(image).withNetworkMode(`container:${server.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${server.getId()}`).start();
|
|
2430
|
+
if (!opened.includes(signaller)) opened.push(signaller);
|
|
2431
|
+
const when = at === void 0 ? [] : ["--at", String(Math.round(at))];
|
|
2432
|
+
return stamped(signaller, [
|
|
2433
|
+
"/opt/harness/inject",
|
|
2434
|
+
"signal",
|
|
2435
|
+
target.program,
|
|
2436
|
+
signal,
|
|
2437
|
+
...when
|
|
2438
|
+
]);
|
|
2439
|
+
},
|
|
1833
2440
|
async probe(args) {
|
|
1834
2441
|
const events = lines();
|
|
1835
2442
|
const container = await new GenericContainer(clientImage).withNetworkMode(`container:${client.getId()}`).withCommand([...command, ...args]).withLogConsumer(events.consume).start();
|
|
@@ -1898,7 +2505,9 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1898
2505
|
image: target.image,
|
|
1899
2506
|
id: (await inspect(server)).Image,
|
|
1900
2507
|
settings,
|
|
1901
|
-
sysctls: targetSysctls
|
|
2508
|
+
sysctls: targetSysctls,
|
|
2509
|
+
...target.files === void 0 ? {} : { files: Object.fromEntries(Object.entries(target.files).map(([path, content]) => [path, createHash("sha256").update(content).digest("hex")])) },
|
|
2510
|
+
...serverLines.seen[0] === void 0 ? {} : { first: serverLines.seen[0] }
|
|
1902
2511
|
},
|
|
1903
2512
|
harness: {
|
|
1904
2513
|
image,
|
|
@@ -2044,7 +2653,7 @@ const viewOf = (group, ips, sentinels, members) => {
|
|
|
2044
2653
|
why: "healthy"
|
|
2045
2654
|
};
|
|
2046
2655
|
};
|
|
2047
|
-
const sleep$
|
|
2656
|
+
const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2048
2657
|
/** This time is the longest that a group takes to converge before a run, or after it. */
|
|
2049
2658
|
const CONVERGE_MS = 24e4;
|
|
2050
2659
|
/**
|
|
@@ -2239,7 +2848,7 @@ const openGroupLab = async (group, options = {}) => {
|
|
|
2239
2848
|
}));
|
|
2240
2849
|
calm = last.healthy ? calm + 1 : 0;
|
|
2241
2850
|
if (calm >= 2 && last.primary !== null) return last.primary;
|
|
2242
|
-
await sleep$
|
|
2851
|
+
await sleep$2(1e3);
|
|
2243
2852
|
}
|
|
2244
2853
|
throw new Error(`the group did not converge in ${within} ms: ${last.why}`);
|
|
2245
2854
|
},
|
|
@@ -2317,7 +2926,7 @@ const openGroupLab = async (group, options = {}) => {
|
|
|
2317
2926
|
const started = Date.now();
|
|
2318
2927
|
while (await cli(member, ["PING"]).catch(() => "") !== "PONG") {
|
|
2319
2928
|
if (Date.now() - started > 3e4) throw new Error(`${member} did not start again`);
|
|
2320
|
-
await sleep$
|
|
2929
|
+
await sleep$2(200);
|
|
2321
2930
|
}
|
|
2322
2931
|
},
|
|
2323
2932
|
failover(coordinated) {
|
|
@@ -2435,9 +3044,9 @@ const openGroupLab = async (group, options = {}) => {
|
|
|
2435
3044
|
}
|
|
2436
3045
|
};
|
|
2437
3046
|
/** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
|
|
2438
|
-
const STEADY = 20;
|
|
3047
|
+
const STEADY$1 = 20;
|
|
2439
3048
|
/** This time is how long a run continues after its outcome, so that the record shows what came next. */
|
|
2440
|
-
const TAIL_MS = 2e3;
|
|
3049
|
+
const TAIL_MS$2 = 2e3;
|
|
2441
3050
|
/** This function gives the address of a member, with the port of the group. */
|
|
2442
3051
|
const addressOf = (lab, member) => `${lab.members[member].ip}:${lab.group.port}`;
|
|
2443
3052
|
/** This function gives the latest time that a group cell waits for its outcome, in ms from the fault. */
|
|
@@ -2460,7 +3069,7 @@ const clientArgs = (cell, lab) => {
|
|
|
2460
3069
|
];
|
|
2461
3070
|
};
|
|
2462
3071
|
/** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
|
|
2463
|
-
const waitFor = (client, match, within) => new Promise((resolve) => {
|
|
3072
|
+
const waitFor$1 = (client, match, within) => new Promise((resolve) => {
|
|
2464
3073
|
const started = Date.now();
|
|
2465
3074
|
const look = () => {
|
|
2466
3075
|
const found = client.events.find(match);
|
|
@@ -2476,7 +3085,7 @@ const subscriptionsOf = (timeline) => {
|
|
|
2476
3085
|
return [...latest.values()];
|
|
2477
3086
|
};
|
|
2478
3087
|
/** This function gives the count of lost records in the latest clock line of the prober. */
|
|
2479
|
-
const droppedSoFar$
|
|
3088
|
+
const droppedSoFar$2 = (records) => {
|
|
2480
3089
|
for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
|
|
2481
3090
|
return 0;
|
|
2482
3091
|
};
|
|
@@ -2498,7 +3107,7 @@ const groupOnce = async (lab, cell, environment, index) => {
|
|
|
2498
3107
|
await lab.forget();
|
|
2499
3108
|
const from = lab.kernel.length;
|
|
2500
3109
|
const timelineFrom = lab.timeline.length;
|
|
2501
|
-
const lostBefore = droppedSoFar$
|
|
3110
|
+
const lostBefore = droppedSoFar$2(lab.kernel);
|
|
2502
3111
|
const client = await lab.client(clientArgs(cell, lab));
|
|
2503
3112
|
const lands = cell.prediction.group?.lands ?? "new";
|
|
2504
3113
|
const place = cell.faultAt ?? { role: "primary" };
|
|
@@ -2508,7 +3117,7 @@ const groupOnce = async (lab, cell, environment, index) => {
|
|
|
2508
3117
|
const undo = [];
|
|
2509
3118
|
try {
|
|
2510
3119
|
await client.until(["ready"], 6e4);
|
|
2511
|
-
if (await waitFor(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY, 3e4) === void 0) throw new Error(`the client did not write ${STEADY} times in 30 s`);
|
|
3120
|
+
if (await waitFor$1(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY$1, 3e4) === void 0) throw new Error(`the client did not write ${STEADY$1} times in 30 s`);
|
|
2512
3121
|
let healing = null;
|
|
2513
3122
|
switch (cell.fault) {
|
|
2514
3123
|
case "blackhole":
|
|
@@ -2522,7 +3131,7 @@ const groupOnce = async (lab, cell, environment, index) => {
|
|
|
2522
3131
|
undo.push(() => lab.lift(kind, part, cuts));
|
|
2523
3132
|
if (cell.stall !== void 0) {
|
|
2524
3133
|
const t1 = fault.t1;
|
|
2525
|
-
const sent = await waitFor(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
|
|
3134
|
+
const sent = await waitFor$1(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
|
|
2526
3135
|
if (sent !== void 0) healing = lab.lift(kind, part, cuts, sent.t + valueIn(cell.stall, "s") * 1e9);
|
|
2527
3136
|
} else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
|
|
2528
3137
|
break;
|
|
@@ -2556,11 +3165,11 @@ const groupOnce = async (lab, cell, environment, index) => {
|
|
|
2556
3165
|
const t0 = fault.t0;
|
|
2557
3166
|
const old = addressOf(lab, primary);
|
|
2558
3167
|
const deadline = deadlineOf(cell);
|
|
2559
|
-
if ((lands === "new" ? await waitFor(client, (event) => event.ev === "ack" && event.t > t0 && typeof event["at"] === "string" && event["at"] !== old, deadline) : await (async () => {
|
|
3168
|
+
if ((lands === "new" ? await waitFor$1(client, (event) => event.ev === "ack" && event.t > t0 && typeof event["at"] === "string" && event["at"] !== old, deadline) : await (async () => {
|
|
2560
3169
|
await healed;
|
|
2561
3170
|
const after = heal?.t0 ?? t0;
|
|
2562
|
-
return waitFor(client, (event) => event.ev === "ack" && event.t > after, deadline);
|
|
2563
|
-
})()) !== void 0) await sleep$
|
|
3171
|
+
return waitFor$1(client, (event) => event.ev === "ack" && event.t > after, deadline);
|
|
3172
|
+
})()) !== void 0) await sleep$2(TAIL_MS$2);
|
|
2564
3173
|
await healed;
|
|
2565
3174
|
} finally {
|
|
2566
3175
|
for (const step of undo.reverse()) await step().catch(() => void 0);
|
|
@@ -2593,66 +3202,730 @@ const groupOnce = async (lab, cell, environment, index) => {
|
|
|
2593
3202
|
};
|
|
2594
3203
|
};
|
|
2595
3204
|
//#endregion
|
|
2596
|
-
//#region src/
|
|
2597
|
-
/**
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
3205
|
+
//#region src/replset.ts
|
|
3206
|
+
/** These are the components of the log of mongod that a run keeps: replication, its heartbeats, the elections, the rollbacks and the commands. */
|
|
3207
|
+
const KEPT = /* @__PURE__ */ new Set([
|
|
3208
|
+
"REPL",
|
|
3209
|
+
"REPL_HB",
|
|
3210
|
+
"ELECTION",
|
|
3211
|
+
"ROLLBACK",
|
|
3212
|
+
"COMMAND"
|
|
3213
|
+
]);
|
|
3214
|
+
/** This function reads one log line of a member, or gives null for a line of another component or a line that is not JSON. */
|
|
3215
|
+
const memberLineOf = (member, text) => {
|
|
3216
|
+
if (!text.startsWith("{")) return null;
|
|
3217
|
+
let line;
|
|
3218
|
+
try {
|
|
3219
|
+
line = JSON.parse(text);
|
|
3220
|
+
} catch {
|
|
3221
|
+
return null;
|
|
3222
|
+
}
|
|
3223
|
+
const real = Date.parse(line.t?.$date ?? "");
|
|
3224
|
+
if (!Number.isFinite(real) || !KEPT.has(line.c ?? "")) return null;
|
|
2604
3225
|
return {
|
|
2605
|
-
|
|
2606
|
-
|
|
3226
|
+
ev: "mongod",
|
|
3227
|
+
t: 0,
|
|
3228
|
+
member,
|
|
3229
|
+
real,
|
|
3230
|
+
id: Number(line.id ?? 0),
|
|
3231
|
+
c: line.c ?? "",
|
|
3232
|
+
msg: line.msg ?? "",
|
|
3233
|
+
attr: line.attr ?? {}
|
|
2607
3234
|
};
|
|
2608
3235
|
};
|
|
2609
|
-
/** This function gives the count of lost records in the latest clock line of the prober, since it began. */
|
|
2610
|
-
const droppedSoFar = (records) => {
|
|
2611
|
-
for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
|
|
2612
|
-
return 0;
|
|
2613
|
-
};
|
|
2614
|
-
/** This function gives the argv of the client for a cell, with a deadline far after all that it waits for. */
|
|
2615
|
-
const probeArgs = (cell, lab) => {
|
|
2616
|
-
const { prediction, probe } = cell;
|
|
2617
|
-
const longest = (prediction.detects ? valueIn(prediction.high, "s") : valueIn(prediction.window, "s")) + (cell.idleBefore === void 0 ? 0 : valueIn(cell.idleBefore, "s"));
|
|
2618
|
-
const args = [
|
|
2619
|
-
lab.targetIp,
|
|
2620
|
-
String(lab.targetPort),
|
|
2621
|
-
"--mode",
|
|
2622
|
-
probe.mode
|
|
2623
|
-
];
|
|
2624
|
-
if (probe.request !== void 0) args.push("--request", probe.request);
|
|
2625
|
-
if (probe.count !== void 0) args.push("--count", String(probe.count));
|
|
2626
|
-
if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
|
|
2627
|
-
if (probe.driver !== void 0) {
|
|
2628
|
-
args.push("--keepalive-ms", String(valueIn(probe.driver.keepAlive, "ms")));
|
|
2629
|
-
args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
|
|
2630
|
-
return args;
|
|
2631
|
-
}
|
|
2632
|
-
if (probe.keepalive) args.push("--keepalive");
|
|
2633
|
-
if (probe.userTimeout !== void 0) args.push("--user-timeout-ms", String(valueIn(probe.userTimeout, "ms")));
|
|
2634
|
-
args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
|
|
2635
|
-
return args;
|
|
2636
|
-
};
|
|
2637
3236
|
/**
|
|
2638
|
-
* This function
|
|
2639
|
-
*
|
|
2640
|
-
*
|
|
3237
|
+
* This function gives the offset of the wall clock from the monotonic clock, in ns, from the clock
|
|
3238
|
+
* lines of a client. Each line pairs its stamp with the wall clock in whole ms, which truncates.
|
|
3239
|
+
* Thus each pair is the offset or less, and the largest pair is the nearest. It is null without a clock line.
|
|
2641
3240
|
*/
|
|
2642
|
-
const
|
|
2643
|
-
const
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
});
|
|
2652
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3241
|
+
const clockOffsetOf = (events) => {
|
|
3242
|
+
const offsets = events.filter((event) => event.ev === "clocks").map((event) => Number(event["real"]) * 1e6 - event.t);
|
|
3243
|
+
return offsets.length === 0 ? null : Math.max(...offsets);
|
|
3244
|
+
};
|
|
3245
|
+
/** This function puts the log lines of the members on the monotonic clock, with the offset of a client. */
|
|
3246
|
+
const onMonotonic = (log, offset) => log.map((line) => ({
|
|
3247
|
+
...line,
|
|
3248
|
+
t: offset === null ? 0 : Math.round(line.real * 1e6 - offset)
|
|
3249
|
+
}));
|
|
2653
3250
|
/**
|
|
2654
|
-
* This function
|
|
2655
|
-
*
|
|
3251
|
+
* This function gives the peers that a fault on a member of a replica set cuts it from. `all` is
|
|
3252
|
+
* each other member and the client, `group` is the other members, and `clients` is the client.
|
|
3253
|
+
* `one` is the first other member in the sequence of the set, and `one-way` is the same member for
|
|
3254
|
+
* the segments from the part only.
|
|
3255
|
+
*/
|
|
3256
|
+
const cutOf = (set, members, client, part, cuts) => {
|
|
3257
|
+
const others = set.members.filter((name) => name !== part).map((name) => members[name].ip);
|
|
3258
|
+
if (cuts === "clients") return { peers: [client] };
|
|
3259
|
+
if (cuts === "group") return { peers: others };
|
|
3260
|
+
if (cuts === "one") return { peers: others.slice(0, 1) };
|
|
3261
|
+
if (cuts === "one-way") return {
|
|
3262
|
+
peers: others.slice(0, 1),
|
|
3263
|
+
direction: "out"
|
|
3264
|
+
};
|
|
3265
|
+
return { peers: [...others, client] };
|
|
3266
|
+
};
|
|
3267
|
+
/** This function gives the command of the server of each member. The server is the child of a shell, thus a fin can stop it and the container stays up. */
|
|
3268
|
+
const memberCommand = (set) => `${set.programs.server} --replSet ${set.name} --bind_ip_all --port ${set.port} ${(set.args ?? []).join(" ")}`.trim();
|
|
3269
|
+
/** This function gives the script of the shell that starts the set: each member by its address, and the settings of the set. */
|
|
3270
|
+
const initiateScript = (set, ips) => {
|
|
3271
|
+
const config = {
|
|
3272
|
+
_id: set.name,
|
|
3273
|
+
members: set.members.map((name, at) => ({
|
|
3274
|
+
_id: at,
|
|
3275
|
+
host: `${ips[name]}:${set.port}`
|
|
3276
|
+
})),
|
|
3277
|
+
settings: set.settings
|
|
3278
|
+
};
|
|
3279
|
+
return `print(JSON.stringify(rs.initiate(${JSON.stringify(config)})))`;
|
|
3280
|
+
};
|
|
3281
|
+
/** This script prints the view of a member: itself, if it accepts writes, and the state of each member as it sees them. */
|
|
3282
|
+
const STATUS_SCRIPT = "const s = rs.status(); const h = db.hello(); print(JSON.stringify({ me: h.me, writable: h.isWritablePrimary === true, members: s.members.map((m) => ({ name: m.name, state: m.stateStr, health: m.health, optime: m.optimeDate ? m.optimeDate.getTime() : null })) }))";
|
|
3283
|
+
/**
|
|
3284
|
+
* This function judges the view of a member: the primary of the set, and if the set is healthy.
|
|
3285
|
+
* The set is healthy when the member is the one primary and accepts writes. Each other member must
|
|
3286
|
+
* be a healthy secondary at the optime of the primary.
|
|
3287
|
+
*/
|
|
3288
|
+
const statusViewOf = (set, ips, status) => {
|
|
3289
|
+
const byAddress = new Map(set.members.map((name) => [`${ips[name]}:${set.port}`, name]));
|
|
3290
|
+
const primaries = status.members.filter((member) => member.state === "PRIMARY");
|
|
3291
|
+
if (primaries.length !== 1) return {
|
|
3292
|
+
primary: null,
|
|
3293
|
+
healthy: false,
|
|
3294
|
+
why: `the set has ${primaries.length} primaries`
|
|
3295
|
+
};
|
|
3296
|
+
const primary = byAddress.get(primaries[0].name) ?? null;
|
|
3297
|
+
if (primary === null) return {
|
|
3298
|
+
primary: null,
|
|
3299
|
+
healthy: false,
|
|
3300
|
+
why: `no member has the address ${primaries[0].name}`
|
|
3301
|
+
};
|
|
3302
|
+
if (status.me !== primaries[0].name || !status.writable) return {
|
|
3303
|
+
primary,
|
|
3304
|
+
healthy: false,
|
|
3305
|
+
why: `${primary} does not accept writes yet`
|
|
3306
|
+
};
|
|
3307
|
+
if (status.members.length !== set.members.length) return {
|
|
3308
|
+
primary,
|
|
3309
|
+
healthy: false,
|
|
3310
|
+
why: `the set has ${status.members.length} members`
|
|
3311
|
+
};
|
|
3312
|
+
for (const member of status.members) {
|
|
3313
|
+
if (member.health !== 1) return {
|
|
3314
|
+
primary,
|
|
3315
|
+
healthy: false,
|
|
3316
|
+
why: `${member.name} is not healthy`
|
|
3317
|
+
};
|
|
3318
|
+
if (member.state !== "PRIMARY" && member.state !== "SECONDARY") return {
|
|
3319
|
+
primary,
|
|
3320
|
+
healthy: false,
|
|
3321
|
+
why: `${member.name} is ${member.state}`
|
|
3322
|
+
};
|
|
3323
|
+
if (member.optime !== primaries[0].optime) return {
|
|
3324
|
+
primary,
|
|
3325
|
+
healthy: false,
|
|
3326
|
+
why: `${member.name} is behind the primary`
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
return {
|
|
3330
|
+
primary,
|
|
3331
|
+
healthy: true,
|
|
3332
|
+
why: "healthy"
|
|
3333
|
+
};
|
|
3334
|
+
};
|
|
3335
|
+
const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3336
|
+
/** These are the arguments of replSetStepDown: the time that the old primary cannot stand again, and the wait for a secondary to catch up. */
|
|
3337
|
+
const STEP_DOWN = {
|
|
3338
|
+
seconds: 60,
|
|
3339
|
+
catchUp: 10
|
|
3340
|
+
};
|
|
3341
|
+
/**
|
|
3342
|
+
* This function opens one environment of a replica set cell:
|
|
3343
|
+
* - one new network, with each member, the client and the sender of a step-down on it.
|
|
3344
|
+
* - each member: the stock image, its server as the child of a shell. A log consumer reads its
|
|
3345
|
+
* JSON log. The first member starts the set with the address of each member, and the lab
|
|
3346
|
+
* waits for a primary.
|
|
3347
|
+
* - an injector sidecar for each member, in its network and process namespaces.
|
|
3348
|
+
* - a client that holds its own network namespace, with a privileged sidecar for its sysctls.
|
|
3349
|
+
* - the prober, which watches the namespace of the client.
|
|
3350
|
+
*/
|
|
3351
|
+
const openReplicaSetLab = async (set) => {
|
|
3352
|
+
const image = await harnessImage();
|
|
3353
|
+
const mongo = await mongoClientImage();
|
|
3354
|
+
const { GenericContainer, Network, Wait, getContainerRuntimeClient } = await containers();
|
|
3355
|
+
const runtime = await getContainerRuntimeClient();
|
|
3356
|
+
const opened = [];
|
|
3357
|
+
let network;
|
|
3358
|
+
const close = async () => {
|
|
3359
|
+
for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
|
|
3360
|
+
await network?.stop().catch(() => void 0);
|
|
3361
|
+
};
|
|
3362
|
+
const ipOf = async (container) => {
|
|
3363
|
+
const ip = (await runtime.container.inspect(runtime.container.getById(container.getId()))).NetworkSettings.Networks[network.getName()]?.IPAddress;
|
|
3364
|
+
if (ip === void 0 || ip === "") throw new Error(`${container.getName()} has no address on ${network.getName()}`);
|
|
3365
|
+
return ip;
|
|
3366
|
+
};
|
|
3367
|
+
try {
|
|
3368
|
+
network = await new Network().start();
|
|
3369
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
3370
|
+
const injectors = /* @__PURE__ */ new Map();
|
|
3371
|
+
const members = {};
|
|
3372
|
+
const log = [];
|
|
3373
|
+
for (const name of set.members) {
|
|
3374
|
+
let partial = "";
|
|
3375
|
+
const box = await new GenericContainer(set.image).withNetwork(network).withNetworkAliases(name).withCommand([
|
|
3376
|
+
"sh",
|
|
3377
|
+
"-c",
|
|
3378
|
+
`${memberCommand(set)} & echo $! > /tmp/server.pid; while :; do sleep 3600; done`
|
|
3379
|
+
]).withWaitStrategy(Wait.forLogMessage(set.ready)).withLogConsumer((stream) => stream.on("data", (chunk) => {
|
|
3380
|
+
partial += chunk.toString();
|
|
3381
|
+
const split = partial.split("\n");
|
|
3382
|
+
partial = split.pop() ?? "";
|
|
3383
|
+
for (const text of split) {
|
|
3384
|
+
const line = memberLineOf(name, text.trim());
|
|
3385
|
+
if (line !== null) log.push(line);
|
|
3386
|
+
}
|
|
3387
|
+
})).start();
|
|
3388
|
+
opened.push(box);
|
|
3389
|
+
boxes.set(name, box);
|
|
3390
|
+
const injector = await withPid(new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${box.getId()}`).start();
|
|
3391
|
+
opened.push(injector);
|
|
3392
|
+
injectors.set(name, injector);
|
|
3393
|
+
members[name] = {
|
|
3394
|
+
ip: await ipOf(box),
|
|
3395
|
+
netns: await netnsOf(injector)
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
const ips = Object.fromEntries(Object.entries(members).map(([name, at]) => [name, at.ip]));
|
|
3399
|
+
const holder = await new GenericContainer(image).withNetwork(network).start();
|
|
3400
|
+
opened.push(holder);
|
|
3401
|
+
const admin = await new GenericContainer(image).withNetworkMode(`container:${holder.getId()}`).withPrivilegedMode().start();
|
|
3402
|
+
opened.push(admin);
|
|
3403
|
+
const clientIp = await ipOf(holder);
|
|
3404
|
+
const commander = await new GenericContainer(mongo).withNetwork(network).withCommand([
|
|
3405
|
+
"sh",
|
|
3406
|
+
"-c",
|
|
3407
|
+
"while :; do sleep 3600; done"
|
|
3408
|
+
]).start();
|
|
3409
|
+
opened.push(commander);
|
|
3410
|
+
const clientNetns = await netnsOf(admin);
|
|
3411
|
+
const records = lines();
|
|
3412
|
+
const prober = await new GenericContainer(image).withPrivilegedMode().withNetworkMode("none").withCommand(["/opt/harness/prober", String(clientNetns)]).withLogConsumer(records.consume).start();
|
|
3413
|
+
opened.push(prober);
|
|
3414
|
+
const begun = await records.until(["begin", "fail"], 3e4, "prober");
|
|
3415
|
+
if (begun.ev === "fail") throw new Error(`the prober failed: ${String(begun["why"])}`);
|
|
3416
|
+
const box = (name) => {
|
|
3417
|
+
const found = boxes.get(name);
|
|
3418
|
+
if (found === void 0) throw new Error(`the set has no member ${name}`);
|
|
3419
|
+
return found;
|
|
3420
|
+
};
|
|
3421
|
+
const injectorOf = (name) => {
|
|
3422
|
+
const found = injectors.get(name);
|
|
3423
|
+
if (found === void 0) throw new Error(`the set has no member ${name}`);
|
|
3424
|
+
return found;
|
|
3425
|
+
};
|
|
3426
|
+
/** This function starts a script of the shell on a member, against its own server only. */
|
|
3427
|
+
const shell = (name, script) => output(box(name), [
|
|
3428
|
+
set.programs.shell,
|
|
3429
|
+
"--quiet",
|
|
3430
|
+
"--norc",
|
|
3431
|
+
`mongodb://127.0.0.1:${set.port}/?directConnection=true&serverSelectionTimeoutMS=3000`,
|
|
3432
|
+
"--eval",
|
|
3433
|
+
script
|
|
3434
|
+
]);
|
|
3435
|
+
const lastLine = (text) => text.split("\n").filter((line) => line.trim() !== "").at(-1) ?? "";
|
|
3436
|
+
const stamped = async (where, argv) => {
|
|
3437
|
+
const stamp = JSON.parse(lastLine(await output(where, argv)));
|
|
3438
|
+
return {
|
|
3439
|
+
t0: stamp.t0,
|
|
3440
|
+
t1: stamp.t1
|
|
3441
|
+
};
|
|
3442
|
+
};
|
|
3443
|
+
const after = (at) => at === void 0 ? [] : ["--at", String(Math.round(at))];
|
|
3444
|
+
let known = set.members[0];
|
|
3445
|
+
/** This function gives the view of the set: from the member that was the primary last, then from each other member. */
|
|
3446
|
+
const view = async () => {
|
|
3447
|
+
let last = {
|
|
3448
|
+
primary: null,
|
|
3449
|
+
healthy: false,
|
|
3450
|
+
why: "no member answered"
|
|
3451
|
+
};
|
|
3452
|
+
for (const name of [known, ...set.members.filter((each) => each !== known)]) {
|
|
3453
|
+
const read = await shell(name, STATUS_SCRIPT).then((text) => statusViewOf(set, ips, JSON.parse(lastLine(text)))).catch((error) => ({
|
|
3454
|
+
primary: null,
|
|
3455
|
+
healthy: false,
|
|
3456
|
+
why: `${name}: ${error.message.slice(0, 200)}`
|
|
3457
|
+
}));
|
|
3458
|
+
if (read.primary !== null && read.primary === name) return read;
|
|
3459
|
+
last = read;
|
|
3460
|
+
}
|
|
3461
|
+
return last;
|
|
3462
|
+
};
|
|
3463
|
+
const converge = async (within = CONVERGE_MS) => {
|
|
3464
|
+
const started = Date.now();
|
|
3465
|
+
let calm = 0;
|
|
3466
|
+
let last = {
|
|
3467
|
+
primary: null,
|
|
3468
|
+
healthy: false,
|
|
3469
|
+
why: "no view yet"
|
|
3470
|
+
};
|
|
3471
|
+
while (Date.now() - started < within) {
|
|
3472
|
+
last = await view();
|
|
3473
|
+
calm = last.healthy ? calm + 1 : 0;
|
|
3474
|
+
if (last.primary !== null) known = last.primary;
|
|
3475
|
+
if (calm >= 2 && last.primary !== null) return last.primary;
|
|
3476
|
+
await sleep$1(1e3);
|
|
3477
|
+
}
|
|
3478
|
+
throw new Error(`the set did not converge in ${within} ms: ${last.why}`);
|
|
3479
|
+
};
|
|
3480
|
+
const initiated = await shell(set.members[0], initiateScript(set, ips));
|
|
3481
|
+
if (!lastLine(initiated).includes("\"ok\":1")) throw new Error(`rs.initiate failed: ${lastLine(initiated)}`);
|
|
3482
|
+
await converge();
|
|
3483
|
+
return {
|
|
3484
|
+
set,
|
|
3485
|
+
clientIp,
|
|
3486
|
+
members,
|
|
3487
|
+
netns: { client: clientNetns },
|
|
3488
|
+
kernel: records.seen,
|
|
3489
|
+
log,
|
|
3490
|
+
async sysctl(values) {
|
|
3491
|
+
const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
|
|
3492
|
+
if (settings.length > 0) await output(admin, [
|
|
3493
|
+
"sysctl",
|
|
3494
|
+
"-q",
|
|
3495
|
+
"-w",
|
|
3496
|
+
...settings
|
|
3497
|
+
]);
|
|
3498
|
+
const read = {};
|
|
3499
|
+
for (const key of Object.keys(values)) read[key] = await output(admin, [
|
|
3500
|
+
"sysctl",
|
|
3501
|
+
"-n",
|
|
3502
|
+
key
|
|
3503
|
+
]);
|
|
3504
|
+
return read;
|
|
3505
|
+
},
|
|
3506
|
+
converge,
|
|
3507
|
+
async flush(primary) {
|
|
3508
|
+
const script = `const r = db.getSiblingDB("cells").w.deleteMany({}, { writeConcern: { w: ${set.members.length}, wtimeout: 10000 } }); print(JSON.stringify({ ok: r.acknowledged === true }))`;
|
|
3509
|
+
const reply = lastLine(await shell(primary, script));
|
|
3510
|
+
if (!reply.includes("\"ok\":true")) throw new Error(`the flush failed: ${reply}`);
|
|
3511
|
+
},
|
|
3512
|
+
async unfreeze(primary) {
|
|
3513
|
+
for (const name of set.members) {
|
|
3514
|
+
if (name === primary) continue;
|
|
3515
|
+
const reply = lastLine(await shell(name, "print(JSON.stringify(rs.freeze(0)))"));
|
|
3516
|
+
if (!reply.includes("\"ok\":1")) throw new Error(`${name} did not unfreeze: ${reply}`);
|
|
3517
|
+
}
|
|
3518
|
+
},
|
|
3519
|
+
async holds(primary) {
|
|
3520
|
+
return JSON.parse(lastLine(await shell(primary, "print(JSON.stringify(db.getSiblingDB(\"cells\").w.find({}, { n: 1, _id: 0 }).toArray().map((d) => d.n)))"))).map(Number).filter((n) => Number.isInteger(n) && n > 0).sort((a, b) => a - b);
|
|
3521
|
+
},
|
|
3522
|
+
partOf(role, primary) {
|
|
3523
|
+
if (role === "primary") return primary;
|
|
3524
|
+
if (role === "sentinel") throw new Error("a replica set has no Sentinel");
|
|
3525
|
+
const secondary = set.members.find((name) => name !== primary);
|
|
3526
|
+
if (secondary === void 0) throw new Error("the set has no secondary");
|
|
3527
|
+
return secondary;
|
|
3528
|
+
},
|
|
3529
|
+
async client(args) {
|
|
3530
|
+
const events = lines();
|
|
3531
|
+
const container = await new GenericContainer(mongo).withNetworkMode(`container:${holder.getId()}`).withCommand([
|
|
3532
|
+
"node",
|
|
3533
|
+
"/opt/harness/mongo/client-mongo.mjs",
|
|
3534
|
+
...args
|
|
3535
|
+
]).withLogConsumer(events.consume).start();
|
|
3536
|
+
return {
|
|
3537
|
+
events: events.seen,
|
|
3538
|
+
until: (kinds, within) => events.until(kinds, within, "client"),
|
|
3539
|
+
stop: () => container.stop({ timeout: 2 }).then(() => void 0).catch(() => void 0)
|
|
3540
|
+
};
|
|
3541
|
+
},
|
|
3542
|
+
fault(kind, part, cuts, at) {
|
|
3543
|
+
const cut = cutOf(set, members, clientIp, part, cuts);
|
|
3544
|
+
const direction = cut.direction === void 0 ? [] : ["--direction", cut.direction];
|
|
3545
|
+
return stamped(injectorOf(part), [
|
|
3546
|
+
"/opt/harness/inject",
|
|
3547
|
+
kind,
|
|
3548
|
+
cut.peers.join(","),
|
|
3549
|
+
...direction,
|
|
3550
|
+
...after(at)
|
|
3551
|
+
]);
|
|
3552
|
+
},
|
|
3553
|
+
lift(kind, part, cuts, at) {
|
|
3554
|
+
const cut = cutOf(set, members, clientIp, part, cuts);
|
|
3555
|
+
const direction = cut.direction === void 0 ? [] : ["--direction", cut.direction];
|
|
3556
|
+
return stamped(injectorOf(part), [
|
|
3557
|
+
"/opt/harness/inject",
|
|
3558
|
+
kind === "rst" ? "clear-rst" : "clear",
|
|
3559
|
+
cut.peers.join(","),
|
|
3560
|
+
...direction,
|
|
3561
|
+
...after(at)
|
|
3562
|
+
]);
|
|
3563
|
+
},
|
|
3564
|
+
signal(part, signal, at) {
|
|
3565
|
+
return stamped(injectorOf(part), [
|
|
3566
|
+
"/opt/harness/inject",
|
|
3567
|
+
"signal",
|
|
3568
|
+
set.programs.server,
|
|
3569
|
+
signal,
|
|
3570
|
+
...after(at)
|
|
3571
|
+
]);
|
|
3572
|
+
},
|
|
3573
|
+
async restart(member) {
|
|
3574
|
+
await output(box(member), [
|
|
3575
|
+
"sh",
|
|
3576
|
+
"-c",
|
|
3577
|
+
`${memberCommand(set)} > /proc/1/fd/1 2>&1 & echo $! > /tmp/server.pid`
|
|
3578
|
+
]);
|
|
3579
|
+
const started = Date.now();
|
|
3580
|
+
while (!(await shell(member, "print(db.hello().ok)").catch(() => "")).includes("1")) {
|
|
3581
|
+
if (Date.now() - started > 6e4) throw new Error(`${member} did not start again`);
|
|
3582
|
+
await sleep$1(500);
|
|
3583
|
+
}
|
|
3584
|
+
},
|
|
3585
|
+
stepDown(primary) {
|
|
3586
|
+
return stamped(commander, [
|
|
3587
|
+
"node",
|
|
3588
|
+
"/opt/harness/mongo/stepdown-mongo.mjs",
|
|
3589
|
+
`${members[primary].ip}:${set.port}`,
|
|
3590
|
+
String(STEP_DOWN.seconds),
|
|
3591
|
+
String(STEP_DOWN.catchUp)
|
|
3592
|
+
]);
|
|
3593
|
+
},
|
|
3594
|
+
async forget() {
|
|
3595
|
+
await output(admin, [
|
|
3596
|
+
"ip",
|
|
3597
|
+
"tcp_metrics",
|
|
3598
|
+
"flush",
|
|
3599
|
+
"all"
|
|
3600
|
+
]);
|
|
3601
|
+
},
|
|
3602
|
+
async settle() {
|
|
3603
|
+
const from = records.seen.length;
|
|
3604
|
+
const clock = await new Promise((resolve) => {
|
|
3605
|
+
const started = Date.now();
|
|
3606
|
+
const look = () => {
|
|
3607
|
+
const line = records.seen.slice(from).find((record) => record.ev === "clock");
|
|
3608
|
+
if (line !== void 0) resolve(line);
|
|
3609
|
+
else if (Date.now() - started > 5e3) resolve(null);
|
|
3610
|
+
else setTimeout(look, 50);
|
|
3611
|
+
};
|
|
3612
|
+
look();
|
|
3613
|
+
});
|
|
3614
|
+
return clock === null ? null : Number(clock["lost"] ?? 0);
|
|
3615
|
+
},
|
|
3616
|
+
async manifest(sysctls) {
|
|
3617
|
+
const config = await output(admin, ["zcat", "/proc/config.gz"]).catch(() => "");
|
|
3618
|
+
const inspect = async (container) => runtime.container.inspect(runtime.container.getById(container.getId()));
|
|
3619
|
+
const script = "print(JSON.stringify({ version: db.version(), settings: rs.conf().settings }))";
|
|
3620
|
+
const list = [];
|
|
3621
|
+
for (const name of set.members) {
|
|
3622
|
+
const read = JSON.parse(lastLine(await shell(name, script)));
|
|
3623
|
+
const settings = { version: read.version };
|
|
3624
|
+
for (const key of Object.keys(set.settings)) settings[key] = String(read.settings[key] ?? "");
|
|
3625
|
+
list.push({
|
|
3626
|
+
name,
|
|
3627
|
+
ip: members[name].ip,
|
|
3628
|
+
id: (await inspect(box(name))).Image,
|
|
3629
|
+
settings
|
|
3630
|
+
});
|
|
3631
|
+
}
|
|
3632
|
+
return {
|
|
3633
|
+
kernel: await output(holder, ["uname", "-r"]),
|
|
3634
|
+
architecture: await output(holder, ["uname", "-m"]),
|
|
3635
|
+
hz: /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1] ?? null,
|
|
3636
|
+
boot_id: await output(holder, ["cat", "/proc/sys/kernel/random/boot_id"]),
|
|
3637
|
+
docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
|
|
3638
|
+
target: {
|
|
3639
|
+
image: set.image,
|
|
3640
|
+
id: list[0].id,
|
|
3641
|
+
settings: list[0].settings,
|
|
3642
|
+
sysctls: {}
|
|
3643
|
+
},
|
|
3644
|
+
harness: {
|
|
3645
|
+
image,
|
|
3646
|
+
id: (await inspect(holder)).Image,
|
|
3647
|
+
packages: (await output(holder, [
|
|
3648
|
+
"apk",
|
|
3649
|
+
"info",
|
|
3650
|
+
"-v"
|
|
3651
|
+
])).split("\n").sort()
|
|
3652
|
+
},
|
|
3653
|
+
client: {
|
|
3654
|
+
kind: "mongodb",
|
|
3655
|
+
image: mongo
|
|
3656
|
+
},
|
|
3657
|
+
netns: {
|
|
3658
|
+
client: clientNetns,
|
|
3659
|
+
target: members[set.members[0]].netns
|
|
3660
|
+
},
|
|
3661
|
+
sysctls,
|
|
3662
|
+
group: {
|
|
3663
|
+
members: list,
|
|
3664
|
+
sentinels: [],
|
|
3665
|
+
observer: { image: mongo }
|
|
3666
|
+
}
|
|
3667
|
+
};
|
|
3668
|
+
},
|
|
3669
|
+
close
|
|
3670
|
+
};
|
|
3671
|
+
} catch (error) {
|
|
3672
|
+
await close();
|
|
3673
|
+
throw error;
|
|
3674
|
+
}
|
|
3675
|
+
};
|
|
3676
|
+
/** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
|
|
3677
|
+
const STEADY = 20;
|
|
3678
|
+
/** This time is how long a run continues after its outcome, so that the record shows what came next. */
|
|
3679
|
+
const TAIL_MS$1 = 2e3;
|
|
3680
|
+
/** This function gives the arguments of the client of a replica set cell: each member, and the stream of writes. */
|
|
3681
|
+
const replicaSetArgs = (cell, members) => {
|
|
3682
|
+
const set = cell.replicaSet;
|
|
3683
|
+
const hosts = set.members.map((name) => `${members[name].ip}:${set.port}`);
|
|
3684
|
+
return [
|
|
3685
|
+
members[set.members[0]].ip,
|
|
3686
|
+
String(set.port),
|
|
3687
|
+
"--mode",
|
|
3688
|
+
"stream",
|
|
3689
|
+
"--hosts",
|
|
3690
|
+
hosts.join(","),
|
|
3691
|
+
"--set",
|
|
3692
|
+
set.name,
|
|
3693
|
+
"--interval-ms",
|
|
3694
|
+
String(cell.probe.interval === void 0 ? 50 : valueIn(cell.probe.interval, "ms")),
|
|
3695
|
+
"--max-s",
|
|
3696
|
+
String(Math.ceil(deadlineOf(cell) / 1e3 + (cell.hold === void 0 ? 0 : valueIn(cell.hold, "s")) + 120)),
|
|
3697
|
+
"--w",
|
|
3698
|
+
String(cell.writeConcern ?? "majority"),
|
|
3699
|
+
...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)]
|
|
3700
|
+
];
|
|
3701
|
+
};
|
|
3702
|
+
/** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
|
|
3703
|
+
const waitFor = (client, match, within) => new Promise((resolve) => {
|
|
3704
|
+
const started = Date.now();
|
|
3705
|
+
const look = () => {
|
|
3706
|
+
const found = client.events.find(match);
|
|
3707
|
+
if (found !== void 0 || Date.now() - started > within) resolve(found);
|
|
3708
|
+
else setTimeout(look, 20);
|
|
3709
|
+
};
|
|
3710
|
+
look();
|
|
3711
|
+
});
|
|
3712
|
+
/** This function gives the count of lost records in the latest clock line of the prober. */
|
|
3713
|
+
const droppedSoFar$1 = (records) => {
|
|
3714
|
+
for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
|
|
3715
|
+
return 0;
|
|
3716
|
+
};
|
|
3717
|
+
/**
|
|
3718
|
+
* This function does one run of a replica set cell:
|
|
3719
|
+
* - The set converges, and its writes go. The client finds the primary with its monitors, and its
|
|
3720
|
+
* stream of writes gets steady.
|
|
3721
|
+
* - The fault occurs on its member, as the primary of this run makes the roles. A fault with a
|
|
3722
|
+
* hold lifts at its instant in the VM. A stall lifts at its length after the first write into
|
|
3723
|
+
* it. A step-down is replSetStepDown to the primary.
|
|
3724
|
+
* - The run waits for its outcome: the first accepted write of a write sent after the commit of
|
|
3725
|
+
* the fault. It is on a new primary, or on any primary. Then the run waits for the end of the
|
|
3726
|
+
* hold, thus the record shows what the set did in the whole hold.
|
|
3727
|
+
* - Each fault goes, the client stops, and the set converges again. The writes that it then holds
|
|
3728
|
+
* tell which writes it kept. The log lines of the members go onto the monotonic clock.
|
|
3729
|
+
*/
|
|
3730
|
+
const replicaSetOnce = async (lab, cell, environment, index) => {
|
|
3731
|
+
const set = cell.replicaSet;
|
|
3732
|
+
const primary = await lab.converge();
|
|
3733
|
+
await lab.flush(primary);
|
|
3734
|
+
await lab.unfreeze(primary);
|
|
3735
|
+
await lab.forget();
|
|
3736
|
+
const from = lab.kernel.length;
|
|
3737
|
+
const logFrom = lab.log.length;
|
|
3738
|
+
const lostBefore = droppedSoFar$1(lab.kernel);
|
|
3739
|
+
const client = await lab.client(replicaSetArgs(cell, lab.members));
|
|
3740
|
+
const lands = cell.prediction.group?.lands ?? "new";
|
|
3741
|
+
const place = cell.faultAt ?? { role: "primary" };
|
|
3742
|
+
let fault = null;
|
|
3743
|
+
let heal = null;
|
|
3744
|
+
let at = null;
|
|
3745
|
+
const undo = [];
|
|
3746
|
+
try {
|
|
3747
|
+
await client.until(["ready"], 6e4);
|
|
3748
|
+
if (await waitFor(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY, 3e4) === void 0) throw new Error(`the client did not write ${STEADY} times in 30 s`);
|
|
3749
|
+
let healing = null;
|
|
3750
|
+
switch (cell.fault) {
|
|
3751
|
+
case "blackhole":
|
|
3752
|
+
case "partition":
|
|
3753
|
+
case "rst": {
|
|
3754
|
+
const kind = cell.fault === "rst" ? "rst" : "blackhole";
|
|
3755
|
+
const cuts = place.cuts ?? (cell.fault === "partition" ? "group" : "all");
|
|
3756
|
+
const part = lab.partOf(place.role, primary);
|
|
3757
|
+
at = part;
|
|
3758
|
+
fault = await lab.fault(kind, part, cuts);
|
|
3759
|
+
undo.push(() => lab.lift(kind, part, cuts));
|
|
3760
|
+
if (cell.stall !== void 0) {
|
|
3761
|
+
const t1 = fault.t1;
|
|
3762
|
+
const sent = await waitFor(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
|
|
3763
|
+
healing = lab.lift(kind, part, cuts, (sent?.t ?? t1) + valueIn(cell.stall, "s") * 1e9);
|
|
3764
|
+
} else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
|
|
3765
|
+
break;
|
|
3766
|
+
}
|
|
3767
|
+
case "freeze-process": {
|
|
3768
|
+
const part = lab.partOf(place.role, primary);
|
|
3769
|
+
at = part;
|
|
3770
|
+
fault = await lab.signal(part, "STOP");
|
|
3771
|
+
undo.push(() => lab.signal(part, "CONT"));
|
|
3772
|
+
if (cell.hold !== void 0) healing = lab.signal(part, "CONT", fault.t0 + valueIn(cell.hold, "s") * 1e9);
|
|
3773
|
+
break;
|
|
3774
|
+
}
|
|
3775
|
+
case "fin": {
|
|
3776
|
+
const part = lab.partOf(place.role, primary);
|
|
3777
|
+
at = part;
|
|
3778
|
+
fault = await lab.signal(part, "KILL");
|
|
3779
|
+
undo.push(() => lab.restart(part));
|
|
3780
|
+
break;
|
|
3781
|
+
}
|
|
3782
|
+
case "step-down":
|
|
3783
|
+
at = primary;
|
|
3784
|
+
fault = await lab.stepDown(primary);
|
|
3785
|
+
break;
|
|
3786
|
+
default: throw new Error(`a replica set cell has no fault ${cell.fault}`);
|
|
3787
|
+
}
|
|
3788
|
+
const healed = healing === null ? Promise.resolve(null) : healing.then((stamp) => {
|
|
3789
|
+
heal = stamp;
|
|
3790
|
+
undo.length = 0;
|
|
3791
|
+
return stamp;
|
|
3792
|
+
});
|
|
3793
|
+
healed.catch(() => void 0);
|
|
3794
|
+
const t1 = fault.t1;
|
|
3795
|
+
const old = `${lab.members[primary].ip}:${set.port}`;
|
|
3796
|
+
const sentAt = (n) => client.events.find((event) => event.ev === "sent" && Number(event["n"]) === n)?.t ?? -Infinity;
|
|
3797
|
+
if (await waitFor(client, (event) => event.ev === "ack" && sentAt(Number(event["n"])) > t1 && (lands === "same" || typeof event["at"] === "string" && event["at"] !== old), deadlineOf(cell)) !== void 0) await sleep$1(TAIL_MS$1);
|
|
3798
|
+
if (healing !== null) {
|
|
3799
|
+
await healed.catch(() => void 0);
|
|
3800
|
+
await sleep$1(TAIL_MS$1);
|
|
3801
|
+
}
|
|
3802
|
+
} finally {
|
|
3803
|
+
for (const step of undo.reverse()) await step().catch(() => void 0);
|
|
3804
|
+
await client.stop();
|
|
3805
|
+
}
|
|
3806
|
+
const settled = await lab.settle();
|
|
3807
|
+
const lost = settled === null ? null : settled - lostBefore;
|
|
3808
|
+
const after = await lab.converge().catch(() => null);
|
|
3809
|
+
const holds = after === null ? null : await lab.holds(after);
|
|
3810
|
+
return {
|
|
3811
|
+
environment,
|
|
3812
|
+
index,
|
|
3813
|
+
events: [...client.events],
|
|
3814
|
+
fault,
|
|
3815
|
+
...heal === null ? {} : { heal },
|
|
3816
|
+
kernel: lab.kernel.slice(from),
|
|
3817
|
+
netns: lab.netns.client,
|
|
3818
|
+
targetNetns: lab.members[primary].netns,
|
|
3819
|
+
lost,
|
|
3820
|
+
group: {
|
|
3821
|
+
members: lab.members,
|
|
3822
|
+
sentinels: {},
|
|
3823
|
+
primary,
|
|
3824
|
+
at,
|
|
3825
|
+
timeline: onMonotonic(lab.log.slice(logFrom), clockOffsetOf(client.events)),
|
|
3826
|
+
after,
|
|
3827
|
+
holds
|
|
3828
|
+
}
|
|
3829
|
+
};
|
|
3830
|
+
};
|
|
3831
|
+
//#endregion
|
|
3832
|
+
//#region src/run.ts
|
|
3833
|
+
/**
|
|
3834
|
+
* This function measures and judges the raw records of a cell, with the code of the harness at
|
|
3835
|
+
* this time. `runCell` uses it during a cell. A holder of the raw records of a cell can use it
|
|
3836
|
+
* again later. Thus the records get a new analysis after a change to observe.ts or to the verdict.
|
|
3837
|
+
*/
|
|
3838
|
+
const judge = (cell, runs) => {
|
|
3839
|
+
const measured = runs.filter((run) => run.warmup !== true).map((run) => measure(cell, run));
|
|
3840
|
+
return {
|
|
3841
|
+
measured,
|
|
3842
|
+
verdict: verdictOf(cell.prediction, measured)
|
|
3843
|
+
};
|
|
3844
|
+
};
|
|
3845
|
+
/** This function gives the count of lost records in the latest clock line of the prober, since it began. */
|
|
3846
|
+
const droppedSoFar = (records) => {
|
|
3847
|
+
for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
|
|
3848
|
+
return 0;
|
|
3849
|
+
};
|
|
3850
|
+
/** This function gives the argv of the client for a cell, with a deadline far after all that it waits for. */
|
|
3851
|
+
const probeArgs = (cell, lab) => {
|
|
3852
|
+
const { prediction, probe } = cell;
|
|
3853
|
+
const longest = (prediction.detects === true && prediction.driver !== void 0 ? longestOf(cell) : prediction.detects ? valueIn(prediction.high, "s") : valueIn(prediction.window, "s")) + (cell.idleBefore === void 0 ? 0 : valueIn(cell.idleBefore, "s"));
|
|
3854
|
+
const args = [
|
|
3855
|
+
lab.targetIp,
|
|
3856
|
+
String(lab.targetPort),
|
|
3857
|
+
"--mode",
|
|
3858
|
+
probe.mode
|
|
3859
|
+
];
|
|
3860
|
+
if (probe.http !== void 0) {
|
|
3861
|
+
const { http } = probe;
|
|
3862
|
+
const gap = http.gap === void 0 ? 0 : valueIn(http.gap, "s");
|
|
3863
|
+
args.push("--method", http.method);
|
|
3864
|
+
if (http.gap !== void 0) args.push("--gap-ms", String(valueIn(http.gap, "ms")));
|
|
3865
|
+
if (http.hold !== void 0) args.push("--hold-ms", String(valueIn(http.hold, "ms")));
|
|
3866
|
+
if (http.bytes !== void 0) args.push("--bytes", String(http.bytes));
|
|
3867
|
+
for (const [name, value] of Object.entries(http.options ?? {})) args.push(`--${name}`, String(value));
|
|
3868
|
+
args.push("--linger-ms", String(LINGER_MS));
|
|
3869
|
+
args.push("--max-s", String(Math.ceil(longest * 2 + gap + 10)));
|
|
3870
|
+
return args;
|
|
3871
|
+
}
|
|
3872
|
+
if (probe.request !== void 0) args.push("--request", probe.request);
|
|
3873
|
+
if (probe.count !== void 0) args.push("--count", String(probe.count));
|
|
3874
|
+
if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
|
|
3875
|
+
if (probe.driver?.name === "mongodb") {
|
|
3876
|
+
args.push("--set", probe.driver.set);
|
|
3877
|
+
if (probe.driver.op !== void 0) args.push("--op", probe.driver.op);
|
|
3878
|
+
if (cell.writeConcern !== void 0) args.push("--w", String(cell.writeConcern));
|
|
3879
|
+
if (cell.before === true) args.push("--before");
|
|
3880
|
+
if (cell.driverOptions !== void 0) args.push("--options", JSON.stringify(cell.driverOptions));
|
|
3881
|
+
args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
|
|
3882
|
+
return args;
|
|
3883
|
+
}
|
|
3884
|
+
if (probe.driver !== void 0) {
|
|
3885
|
+
args.push("--keepalive-ms", String(valueIn(probe.driver.keepAlive, "ms")));
|
|
3886
|
+
args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
|
|
3887
|
+
return args;
|
|
3888
|
+
}
|
|
3889
|
+
if (probe.keepalive) args.push("--keepalive");
|
|
3890
|
+
if (probe.userTimeout !== void 0) args.push("--user-timeout-ms", String(valueIn(probe.userTimeout, "ms")));
|
|
3891
|
+
args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
|
|
3892
|
+
return args;
|
|
3893
|
+
};
|
|
3894
|
+
/**
|
|
3895
|
+
* This function waits for an error on the socket of the observer, or for its end of file
|
|
3896
|
+
* (CLOSE_WAIT, state 8). The prober records both. A cell whose observer is the server, or a proxy
|
|
3897
|
+
* of a topology, waits for that. The promise resolves after `within` ms at the latest.
|
|
3898
|
+
*/
|
|
3899
|
+
const observerError = (cell, partial, within) => new Promise((resolve) => {
|
|
3900
|
+
const started = Date.now();
|
|
3901
|
+
const look = () => {
|
|
3902
|
+
const run = partial();
|
|
3903
|
+
const at = socketOf(cell, run);
|
|
3904
|
+
if (at !== void 0 && run.kernel.some((record) => record.netns === at.netns && record.cookie === at.cookie && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && record.new === 8)) || Date.now() - started > within) resolve();
|
|
3905
|
+
else setTimeout(look, 50);
|
|
3906
|
+
};
|
|
3907
|
+
look();
|
|
3908
|
+
});
|
|
3909
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3910
|
+
/**
|
|
3911
|
+
* This time is the pause between the client's first reply and the fault. It is more than Linux's
|
|
3912
|
+
* TCP_DELACK_MAX of 200 ms. Thus the delayed ACK of the reply is out before the fault.
|
|
3913
|
+
*/
|
|
3914
|
+
const QUIET_MS = 250;
|
|
3915
|
+
/**
|
|
3916
|
+
* This time is how long an HTTP client waits after its second request, before it exits. The exit
|
|
3917
|
+
* closes its sockets, thus that close comes after the window of the cell.
|
|
3918
|
+
*/
|
|
3919
|
+
const LINGER_MS = 1e3;
|
|
3920
|
+
/** These are the lines that end the workload of an HTTP client: the end of its second request, its deadline, or its failure. */
|
|
3921
|
+
const HTTP_DONE = [
|
|
3922
|
+
"done",
|
|
3923
|
+
"deadline",
|
|
3924
|
+
"fail"
|
|
3925
|
+
];
|
|
3926
|
+
/**
|
|
3927
|
+
* This function waits for the request that an in-flight client sends on the signal: its second
|
|
3928
|
+
* send, after the PING that made it ready. Its stamp is when the request first met the fault.
|
|
2656
3929
|
*/
|
|
2657
3930
|
const requestSent = (probe, within) => new Promise((resolve, reject) => {
|
|
2658
3931
|
const started = Date.now();
|
|
@@ -2664,6 +3937,69 @@ const requestSent = (probe, within) => new Promise((resolve, reject) => {
|
|
|
2664
3937
|
};
|
|
2665
3938
|
look();
|
|
2666
3939
|
});
|
|
3940
|
+
/** This time is how long a run of a driver cell continues after its outcome, so that the record shows what came next. */
|
|
3941
|
+
const TAIL_MS = 1e3;
|
|
3942
|
+
/** This function waits for the outcome of a run of a driver cell, or for `within` ms. The outcome is the end of its band and of each later band. */
|
|
3943
|
+
const driverOutcome = async (cell, current, within) => {
|
|
3944
|
+
const started = Date.now();
|
|
3945
|
+
while (Date.now() - started < within) {
|
|
3946
|
+
if (settled(cell, current())) return;
|
|
3947
|
+
await sleep(250);
|
|
3948
|
+
}
|
|
3949
|
+
};
|
|
3950
|
+
/** This function gives the longest wait of a run of a driver cell for its outcome, in ms. */
|
|
3951
|
+
const driverWithin = (cell) => (longestOf(cell) * 1.2 + 10) * 1e3;
|
|
3952
|
+
/**
|
|
3953
|
+
* This function does one run of a cell whose fault holds before the client starts (`Cell.before`).
|
|
3954
|
+
* The fault is a network fault at its place. The client then starts into it, and the run waits for
|
|
3955
|
+
* each band of the driver. Then the fault lifts.
|
|
3956
|
+
*/
|
|
3957
|
+
const beforeOnce = async (lab, cell, environment, index) => {
|
|
3958
|
+
if (cell.fault !== "rst" && cell.fault !== "blackhole" && cell.fault !== "unreachable") throw new Error(`${cell.name}: a fault before the client starts is a network fault, not ${cell.fault}`);
|
|
3959
|
+
await lab.forget();
|
|
3960
|
+
const from = lab.kernel.length;
|
|
3961
|
+
const middleboxFrom = lab.middlebox.length;
|
|
3962
|
+
const lostBefore = droppedSoFar(lab.kernel);
|
|
3963
|
+
const faultAt = cell.faultAt ?? "target";
|
|
3964
|
+
const kind = cell.fault === "unreachable" ? "reject" : cell.fault;
|
|
3965
|
+
const options = cell.fault === "unreachable" ? { code: cell.icmp ?? "host" } : {};
|
|
3966
|
+
const fault = await lab.fault(kind, faultAt, options);
|
|
3967
|
+
let probe;
|
|
3968
|
+
try {
|
|
3969
|
+
probe = await lab.probe(probeArgs(cell, lab));
|
|
3970
|
+
const started = probe;
|
|
3971
|
+
const current = () => ({
|
|
3972
|
+
environment,
|
|
3973
|
+
index,
|
|
3974
|
+
events: started.events,
|
|
3975
|
+
fault,
|
|
3976
|
+
kernel: lab.kernel.slice(from),
|
|
3977
|
+
netns: lab.netns.client,
|
|
3978
|
+
targetNetns: lab.netns.target,
|
|
3979
|
+
lost: 0
|
|
3980
|
+
});
|
|
3981
|
+
await driverOutcome(cell, current, driverWithin(cell));
|
|
3982
|
+
await sleep(TAIL_MS);
|
|
3983
|
+
} finally {
|
|
3984
|
+
await lab.lift(kind, faultAt, options);
|
|
3985
|
+
await probe?.stop();
|
|
3986
|
+
}
|
|
3987
|
+
const settled = await lab.settle();
|
|
3988
|
+
return {
|
|
3989
|
+
environment,
|
|
3990
|
+
index,
|
|
3991
|
+
events: [...probe?.events ?? []],
|
|
3992
|
+
fault,
|
|
3993
|
+
kernel: lab.kernel.slice(from),
|
|
3994
|
+
netns: lab.netns.client,
|
|
3995
|
+
targetNetns: lab.netns.target,
|
|
3996
|
+
lost: settled === null ? null : settled - lostBefore,
|
|
3997
|
+
...lab.topology === null ? {} : {
|
|
3998
|
+
topology: lab.topology,
|
|
3999
|
+
middlebox: lab.middlebox.slice(middleboxFrom)
|
|
4000
|
+
}
|
|
4001
|
+
};
|
|
4002
|
+
};
|
|
2667
4003
|
/** This function gives the netem specification of a cell: its latency, then the netem of its fault when it has one. */
|
|
2668
4004
|
const netemSpec = (cell, fault) => {
|
|
2669
4005
|
const parts = [...cell.latency === void 0 ? [] : [`delay ${valueIn(cell.latency, "ms")}ms`], ...fault === void 0 ? [] : [fault]];
|
|
@@ -2683,9 +4019,11 @@ const netemSpec = (cell, fault) => {
|
|
|
2683
4019
|
* A dry run does all of this but the fault: the warm-up of a cell whose fault ends its environment.
|
|
2684
4020
|
*/
|
|
2685
4021
|
const once = async (lab, cell, environment, index, dry = false) => {
|
|
4022
|
+
if (cell.before === true) return beforeOnce(lab, cell, environment, index);
|
|
2686
4023
|
await lab.forget();
|
|
2687
4024
|
const from = lab.kernel.length;
|
|
2688
4025
|
const middleboxFrom = lab.middlebox.length;
|
|
4026
|
+
const serverFrom = lab.server.length;
|
|
2689
4027
|
const lostBefore = droppedSoFar(lab.kernel);
|
|
2690
4028
|
const probe = await lab.probe(probeArgs(cell, lab));
|
|
2691
4029
|
const server = cell.observer === "server";
|
|
@@ -2715,6 +4053,8 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2715
4053
|
});
|
|
2716
4054
|
let fault = null;
|
|
2717
4055
|
let heal = null;
|
|
4056
|
+
const signalled = !server && cell.target.program !== void 0;
|
|
4057
|
+
let thawing = null;
|
|
2718
4058
|
try {
|
|
2719
4059
|
await probe.until(["ready"], 15e3);
|
|
2720
4060
|
if (dry) {
|
|
@@ -2741,6 +4081,17 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2741
4081
|
const apply = async () => {
|
|
2742
4082
|
switch (cell.fault) {
|
|
2743
4083
|
case "freeze-process":
|
|
4084
|
+
if (signalled) {
|
|
4085
|
+
const stamp = await lab.signal("STOP");
|
|
4086
|
+
if (cell.hold !== void 0) {
|
|
4087
|
+
thawing = lab.signal("CONT", stamp.t0 + valueIn(cell.hold, "s") * 1e9).then((thawed) => {
|
|
4088
|
+
heal = thawed;
|
|
4089
|
+
return thawed;
|
|
4090
|
+
});
|
|
4091
|
+
thawing.catch(() => void 0);
|
|
4092
|
+
}
|
|
4093
|
+
return stamp;
|
|
4094
|
+
}
|
|
2744
4095
|
await peer.freeze();
|
|
2745
4096
|
return null;
|
|
2746
4097
|
case "fin":
|
|
@@ -2748,11 +4099,12 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2748
4099
|
return null;
|
|
2749
4100
|
case "unreachable": return lab.fault("reject", faultAt, { code: cell.icmp ?? "host" });
|
|
2750
4101
|
case "simplex": return lab.fault("blackhole", faultAt, options);
|
|
2751
|
-
case "mtu-blackhole": return lab.fault("mtu", "client", { above: cell.mtu ?? 1200 });
|
|
4102
|
+
case "mtu-blackhole": return lab.fault("mtu", cell.faultAt ?? "client", { above: cell.mtu ?? 1200 });
|
|
2752
4103
|
case "netem": return netemOn(cell.netem);
|
|
2753
4104
|
case "rst": return lab.fault("rst", faultAt);
|
|
2754
4105
|
case "middlebox-forget":
|
|
2755
4106
|
case "proxy-idle":
|
|
4107
|
+
case "app-stall":
|
|
2756
4108
|
case "none": return null;
|
|
2757
4109
|
case "toxic":
|
|
2758
4110
|
if (cell.toxic === void 0) throw new Error(`${cell.name} names no toxic`);
|
|
@@ -2765,15 +4117,22 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2765
4117
|
};
|
|
2766
4118
|
const lift = async () => {
|
|
2767
4119
|
switch (cell.fault) {
|
|
2768
|
-
case "freeze-process":
|
|
4120
|
+
case "freeze-process":
|
|
4121
|
+
if (signalled) {
|
|
4122
|
+
if (thawing !== null) await thawing.catch(() => lab.signal("CONT"));
|
|
4123
|
+
else await lab.signal("CONT");
|
|
4124
|
+
return;
|
|
4125
|
+
}
|
|
4126
|
+
return peer.thaw();
|
|
2769
4127
|
case "fin":
|
|
2770
4128
|
case "middlebox-forget":
|
|
2771
4129
|
case "proxy-idle":
|
|
2772
4130
|
case "server-close":
|
|
4131
|
+
case "app-stall":
|
|
2773
4132
|
case "none": return;
|
|
2774
4133
|
case "unreachable": return lab.lift("reject", faultAt, { code: cell.icmp ?? "host" });
|
|
2775
4134
|
case "simplex": return lab.lift("blackhole", faultAt, options);
|
|
2776
|
-
case "mtu-blackhole": return lab.lift("mtu", "client", { above: cell.mtu ?? 1200 });
|
|
4135
|
+
case "mtu-blackhole": return lab.lift("mtu", cell.faultAt ?? "client", { above: cell.mtu ?? 1200 });
|
|
2777
4136
|
case "netem":
|
|
2778
4137
|
await netemOn(void 0);
|
|
2779
4138
|
return;
|
|
@@ -2787,9 +4146,21 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2787
4146
|
fault = await apply();
|
|
2788
4147
|
try {
|
|
2789
4148
|
if (cell.idleBefore !== void 0 && cell.probe.mode !== "idle") await sleep(valueIn(cell.idleBefore, "ms"));
|
|
2790
|
-
if (cell.probe.mode
|
|
4149
|
+
if ((cell.probe.mode === "inflight" || cell.probe.mode === "stream") && !replyInFlight) await go();
|
|
2791
4150
|
const { prediction } = cell;
|
|
2792
|
-
|
|
4151
|
+
const http = cell.probe.http !== void 0;
|
|
4152
|
+
if (prediction.detects !== null && prediction.driver !== void 0) {
|
|
4153
|
+
if (prediction.detects) await driverOutcome(cell, () => ({
|
|
4154
|
+
...partial(),
|
|
4155
|
+
fault
|
|
4156
|
+
}), driverWithin(cell));
|
|
4157
|
+
else await sleep(valueIn(prediction.window, "s") * 1e3);
|
|
4158
|
+
await sleep(TAIL_MS);
|
|
4159
|
+
} else if (cell.probe.mode === "gap") {
|
|
4160
|
+
const gap = cell.probe.http?.gap === void 0 ? 0 : valueIn(cell.probe.http.gap, "s");
|
|
4161
|
+
const window = valueIn(prediction.detects ? prediction.high : prediction.window, "s");
|
|
4162
|
+
await probe.until(HTTP_DONE, (gap + window) * 2e3 + 1e4).catch(() => void 0);
|
|
4163
|
+
} else if (cell.fault === "stall") {
|
|
2793
4164
|
if (cell.stall === void 0) throw new Error(`${cell.name} says no stall's length`);
|
|
2794
4165
|
const sent = await requestSent(probe, 5e3);
|
|
2795
4166
|
heal = await lab.heal(faultAt, sent.t + valueIn(cell.stall, "s") * 1e9);
|
|
@@ -2810,10 +4181,12 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2810
4181
|
} else if (prediction.detects) {
|
|
2811
4182
|
const within = valueIn(prediction.high, "s") * 2e3 + 1e4;
|
|
2812
4183
|
if (cell.observer !== void 0 && cell.observer !== "client") await observerError(cell, partial, within);
|
|
4184
|
+
else if (http && cell.probe.mode !== "idle") await probe.until(HTTP_DONE, within).catch(() => void 0);
|
|
2813
4185
|
else await probe.until([
|
|
2814
4186
|
"error",
|
|
2815
4187
|
"eof",
|
|
2816
|
-
"deadline"
|
|
4188
|
+
"deadline",
|
|
4189
|
+
...http ? ["closed"] : []
|
|
2817
4190
|
], within).catch(() => void 0);
|
|
2818
4191
|
} else await sleep(valueIn(prediction.window, "s") * 1e3);
|
|
2819
4192
|
} finally {
|
|
@@ -2832,13 +4205,14 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2832
4205
|
index,
|
|
2833
4206
|
events: [...probe.events],
|
|
2834
4207
|
fault,
|
|
2835
|
-
...cell.fault === "stall" ? { heal } : {},
|
|
4208
|
+
...cell.fault === "stall" || thawing !== null ? { heal } : {},
|
|
2836
4209
|
kernel: lab.kernel.slice(from),
|
|
2837
4210
|
netns: lab.netns.client,
|
|
2838
4211
|
targetNetns: lab.netns.target,
|
|
2839
4212
|
lost,
|
|
2840
4213
|
...placed,
|
|
2841
|
-
...lab.topology === null ? {} : { middlebox: lab.middlebox.slice(middleboxFrom) }
|
|
4214
|
+
...lab.topology === null ? {} : { middlebox: lab.middlebox.slice(middleboxFrom) },
|
|
4215
|
+
...cell.target.lines === true ? { server: lab.server.slice(serverFrom) } : {}
|
|
2842
4216
|
};
|
|
2843
4217
|
};
|
|
2844
4218
|
/** This function divides a count of runs across environments, as equally as possible. */
|
|
@@ -2855,6 +4229,7 @@ const line = (cell, run, read) => `${cell.name}: env ${run.environment} ${run.wa
|
|
|
2855
4229
|
*/
|
|
2856
4230
|
const inEnvironment = async (cell, environment, count, say) => {
|
|
2857
4231
|
if (cell.group !== void 0) return inGroupEnvironment(cell, environment, count, say);
|
|
4232
|
+
if (cell.replicaSet !== void 0) return inReplicaSetEnvironment(cell, environment, count, say);
|
|
2858
4233
|
const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
|
|
2859
4234
|
try {
|
|
2860
4235
|
const settings = await lab.configure(cell.targetSettings ?? {});
|
|
@@ -2919,6 +4294,34 @@ const inGroupEnvironment = async (cell, environment, count, say) => {
|
|
|
2919
4294
|
}
|
|
2920
4295
|
};
|
|
2921
4296
|
/**
|
|
4297
|
+
* This function does the part of the runs of a replica set cell in one environment. The warm-up
|
|
4298
|
+
* is a whole run with its fault, as for a group under Sentinels. The set converges before each run.
|
|
4299
|
+
*/
|
|
4300
|
+
const inReplicaSetEnvironment = async (cell, environment, count, say) => {
|
|
4301
|
+
const lab = await openReplicaSetLab(cell.replicaSet);
|
|
4302
|
+
try {
|
|
4303
|
+
const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
|
|
4304
|
+
const warmup = {
|
|
4305
|
+
...await replicaSetOnce(lab, cell, environment, -1),
|
|
4306
|
+
warmup: true
|
|
4307
|
+
};
|
|
4308
|
+
say(line(cell, warmup, measure(cell, warmup)));
|
|
4309
|
+
const runs = [];
|
|
4310
|
+
for (let index = 0; index < count; index += 1) {
|
|
4311
|
+
const run = await replicaSetOnce(lab, cell, environment, index);
|
|
4312
|
+
runs.push(run);
|
|
4313
|
+
say(line(cell, run, measure(cell, run)));
|
|
4314
|
+
}
|
|
4315
|
+
return {
|
|
4316
|
+
manifest,
|
|
4317
|
+
runs,
|
|
4318
|
+
warmup
|
|
4319
|
+
};
|
|
4320
|
+
} finally {
|
|
4321
|
+
await lab.close();
|
|
4322
|
+
}
|
|
4323
|
+
};
|
|
4324
|
+
/**
|
|
2922
4325
|
* This function does the runs of a cell. They go across new environments, and each environment records
|
|
2923
4326
|
* its manifest. Then the verdict judges the runs. A median across the edge of the band extends
|
|
2924
4327
|
* the cell from 11 runs to 22, and then to 44, each extension in new environments. With a
|
|
@@ -2967,6 +4370,6 @@ const runCell = async (cell, say = () => {}) => {
|
|
|
2967
4370
|
}
|
|
2968
4371
|
};
|
|
2969
4372
|
//#endregion
|
|
2970
|
-
export { CONVERGE_MS, LISTING, NOISE, PUMP, RELATIVE, STAGES, STEPS, crossingsOf, deadlineOf, deviceOf, endpointAfter, errorsOf, fieldsOf, groupOnce, groupVerdictOf, harnessImage, infoOf, isHopPlace, judge, lagsOf, measure, measureGroup, medianInterval, memberConfig, nodeClientImage, openGroupLab, openLab, peersOf, quantileOf, reorderingOf, routerImage, routesOf, runCell, sentinelConfig, socketOf, spread, spreadOf, stagesOf, subscriptionsOf, templated, toxicBody, verdictOf, viewOf, writesOf };
|
|
4373
|
+
export { CONVERGE_MS, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, NOISE, PUMP, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, clockOffsetOf, crossingsOf, cutOf, deadlineOf, deviceOf, endpointAfter, errorsOf, fieldOf, fieldsOf, goHttpImage, groupOnce, groupVerdictOf, harnessImage, infoOf, initiateScript, isHopPlace, judge, lagsOf, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, nodeClientImage, nodeHttpImage, onMonotonic, openGroupLab, openLab, openReplicaSetLab, peersOf, quantileOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subscriptionsOf, templated, toxicBody, verdictOf, viewOf, writesOf };
|
|
2971
4374
|
|
|
2972
4375
|
//# sourceMappingURL=index.js.map
|