@mark1russell7/harness 0.6.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 +44 -2
- package/build/index.d.ts +409 -19
- package/build/index.js +1358 -103
- package/build/index.js.map +1 -1
- 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/package.json +7 -2
package/build/index.js
CHANGED
|
@@ -159,9 +159,10 @@ const groupVerdictOf = (prediction, counted) => {
|
|
|
159
159
|
for (const run of counted) {
|
|
160
160
|
const at = run.group?.stages[band.stage];
|
|
161
161
|
if (at === void 0) {
|
|
162
|
+
const who = band.stage.startsWith("+") ? "no Sentinel announced" : "no member or client marked";
|
|
162
163
|
outside.push({
|
|
163
164
|
run: name(run),
|
|
164
|
-
why:
|
|
165
|
+
why: `${who} ${band.stage}`
|
|
165
166
|
});
|
|
166
167
|
continue;
|
|
167
168
|
}
|
|
@@ -382,13 +383,367 @@ const verdictOfBand = (prediction, runs) => {
|
|
|
382
383
|
};
|
|
383
384
|
};
|
|
384
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
|
|
385
739
|
//#region src/timeline.ts
|
|
386
740
|
/**
|
|
387
741
|
* This module measures a run of a group cell. It gives the first write that a primary accepted
|
|
388
|
-
* 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
|
|
389
744
|
* became of each write.
|
|
390
745
|
*/
|
|
391
|
-
/** 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. */
|
|
392
747
|
const STAGES = [
|
|
393
748
|
"+sdown",
|
|
394
749
|
"+odown",
|
|
@@ -418,6 +773,105 @@ const stagesOf = (timeline, from) => {
|
|
|
418
773
|
}
|
|
419
774
|
return out;
|
|
420
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
|
+
};
|
|
421
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. */
|
|
422
876
|
const writesOf = (run, after, primary) => {
|
|
423
877
|
const { events } = run;
|
|
@@ -490,7 +944,10 @@ const lagsOf = (run, kernel) => {
|
|
|
490
944
|
* - The reference is the start of the commit of the fault, as the injector stamped it. For a
|
|
491
945
|
* stall, it is the first write into the fault, as for the stall of a direct pair.
|
|
492
946
|
* - The outcome is the first write that a primary accepted after the fault. The prediction says
|
|
493
|
-
* 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`).
|
|
494
951
|
* - Its time is the last segment with data that the client's socket to that primary received
|
|
495
952
|
* before the client saw the acceptance. The stamp of the client is the fallback.
|
|
496
953
|
* - A run is void when the prober lost records, or when the client did not become ready. It is
|
|
@@ -546,16 +1003,25 @@ const measureGroup = (cell, run) => {
|
|
|
546
1003
|
group: empty
|
|
547
1004
|
};
|
|
548
1005
|
const timeline = run.group?.timeline ?? [];
|
|
549
|
-
const stages = stagesOf(timeline, t0);
|
|
550
1006
|
const heal = run.heal ?? null;
|
|
551
1007
|
const lands = expected?.lands ?? "new";
|
|
552
|
-
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);
|
|
553
1014
|
const accepted = events.find((event) => {
|
|
554
1015
|
if (event.ev !== "ack" || event.t <= after) return false;
|
|
1016
|
+
if (replica && (sentAt.get(Number(event["n"])) ?? -Infinity) <= run.fault.t1) return false;
|
|
555
1017
|
const member = memberAt(run, event["at"]);
|
|
556
1018
|
return member !== null && (lands === "new" ? member !== primary : true);
|
|
557
1019
|
});
|
|
558
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
|
+
}
|
|
559
1025
|
const group = {
|
|
560
1026
|
from: primary,
|
|
561
1027
|
to,
|
|
@@ -580,7 +1046,7 @@ const measureGroup = (cell, run) => {
|
|
|
580
1046
|
}
|
|
581
1047
|
}
|
|
582
1048
|
const assumed = prediction.detects === null ? void 0 : prediction.rtt;
|
|
583
|
-
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);
|
|
584
1050
|
const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
|
|
585
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`;
|
|
586
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`;
|
|
@@ -596,8 +1062,12 @@ const measureGroup = (cell, run) => {
|
|
|
596
1062
|
}
|
|
597
1063
|
}
|
|
598
1064
|
const findings = [];
|
|
599
|
-
const switched = stages["+switch-master"];
|
|
1065
|
+
const switched = replica ? stages["elected"] : stages["+switch-master"];
|
|
600
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
|
+
}
|
|
601
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}`);
|
|
602
1072
|
for (const error of expected?.errors ?? []) {
|
|
603
1073
|
const met = group.errors.find((each) => each.name === error.name);
|
|
@@ -615,7 +1085,8 @@ const measureGroup = (cell, run) => {
|
|
|
615
1085
|
};
|
|
616
1086
|
}
|
|
617
1087
|
const address = String(accepted["at"]);
|
|
618
|
-
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);
|
|
619
1090
|
const at = segment?.mono ?? accepted.t;
|
|
620
1091
|
return {
|
|
621
1092
|
outcome: { kind: "answered" },
|
|
@@ -835,7 +1306,7 @@ const requestOf = (events) => {
|
|
|
835
1306
|
};
|
|
836
1307
|
/** This function reads the socket of one run against its cell, as `measure` does without the second request of an HTTP client. */
|
|
837
1308
|
const measureSocket = (cell, run) => {
|
|
838
|
-
if (cell.group !== void 0) {
|
|
1309
|
+
if (cell.group !== void 0 || cell.replicaSet !== void 0) {
|
|
839
1310
|
const read = measureGroup(cell, run);
|
|
840
1311
|
return {
|
|
841
1312
|
environment: run.environment,
|
|
@@ -853,6 +1324,7 @@ const measureSocket = (cell, run) => {
|
|
|
853
1324
|
group: read.group
|
|
854
1325
|
};
|
|
855
1326
|
}
|
|
1327
|
+
if (cell.prediction.detects !== null && cell.prediction.driver !== void 0) return measureDriver(cell, run);
|
|
856
1328
|
const { events, kernel } = run;
|
|
857
1329
|
const ready = events.find((event) => event.ev === "ready");
|
|
858
1330
|
const local = events.find((event) => event.ev === "connected")?.["local"];
|
|
@@ -960,6 +1432,7 @@ const measureSocket = (cell, run) => {
|
|
|
960
1432
|
const referenceAt = (from) => {
|
|
961
1433
|
if (from === "request-sent") return request.t;
|
|
962
1434
|
if (from === "fault") return run.fault?.t0;
|
|
1435
|
+
if (from === "client-start") return (events.find((event) => event.ev === "connecting") ?? events[0])?.t;
|
|
963
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;
|
|
964
1437
|
};
|
|
965
1438
|
const lastRx = kernel.filter((each) => each.netns === run.netns && each.src === local && each.ev === "rx" && each.mono <= before).at(-1)?.mono ?? ready.t;
|
|
@@ -1200,6 +1673,12 @@ const NODE_HTTP = {
|
|
|
1200
1673
|
dockerfile: "harness/image/node-http/Dockerfile",
|
|
1201
1674
|
from: ["harness/image/node-http"]
|
|
1202
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
|
+
};
|
|
1203
1682
|
/** This image is the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow. */
|
|
1204
1683
|
const ROUTER = {
|
|
1205
1684
|
name: "loom-harness-router",
|
|
@@ -1270,6 +1749,8 @@ const routerImage = () => build(ROUTER);
|
|
|
1270
1749
|
const goHttpImage = () => build(GO_HTTP);
|
|
1271
1750
|
/** This function gives the image of the Node HTTP clients. */
|
|
1272
1751
|
const nodeHttpImage = () => build(NODE_HTTP);
|
|
1752
|
+
/** This function gives the image of the MongoDB client. */
|
|
1753
|
+
const mongoClientImage = () => build(MONGO);
|
|
1273
1754
|
/**
|
|
1274
1755
|
* This time is the interval of the listing of a router, when its hop does not set one. A listing
|
|
1275
1756
|
* evicts each expired flow that it walks past, and the DESTROY event of that flow follows.
|
|
@@ -1422,6 +1903,11 @@ const clientOf = async (kind, image) => {
|
|
|
1422
1903
|
command: ["node", `/opt/harness/node-http/client-${kind === "undici" ? "undici" : "agent"}.mjs`],
|
|
1423
1904
|
clientKind: kind
|
|
1424
1905
|
};
|
|
1906
|
+
if (kind === "mongodb") return {
|
|
1907
|
+
clientImage: await mongoClientImage(),
|
|
1908
|
+
command: ["node", "/opt/harness/mongo/client-mongo.mjs"],
|
|
1909
|
+
clientKind: kind
|
|
1910
|
+
};
|
|
1425
1911
|
return {
|
|
1426
1912
|
clientImage: image,
|
|
1427
1913
|
command: ["/opt/harness/client"],
|
|
@@ -1675,6 +2161,7 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1675
2161
|
};
|
|
1676
2162
|
const paused = runtime.container.getById(server.getId());
|
|
1677
2163
|
let targetAdmin;
|
|
2164
|
+
let signaller;
|
|
1678
2165
|
const hopNamed = (name) => {
|
|
1679
2166
|
const found = inChain.find((hop) => hop.hop.name === name);
|
|
1680
2167
|
if (found === void 0) throw new Error(`the topology has no hop ${name}`);
|
|
@@ -1937,6 +2424,19 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1937
2424
|
},
|
|
1938
2425
|
freeze: () => paused.pause().then(() => void 0),
|
|
1939
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
|
+
},
|
|
1940
2440
|
async probe(args) {
|
|
1941
2441
|
const events = lines();
|
|
1942
2442
|
const container = await new GenericContainer(clientImage).withNetworkMode(`container:${client.getId()}`).withCommand([...command, ...args]).withLogConsumer(events.consume).start();
|
|
@@ -2153,7 +2653,7 @@ const viewOf = (group, ips, sentinels, members) => {
|
|
|
2153
2653
|
why: "healthy"
|
|
2154
2654
|
};
|
|
2155
2655
|
};
|
|
2156
|
-
const sleep$
|
|
2656
|
+
const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2157
2657
|
/** This time is the longest that a group takes to converge before a run, or after it. */
|
|
2158
2658
|
const CONVERGE_MS = 24e4;
|
|
2159
2659
|
/**
|
|
@@ -2348,7 +2848,7 @@ const openGroupLab = async (group, options = {}) => {
|
|
|
2348
2848
|
}));
|
|
2349
2849
|
calm = last.healthy ? calm + 1 : 0;
|
|
2350
2850
|
if (calm >= 2 && last.primary !== null) return last.primary;
|
|
2351
|
-
await sleep$
|
|
2851
|
+
await sleep$2(1e3);
|
|
2352
2852
|
}
|
|
2353
2853
|
throw new Error(`the group did not converge in ${within} ms: ${last.why}`);
|
|
2354
2854
|
},
|
|
@@ -2426,7 +2926,7 @@ const openGroupLab = async (group, options = {}) => {
|
|
|
2426
2926
|
const started = Date.now();
|
|
2427
2927
|
while (await cli(member, ["PING"]).catch(() => "") !== "PONG") {
|
|
2428
2928
|
if (Date.now() - started > 3e4) throw new Error(`${member} did not start again`);
|
|
2429
|
-
await sleep$
|
|
2929
|
+
await sleep$2(200);
|
|
2430
2930
|
}
|
|
2431
2931
|
},
|
|
2432
2932
|
failover(coordinated) {
|
|
@@ -2544,9 +3044,9 @@ const openGroupLab = async (group, options = {}) => {
|
|
|
2544
3044
|
}
|
|
2545
3045
|
};
|
|
2546
3046
|
/** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
|
|
2547
|
-
const STEADY = 20;
|
|
3047
|
+
const STEADY$1 = 20;
|
|
2548
3048
|
/** This time is how long a run continues after its outcome, so that the record shows what came next. */
|
|
2549
|
-
const TAIL_MS = 2e3;
|
|
3049
|
+
const TAIL_MS$2 = 2e3;
|
|
2550
3050
|
/** This function gives the address of a member, with the port of the group. */
|
|
2551
3051
|
const addressOf = (lab, member) => `${lab.members[member].ip}:${lab.group.port}`;
|
|
2552
3052
|
/** This function gives the latest time that a group cell waits for its outcome, in ms from the fault. */
|
|
@@ -2569,7 +3069,7 @@ const clientArgs = (cell, lab) => {
|
|
|
2569
3069
|
];
|
|
2570
3070
|
};
|
|
2571
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. */
|
|
2572
|
-
const waitFor = (client, match, within) => new Promise((resolve) => {
|
|
3072
|
+
const waitFor$1 = (client, match, within) => new Promise((resolve) => {
|
|
2573
3073
|
const started = Date.now();
|
|
2574
3074
|
const look = () => {
|
|
2575
3075
|
const found = client.events.find(match);
|
|
@@ -2585,7 +3085,7 @@ const subscriptionsOf = (timeline) => {
|
|
|
2585
3085
|
return [...latest.values()];
|
|
2586
3086
|
};
|
|
2587
3087
|
/** This function gives the count of lost records in the latest clock line of the prober. */
|
|
2588
|
-
const droppedSoFar$
|
|
3088
|
+
const droppedSoFar$2 = (records) => {
|
|
2589
3089
|
for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
|
|
2590
3090
|
return 0;
|
|
2591
3091
|
};
|
|
@@ -2607,7 +3107,7 @@ const groupOnce = async (lab, cell, environment, index) => {
|
|
|
2607
3107
|
await lab.forget();
|
|
2608
3108
|
const from = lab.kernel.length;
|
|
2609
3109
|
const timelineFrom = lab.timeline.length;
|
|
2610
|
-
const lostBefore = droppedSoFar$
|
|
3110
|
+
const lostBefore = droppedSoFar$2(lab.kernel);
|
|
2611
3111
|
const client = await lab.client(clientArgs(cell, lab));
|
|
2612
3112
|
const lands = cell.prediction.group?.lands ?? "new";
|
|
2613
3113
|
const place = cell.faultAt ?? { role: "primary" };
|
|
@@ -2617,7 +3117,7 @@ const groupOnce = async (lab, cell, environment, index) => {
|
|
|
2617
3117
|
const undo = [];
|
|
2618
3118
|
try {
|
|
2619
3119
|
await client.until(["ready"], 6e4);
|
|
2620
|
-
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`);
|
|
2621
3121
|
let healing = null;
|
|
2622
3122
|
switch (cell.fault) {
|
|
2623
3123
|
case "blackhole":
|
|
@@ -2631,7 +3131,7 @@ const groupOnce = async (lab, cell, environment, index) => {
|
|
|
2631
3131
|
undo.push(() => lab.lift(kind, part, cuts));
|
|
2632
3132
|
if (cell.stall !== void 0) {
|
|
2633
3133
|
const t1 = fault.t1;
|
|
2634
|
-
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);
|
|
2635
3135
|
if (sent !== void 0) healing = lab.lift(kind, part, cuts, sent.t + valueIn(cell.stall, "s") * 1e9);
|
|
2636
3136
|
} else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
|
|
2637
3137
|
break;
|
|
@@ -2665,11 +3165,11 @@ const groupOnce = async (lab, cell, environment, index) => {
|
|
|
2665
3165
|
const t0 = fault.t0;
|
|
2666
3166
|
const old = addressOf(lab, primary);
|
|
2667
3167
|
const deadline = deadlineOf(cell);
|
|
2668
|
-
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 () => {
|
|
2669
3169
|
await healed;
|
|
2670
3170
|
const after = heal?.t0 ?? t0;
|
|
2671
|
-
return waitFor(client, (event) => event.ev === "ack" && event.t > after, deadline);
|
|
2672
|
-
})()) !== 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);
|
|
2673
3173
|
await healed;
|
|
2674
3174
|
} finally {
|
|
2675
3175
|
for (const step of undo.reverse()) await step().catch(() => void 0);
|
|
@@ -2702,91 +3202,727 @@ const groupOnce = async (lab, cell, environment, index) => {
|
|
|
2702
3202
|
};
|
|
2703
3203
|
};
|
|
2704
3204
|
//#endregion
|
|
2705
|
-
//#region src/
|
|
2706
|
-
/**
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
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;
|
|
2713
3225
|
return {
|
|
2714
|
-
|
|
2715
|
-
|
|
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 ?? {}
|
|
2716
3234
|
};
|
|
2717
3235
|
};
|
|
2718
|
-
/** This function gives the count of lost records in the latest clock line of the prober, since it began. */
|
|
2719
|
-
const droppedSoFar = (records) => {
|
|
2720
|
-
for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
|
|
2721
|
-
return 0;
|
|
2722
|
-
};
|
|
2723
|
-
/** This function gives the argv of the client for a cell, with a deadline far after all that it waits for. */
|
|
2724
|
-
const probeArgs = (cell, lab) => {
|
|
2725
|
-
const { prediction, probe } = cell;
|
|
2726
|
-
const longest = (prediction.detects ? valueIn(prediction.high, "s") : valueIn(prediction.window, "s")) + (cell.idleBefore === void 0 ? 0 : valueIn(cell.idleBefore, "s"));
|
|
2727
|
-
const args = [
|
|
2728
|
-
lab.targetIp,
|
|
2729
|
-
String(lab.targetPort),
|
|
2730
|
-
"--mode",
|
|
2731
|
-
probe.mode
|
|
2732
|
-
];
|
|
2733
|
-
if (probe.http !== void 0) {
|
|
2734
|
-
const { http } = probe;
|
|
2735
|
-
const gap = http.gap === void 0 ? 0 : valueIn(http.gap, "s");
|
|
2736
|
-
args.push("--method", http.method);
|
|
2737
|
-
if (http.gap !== void 0) args.push("--gap-ms", String(valueIn(http.gap, "ms")));
|
|
2738
|
-
if (http.hold !== void 0) args.push("--hold-ms", String(valueIn(http.hold, "ms")));
|
|
2739
|
-
if (http.bytes !== void 0) args.push("--bytes", String(http.bytes));
|
|
2740
|
-
for (const [name, value] of Object.entries(http.options ?? {})) args.push(`--${name}`, String(value));
|
|
2741
|
-
args.push("--linger-ms", String(LINGER_MS));
|
|
2742
|
-
args.push("--max-s", String(Math.ceil(longest * 2 + gap + 10)));
|
|
2743
|
-
return args;
|
|
2744
|
-
}
|
|
2745
|
-
if (probe.request !== void 0) args.push("--request", probe.request);
|
|
2746
|
-
if (probe.count !== void 0) args.push("--count", String(probe.count));
|
|
2747
|
-
if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
|
|
2748
|
-
if (probe.driver !== void 0) {
|
|
2749
|
-
args.push("--keepalive-ms", String(valueIn(probe.driver.keepAlive, "ms")));
|
|
2750
|
-
args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
|
|
2751
|
-
return args;
|
|
2752
|
-
}
|
|
2753
|
-
if (probe.keepalive) args.push("--keepalive");
|
|
2754
|
-
if (probe.userTimeout !== void 0) args.push("--user-timeout-ms", String(valueIn(probe.userTimeout, "ms")));
|
|
2755
|
-
args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
|
|
2756
|
-
return args;
|
|
2757
|
-
};
|
|
2758
3236
|
/**
|
|
2759
|
-
* This function
|
|
2760
|
-
*
|
|
2761
|
-
*
|
|
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.
|
|
2762
3240
|
*/
|
|
2763
|
-
const
|
|
2764
|
-
const
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
});
|
|
2773
|
-
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
|
+
}));
|
|
2774
3250
|
/**
|
|
2775
|
-
* This
|
|
2776
|
-
*
|
|
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.
|
|
2777
3255
|
*/
|
|
2778
|
-
const
|
|
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 })) }))";
|
|
2779
3283
|
/**
|
|
2780
|
-
* This
|
|
2781
|
-
*
|
|
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.
|
|
2782
3287
|
*/
|
|
2783
|
-
const
|
|
2784
|
-
|
|
2785
|
-
const
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
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
|
+
];
|
|
2790
3926
|
/**
|
|
2791
3927
|
* This function waits for the request that an in-flight client sends on the signal: its second
|
|
2792
3928
|
* send, after the PING that made it ready. Its stamp is when the request first met the fault.
|
|
@@ -2801,6 +3937,69 @@ const requestSent = (probe, within) => new Promise((resolve, reject) => {
|
|
|
2801
3937
|
};
|
|
2802
3938
|
look();
|
|
2803
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
|
+
};
|
|
2804
4003
|
/** This function gives the netem specification of a cell: its latency, then the netem of its fault when it has one. */
|
|
2805
4004
|
const netemSpec = (cell, fault) => {
|
|
2806
4005
|
const parts = [...cell.latency === void 0 ? [] : [`delay ${valueIn(cell.latency, "ms")}ms`], ...fault === void 0 ? [] : [fault]];
|
|
@@ -2820,6 +4019,7 @@ const netemSpec = (cell, fault) => {
|
|
|
2820
4019
|
* A dry run does all of this but the fault: the warm-up of a cell whose fault ends its environment.
|
|
2821
4020
|
*/
|
|
2822
4021
|
const once = async (lab, cell, environment, index, dry = false) => {
|
|
4022
|
+
if (cell.before === true) return beforeOnce(lab, cell, environment, index);
|
|
2823
4023
|
await lab.forget();
|
|
2824
4024
|
const from = lab.kernel.length;
|
|
2825
4025
|
const middleboxFrom = lab.middlebox.length;
|
|
@@ -2853,6 +4053,8 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2853
4053
|
});
|
|
2854
4054
|
let fault = null;
|
|
2855
4055
|
let heal = null;
|
|
4056
|
+
const signalled = !server && cell.target.program !== void 0;
|
|
4057
|
+
let thawing = null;
|
|
2856
4058
|
try {
|
|
2857
4059
|
await probe.until(["ready"], 15e3);
|
|
2858
4060
|
if (dry) {
|
|
@@ -2879,6 +4081,17 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2879
4081
|
const apply = async () => {
|
|
2880
4082
|
switch (cell.fault) {
|
|
2881
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
|
+
}
|
|
2882
4095
|
await peer.freeze();
|
|
2883
4096
|
return null;
|
|
2884
4097
|
case "fin":
|
|
@@ -2904,7 +4117,13 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2904
4117
|
};
|
|
2905
4118
|
const lift = async () => {
|
|
2906
4119
|
switch (cell.fault) {
|
|
2907
|
-
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();
|
|
2908
4127
|
case "fin":
|
|
2909
4128
|
case "middlebox-forget":
|
|
2910
4129
|
case "proxy-idle":
|
|
@@ -2930,7 +4149,14 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2930
4149
|
if ((cell.probe.mode === "inflight" || cell.probe.mode === "stream") && !replyInFlight) await go();
|
|
2931
4150
|
const { prediction } = cell;
|
|
2932
4151
|
const http = cell.probe.http !== void 0;
|
|
2933
|
-
if (
|
|
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") {
|
|
2934
4160
|
const gap = cell.probe.http?.gap === void 0 ? 0 : valueIn(cell.probe.http.gap, "s");
|
|
2935
4161
|
const window = valueIn(prediction.detects ? prediction.high : prediction.window, "s");
|
|
2936
4162
|
await probe.until(HTTP_DONE, (gap + window) * 2e3 + 1e4).catch(() => void 0);
|
|
@@ -2979,7 +4205,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
2979
4205
|
index,
|
|
2980
4206
|
events: [...probe.events],
|
|
2981
4207
|
fault,
|
|
2982
|
-
...cell.fault === "stall" ? { heal } : {},
|
|
4208
|
+
...cell.fault === "stall" || thawing !== null ? { heal } : {},
|
|
2983
4209
|
kernel: lab.kernel.slice(from),
|
|
2984
4210
|
netns: lab.netns.client,
|
|
2985
4211
|
targetNetns: lab.netns.target,
|
|
@@ -3003,6 +4229,7 @@ const line = (cell, run, read) => `${cell.name}: env ${run.environment} ${run.wa
|
|
|
3003
4229
|
*/
|
|
3004
4230
|
const inEnvironment = async (cell, environment, count, say) => {
|
|
3005
4231
|
if (cell.group !== void 0) return inGroupEnvironment(cell, environment, count, say);
|
|
4232
|
+
if (cell.replicaSet !== void 0) return inReplicaSetEnvironment(cell, environment, count, say);
|
|
3006
4233
|
const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
|
|
3007
4234
|
try {
|
|
3008
4235
|
const settings = await lab.configure(cell.targetSettings ?? {});
|
|
@@ -3067,6 +4294,34 @@ const inGroupEnvironment = async (cell, environment, count, say) => {
|
|
|
3067
4294
|
}
|
|
3068
4295
|
};
|
|
3069
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
|
+
/**
|
|
3070
4325
|
* This function does the runs of a cell. They go across new environments, and each environment records
|
|
3071
4326
|
* its manifest. Then the verdict judges the runs. A median across the edge of the band extends
|
|
3072
4327
|
* the cell from 11 runs to 22, and then to 44, each extension in new environments. With a
|
|
@@ -3115,6 +4370,6 @@ const runCell = async (cell, say = () => {}) => {
|
|
|
3115
4370
|
}
|
|
3116
4371
|
};
|
|
3117
4372
|
//#endregion
|
|
3118
|
-
export { CONVERGE_MS, LINGER_MS, LISTING, NOISE, PUMP, QUIET_MS, RELATIVE, STAGES, STEPS, crossingsOf, deadlineOf, deviceOf, endpointAfter, errorsOf, fieldsOf, goHttpImage, groupOnce, groupVerdictOf, harnessImage, infoOf, isHopPlace, judge, lagsOf, measure, measureGroup, medianInterval, memberConfig, nodeClientImage, nodeHttpImage, openGroupLab, openLab, peersOf, quantileOf, reorderingOf, requestOf, requestWords, 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 };
|
|
3119
4374
|
|
|
3120
4375
|
//# sourceMappingURL=index.js.map
|