@mark1russell7/harness 0.6.0 → 0.8.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/build/index.js CHANGED
@@ -119,7 +119,8 @@ const middle = (sorted) => {
119
119
  const verdictOf = (prediction, runs) => {
120
120
  const band = verdictOfBand(prediction, runs);
121
121
  const counted = runs.filter((run) => run.void === null);
122
- const verdict = counted.some((run) => run.request !== void 0) ? withRequests(band, counted) : band;
122
+ const asked = counted.some((run) => run.request !== void 0) ? withRequests(band, counted) : band;
123
+ const verdict = withProgram(asked, counted);
123
124
  if (prediction.group === void 0 && !counted.some((run) => run.group !== void 0)) return verdict;
124
125
  return {
125
126
  ...verdict,
@@ -127,6 +128,43 @@ const verdictOf = (prediction, runs) => {
127
128
  };
128
129
  };
129
130
  /**
131
+ * This function adds the reconnect of the client and the outcomes of its call in flight to a
132
+ * verdict. Its reason gets a sentence for each. A verdict whose runs have neither stays as it is.
133
+ */
134
+ const withProgram = (verdict, counted) => {
135
+ const reconnects = counted.flatMap((run) => run.reconnect === void 0 ? [] : [run.reconnect]);
136
+ const calls = counted.flatMap((run) => run.call === void 0 ? [] : [run.call]);
137
+ const lefts = counted.flatMap((run) => run.left === void 0 ? [] : [run.left]);
138
+ if (reconnects.length === 0 && calls.length === 0 && lefts.length === 0) return verdict;
139
+ const ms = (spread) => spread === null ? "none" : `${Math.round(valueIn(spread.median, "s") * 1e6) / 1e3} ms`;
140
+ const tries = Math.max(0, ...reconnects.map((each) => each.waits.length));
141
+ const reconnect = reconnects.length === 0 ? void 0 : {
142
+ waits: Array.from({ length: tries }, (_, at) => spreadOf(reconnects.flatMap((each) => each.waits[at] === void 0 ? [] : [each.waits[at]]))),
143
+ syn: spreadOf(reconnects.flatMap((each) => each.syn === null ? [] : [each.syn])),
144
+ established: spreadOf(reconnects.flatMap((each) => each.established === null ? [] : [each.established])),
145
+ connect: spreadOf(reconnects.flatMap((each) => each.connects))
146
+ };
147
+ const outcomes = {};
148
+ for (const call of calls) outcomes[call.outcome] = (outcomes[call.outcome] ?? 0) + 1;
149
+ const gone = lefts.flatMap((each) => each === null ? [] : [each]);
150
+ const left = lefts.length === 0 ? void 0 : {
151
+ spread: spreadOf(gone),
152
+ stayed: lefts.length - gone.length
153
+ };
154
+ const said = [
155
+ ...left === void 0 ? [] : [`After the drop, the program closed its socket in ${gone.length} runs, median ${ms(left.spread)} after its drop line${left.stayed === 0 ? "" : `, and it did not close it in ${left.stayed}`}.`],
156
+ ...reconnect === void 0 ? [] : [`Reconnect: the waits before each try, median ${reconnect.waits.map(ms).join(", ")}; the first SYN median ${ms(reconnect.syn)} and ESTABLISHED median ${ms(reconnect.established)} after the detection.`],
157
+ ...calls.length === 0 ? [] : [`Calls in flight: ${Object.entries(outcomes).map(([outcome, n]) => `${n} ${outcome}`).join("; ")}.`]
158
+ ];
159
+ return {
160
+ ...verdict,
161
+ ...reconnect === void 0 ? {} : { reconnect },
162
+ ...calls.length === 0 ? {} : { calls: outcomes },
163
+ ...left === void 0 ? {} : { left },
164
+ why: `${verdict.why.endsWith(".") ? verdict.why : `${verdict.why}.`} ${said.join(" ")}`
165
+ };
166
+ };
167
+ /**
130
168
  * This function adds the outcomes of the second request of an HTTP client to a verdict. A cell
131
169
  * whose outcome is a distribution gets them in its reason too.
132
170
  */
@@ -159,9 +197,10 @@ const groupVerdictOf = (prediction, counted) => {
159
197
  for (const run of counted) {
160
198
  const at = run.group?.stages[band.stage];
161
199
  if (at === void 0) {
200
+ const who = band.stage.startsWith("+") ? "no Sentinel announced" : "no member or client marked";
162
201
  outside.push({
163
202
  run: name(run),
164
- why: `no Sentinel announced ${band.stage}`
203
+ why: `${who} ${band.stage}`
165
204
  });
166
205
  continue;
167
206
  }
@@ -382,13 +421,511 @@ const verdictOfBand = (prediction, runs) => {
382
421
  };
383
422
  };
384
423
  //#endregion
424
+ //#region src/application.ts
425
+ /**
426
+ * This module reads the acts of the program of the observer: its heartbeat, its drop of the
427
+ * connection, its reconnect and its call in flight. The kernel has no report of a detection by
428
+ * a program. Thus the program writes a line before each of its own acts, on CLOCK_MONOTONIC:
429
+ * - `ping` before each ping of its heartbeat, and `drop` before it closes the connection by itself.
430
+ * - `reconnecting` when it starts its reconnect timer, and `dial` before each new connection.
431
+ *
432
+ * The kernel then stamps each write of the program on the socket (`tx`) and each change of the
433
+ * state of the socket. A line of the client names its socket by its local address (`local`), and
434
+ * a line of a server names it by its peer (`peer`).
435
+ */
436
+ /** This function gives the interval between two instants on the one clock, in seconds to the microsecond. */
437
+ const between$3 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
438
+ /** This function gives the state of a state record after the change, and before it. TCP_ESTABLISHED is 1, TCP_SYN_SENT 2. */
439
+ const newState$2 = (record) => record.new;
440
+ const oldState$2 = (record) => record.old;
441
+ /** This function gives the lines of the program of the observer: the client, or the target for the server. A proxy has no lines. */
442
+ const programLines = (cell, run) => {
443
+ const observer = cell.observer ?? "client";
444
+ if (observer === "client") return run.events;
445
+ if (observer === "server") return run.server ?? [];
446
+ return [];
447
+ };
448
+ /**
449
+ * This function tells if a line of the program is about the connection of the run. The client
450
+ * names it by its local address, and a server by its peer. A line with neither is about the one
451
+ * connection of its program. A line whose address is null is about a socket that did not open:
452
+ * a try that failed.
453
+ */
454
+ const onConnection = (line, local) => {
455
+ if (!("local" in line) && !("peer" in line)) return true;
456
+ return (line["local"] ?? line["peer"]) === local;
457
+ };
458
+ /** This function gives the first record of a socket that matches, at the stamp of a line or in δ_abs after it. */
459
+ const kernelAt = (socket, line, matches) => {
460
+ const noise = valueIn(NOISE, "s") * 1e9;
461
+ return socket.find((record) => record.mono >= line.t && record.mono <= line.t + noise && matches(record));
462
+ };
463
+ const eventOf = (socket, line, matches) => {
464
+ const record = kernelAt(socket, line, matches);
465
+ return {
466
+ line,
467
+ at: record?.mono ?? line.t,
468
+ kernel: record !== void 0,
469
+ record: record === void 0 ? null : record.ev === "tx" ? "write" : "state"
470
+ };
471
+ };
472
+ /**
473
+ * This function gives the time from a line of the program to the close of the socket by its own
474
+ * side. That close is its change from ESTABLISHED to FIN_WAIT1, or to CLOSE (an abort). A FIN of
475
+ * the peer (CLOSE_WAIT) is not such a close. It is null when the socket did not close itself.
476
+ */
477
+ const leftAfter = (socket, line) => {
478
+ const left = socket.find((record) => record.ev === "state" && oldState$2(record) === 1 && (newState$2(record) === 4 || newState$2(record) === 7) && record.mono >= line.t);
479
+ return left === void 0 ? null : between$3(line.t, left.mono);
480
+ };
481
+ /**
482
+ * This function tells how a drop differs from the way that its prediction gives, or gives null.
483
+ * A close closes the socket in δ_abs. A close frame is a write, and the program does not close
484
+ * the socket at the drop. A peer that answers the close frame can close it, and that is no finding.
485
+ */
486
+ const dropFinding = (expected, drop, left) => {
487
+ const noise = valueIn(NOISE, "s");
488
+ const soon = left !== null && valueIn(left, "s") <= noise;
489
+ if (expected === "close") {
490
+ if (left === null) return "the program did not close its socket after the drop, where a close is at once";
491
+ return soon ? null : `the program closed its socket ${left.value} s after the drop, not at once`;
492
+ }
493
+ if (drop.record !== "write") return "the drop wrote no close frame on its socket";
494
+ return soon ? "the program closed its socket at the drop, where a close frame leaves it open" : null;
495
+ };
496
+ const isWrite = (record) => record.ev === "tx";
497
+ /** This function gives each ping of the program on the connection of the run, with the kernel's stamp of its write. */
498
+ const pingsOf = (lines, socket, local) => lines.filter((line) => line.ev === "ping" && onConnection(line, local)).map((line) => eventOf(socket, line, isWrite));
499
+ /**
500
+ * This function gives the first ping that got no answer: no segment with data reached the socket
501
+ * after its write, up to `until`. Its write starts the wait of the heartbeat that the fault met.
502
+ */
503
+ const firstUnanswered = (pings, socket, until = Number.POSITIVE_INFINITY) => pings.filter((ping) => ping.at <= until).find((ping) => !socket.some((record) => record.ev === "rx" && (record.seg?.len.value ?? 0) > 0 && record.mono > ping.at && record.mono <= until));
504
+ /**
505
+ * This function gives the first drop of the connection by its program. The kernel stamps it as the
506
+ * write of a close frame. Or it stamps the change of the socket out of ESTABLISHED: a close or an
507
+ * abort.
508
+ */
509
+ const dropOf = (lines, socket, local) => {
510
+ const line = lines.find((each) => each.ev === "drop" && onConnection(each, local));
511
+ return line === void 0 ? void 0 : eventOf(socket, line, (record) => isWrite(record) || record.ev === "state" && oldState$2(record) === 1);
512
+ };
513
+ /**
514
+ * This function reads the reconnect of the client after the detection at `detected`. The socket of
515
+ * the run has the cookie `cookie`. The lines of the program count from `from`: the drop line of a
516
+ * drop, which comes before the kernel's stamp of it.
517
+ */
518
+ const reconnectOf = (run, detected, cookie, from = detected) => {
519
+ const after = run.events.filter((line) => line.t > from);
520
+ const starts = after.filter((line) => line.ev === "reconnecting");
521
+ const dials = after.filter((line) => line.ev === "dial");
522
+ const waits = starts.flatMap((start, at) => {
523
+ const next = starts[at + 1]?.t ?? Number.POSITIVE_INFINITY;
524
+ const dial = dials.find((each) => each.t >= start.t && each.t < next);
525
+ return dial === void 0 ? [] : [between$3(start.t, dial.t)];
526
+ });
527
+ const fresh = run.kernel.filter((record) => record.netns === run.netns && record.cookie !== cookie && record.ev === "state" && record.mono > detected);
528
+ const syns = fresh.filter((record) => newState$2(record) === 2);
529
+ const established = fresh.find((record) => newState$2(record) === 1);
530
+ const connects = dials.flatMap((dial) => {
531
+ const syn = syns.find((record) => record.mono >= dial.t);
532
+ return syn === void 0 ? [] : [between$3(dial.t, syn.mono)];
533
+ });
534
+ return {
535
+ waits,
536
+ syn: syns[0] === void 0 ? null : between$3(detected, syns[0].mono),
537
+ established: established === void 0 ? null : between$3(detected, established.mono),
538
+ connects
539
+ };
540
+ };
541
+ /** This function tells how the waits of a reconnect differ from their bands, each widened by δ_abs, or gives null. */
542
+ const reconnectFinding = (expected, read) => {
543
+ const noise = valueIn(NOISE, "s");
544
+ const round = (value) => Math.round(value * 1e6) / 1e6;
545
+ const found = expected.waits.flatMap((band, at) => {
546
+ const wait = read.waits[at];
547
+ if (wait === void 0) return [`the client made no try ${at + 1} after the detection`];
548
+ const value = valueIn(wait, "s");
549
+ const low = round(valueIn(band.low, "s") - noise);
550
+ const high = round(valueIn(band.high, "s") + noise);
551
+ return value < low || value > high ? [`its wait before try ${at + 1} was ${value} s, outside [${low}, ${high}] s`] : [];
552
+ });
553
+ return found.length === 0 ? null : found.join(". ");
554
+ };
555
+ /** This function reads the call that the client sent into the fault: its `call-error` or `data` line after the request. */
556
+ const callOf = (events, request, detected) => {
557
+ const end = events.find((line) => (line.ev === "call-error" || line.ev === "data") && line.t > request.t);
558
+ if (end === void 0) return {
559
+ outcome: "pending",
560
+ after: null
561
+ };
562
+ return {
563
+ outcome: end.ev === "data" ? "a reply" : String(end["message"] ?? end["code"] ?? "an error"),
564
+ after: detected === void 0 ? null : between$3(detected, end.t)
565
+ };
566
+ };
567
+ //#endregion
568
+ //#region src/driver.ts
569
+ /** These are the errno numbers that the kernel reports to a socket, by the names that predictions use. */
570
+ const ERRNO$1 = {
571
+ 32: "EPIPE",
572
+ 101: "ENETUNREACH",
573
+ 103: "ECONNABORTED",
574
+ 104: "ECONNRESET",
575
+ 110: "ETIMEDOUT",
576
+ 111: "ECONNREFUSED",
577
+ 113: "EHOSTUNREACH"
578
+ };
579
+ const between$2 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
580
+ const round = (seconds) => Math.round(seconds * 1e6) / 1e6;
581
+ /** This function gives the value of a field of a line by its name. A dotted name is a field in a field. */
582
+ const fieldOf = (line, path) => path.split(".").reduce((value, key) => value !== null && typeof value === "object" ? value[key] : void 0, line);
583
+ /** This function tells if a line of the client matches a line match: its `ev`, and each field that the match names. */
584
+ const matches = (line, match) => line.ev === match.ev && Object.entries(match.where ?? {}).every(([path, value]) => fieldOf(line, path) === value);
585
+ /** This function says a line match in words, for a finding. */
586
+ const wordsOf = (match) => {
587
+ const where = Object.entries(match.where ?? {}).map(([path, value]) => `${path} ${String(value)}`);
588
+ return where.length === 0 ? `a ${match.ev} line` : `a ${match.ev} line with ${where.join(", ")}`;
589
+ };
590
+ /** This function gives the sockets of the driver in a run, from the `socket` and `socket-end` lines of the client. */
591
+ const socketsOf = (events) => events.filter((event) => event.ev === "socket").map((event) => {
592
+ const local = String(event["local"]);
593
+ const end = events.find((each) => each.ev === "socket-end" && each["local"] === local && each.t >= event.t);
594
+ return {
595
+ role: String(event["role"]),
596
+ local,
597
+ server: String(event["server"]),
598
+ opened: event.t,
599
+ ended: end?.t ?? null
600
+ };
601
+ });
602
+ /**
603
+ * This function finds the socket of a role at an instant. The pool's socket is the first one, which
604
+ * the client names in its `connected` line. Each other role has the latest socket that opened at
605
+ * the instant or before it.
606
+ */
607
+ const socketAt = (events, role, at) => {
608
+ const sockets = socketsOf(events);
609
+ if (role === "pool") {
610
+ const local = events.find((event) => event.ev === "connected")?.["local"];
611
+ return sockets.find((socket) => socket.local === local);
612
+ }
613
+ return sockets.filter((socket) => socket.role === role && socket.opened <= at).at(-1);
614
+ };
615
+ const placeOf = (run, netns, find) => {
616
+ const cookie = run.kernel.find((record) => record.netns === netns && find(record))?.cookie;
617
+ if (cookie === void 0) return void 0;
618
+ return {
619
+ netns,
620
+ cookie,
621
+ records: run.kernel.filter((record) => record.netns === netns && record.cookie === cookie)
622
+ };
623
+ };
624
+ const portOf = (address) => address.split(":").at(-1) ?? "";
625
+ /**
626
+ * This function gives the socket of the target for a socket of the client. In a direct pair, its
627
+ * peer is the address of the client socket. Behind a router, its peer is the address of the router
628
+ * with the port of the client socket. The router keeps the port when it is free.
629
+ */
630
+ const peerPlace = (run, local) => {
631
+ const peer = run.topology === void 0 ? local : `${run.topology.targetPeer}:${portOf(local)}`;
632
+ return placeOf(run, run.targetNetns, (record) => record.dst === peer);
633
+ };
634
+ /** This function gives the kernel place of a socket of a role at an instant. The role `server` is a socket of the target. */
635
+ const kernelOf = (run, role, at) => {
636
+ if (role === "server") {
637
+ const pool = socketAt(run.events, "pool", at);
638
+ return {
639
+ socket: pool,
640
+ place: pool === void 0 ? void 0 : peerPlace(run, pool.local)
641
+ };
642
+ }
643
+ const socket = socketAt(run.events, role, at);
644
+ return {
645
+ socket,
646
+ place: socket === void 0 ? void 0 : placeOf(run, run.netns, (record) => record.src === socket.local)
647
+ };
648
+ };
649
+ const newState$1 = (record) => record.new;
650
+ const oldState$1 = (record) => record.old;
651
+ /** 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. */
652
+ const outsetOf = (run) => {
653
+ const ready = run.events.find((event) => event.ev === "ready");
654
+ return run.fault?.t0 ?? ready?.t ?? run.events[0]?.t ?? 0;
655
+ };
656
+ /**
657
+ * This function gives the instant of a reference in a run, on a socket of the driver:
658
+ * - `last-received`: the last segment that the socket received before the fault held. A band of
659
+ * the kernel counts from the last segment of any kind. A band that a line ends is a timer of
660
+ * the driver. It counts from the last segment with data that the socket received or sent.
661
+ * - `request-sent`: the send of the operation into the fault, the second `sent` line.
662
+ * - `first-retransmission`: the first retransmission of the RTO timer on the socket.
663
+ * - `peer-closed`: the change of state of the socket of the target, when its process dies.
664
+ * - `fault`: the start of the commit of the fault. `client-start`: the `connecting` line.
665
+ */
666
+ const referenceOf = (run, from, role, data) => {
667
+ const ready = run.events.find((event) => event.ev === "ready");
668
+ const before = run.fault?.t1 ?? ready?.t ?? Infinity;
669
+ if (from === "fault") return run.fault?.t0;
670
+ if (from === "client-start") return (run.events.find((event) => event.ev === "connecting") ?? run.events[0])?.t;
671
+ if (from === "request-sent") return run.events.filter((event) => event.ev === "sent")[1]?.t;
672
+ const { socket, place } = kernelOf(run, role, before);
673
+ if (place === void 0) return void 0;
674
+ if (from === "last-received") {
675
+ const counts = (record) => data ? (record.ev === "rx" || record.ev === "tx") && (record.seg?.len.value ?? 0) > 0 : record.ev === "rx";
676
+ return place.records.filter((record) => record.mono <= before && counts(record)).at(-1)?.mono;
677
+ }
678
+ if (from === "first-retransmission") {
679
+ const request = run.events.filter((event) => event.ev === "sent")[1]?.t ?? before;
680
+ return place.records.find((record) => record.ev === "retrans" && record.why === "rto" && record.mono > request)?.mono;
681
+ }
682
+ return (socket === void 0 ? void 0 : peerPlace(run, socket.local))?.records.find((record) => record.ev === "state" && oldState$1(record) === 1)?.mono;
683
+ };
684
+ /** This function gives the first line of the client that matches, after an instant. */
685
+ const lineAfter = (events, match, after) => events.find((event) => event.t > after && matches(event, match));
686
+ /** This function gives the name of the error of a line: the field that the match names, or `name`. */
687
+ const nameOf = (line, match) => String(fieldOf(line, match.name ?? "name") ?? line.ev);
688
+ /** This function gives the report of the kernel on a socket after an instant: an error, or the end of the file (CLOSE_WAIT). */
689
+ const reportOf = (place, after) => {
690
+ const report = place.records.find((record) => record.mono > after && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && newState$1(record) === 8));
691
+ if (report === void 0) return void 0;
692
+ return report.ev === "error" ? {
693
+ at: report.mono,
694
+ outcome: {
695
+ kind: "error",
696
+ errno: ERRNO$1[report.err] ?? `E${report.err}`
697
+ }
698
+ } : {
699
+ at: report.mono,
700
+ outcome: { kind: "eof" }
701
+ };
702
+ };
703
+ /** This function gives the latest instant of the bands of a driver prediction, in seconds from their references. */
704
+ const longestOf = (cell) => {
705
+ const { prediction } = cell;
706
+ if (prediction.detects === null) return valueIn(prediction.window, "s");
707
+ const steps = (prediction.driver?.then ?? []).map((step) => valueIn(step.high, "s"));
708
+ const own = prediction.detects ? valueIn(prediction.high, "s") : valueIn(prediction.window, "s");
709
+ return Math.max(own, ...steps);
710
+ };
711
+ /**
712
+ * This function tells if a run of a driver cell has its outcome, so that its wait can stop. For a
713
+ * detection, the outcome is the end of its band and of each later band. A silent prediction waits
714
+ * for its window.
715
+ */
716
+ const settled = (cell, run) => {
717
+ const { prediction } = cell;
718
+ if (prediction.detects !== true || prediction.driver === void 0) return false;
719
+ const driver = prediction.driver;
720
+ const after = outsetOf(run);
721
+ return (driver.ends === void 0 ? (() => {
722
+ const { place } = kernelOf(run, driver.socket, run.fault?.t1 ?? after);
723
+ return place !== void 0 && reportOf(place, after) !== void 0;
724
+ })() : lineAfter(run.events, driver.ends, after) !== void 0) && (driver.then ?? []).every((step) => lineAfter(run.events, step.ends, after) !== void 0);
725
+ };
726
+ /**
727
+ * This function examines the keepalive timer of a socket between two instants. A firing two
728
+ * jiffies or more behind its wheel slot was delayed by the host, which stopped a vCPU. A firing
729
+ * that found the socket held by its owner put itself off by HZ/20. Either way, the run measured
730
+ * something other than the timer.
731
+ */
732
+ const delayed = (place, from, to) => {
733
+ const firings = place.records.filter((record) => record.ev === "timer" && record.what === "keepalive" && record.mono >= from && record.mono <= to);
734
+ const late = firings.find((record) => (record.behind?.value ?? 0) >= 2);
735
+ if (late !== void 0) return `its keepalive timer ran ${late.behind.value} jiffies behind its wheel slot: the VM delayed it, not the kernel`;
736
+ 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";
737
+ };
738
+ /**
739
+ * This function reads one run of a driver cell.
740
+ * - The band of the cell counts from its reference on its socket. It ends at its line, or at the
741
+ * report of the kernel on that socket.
742
+ * - Each later band (`then`) gets the same test. A later band outside its band, or absent, is a finding.
743
+ * - A line that the prediction names as quiet, after the fault and in its window, is a finding.
744
+ * So is a flow of an open socket of the driver that a router forgot, for a silent prediction.
745
+ * - A silent prediction can time a reply (`reply`), and the verdict tests it against `replies`.
746
+ *
747
+ * A run is void when the prober lost records, or when the client did not get its first reply. It
748
+ * is also void when the operation went out before the fault held. A late lift of a hold and a
749
+ * keepalive probe before the fault also make it void. So do a delayed timer and a round trip
750
+ * above the assumption.
751
+ */
752
+ const measureDriver = (cell, run) => {
753
+ const { prediction } = cell;
754
+ if (prediction.detects === null || prediction.driver === void 0) throw new Error(`${cell.name} has no driver band`);
755
+ const driver = prediction.driver;
756
+ const { events } = run;
757
+ const ready = events.find((event) => event.ev === "ready");
758
+ const request = events.filter((event) => event.ev === "sent")[1];
759
+ const after = outsetOf(run);
760
+ const before = run.fault?.t1 ?? ready?.t ?? after;
761
+ const { place } = kernelOf(run, driver.socket, before);
762
+ const records = place?.records ?? [];
763
+ const srtt = kernelOf(run, "pool", before).place?.records.find((record) => record.tcp?.srtt !== void 0)?.tcp?.srtt;
764
+ const rtt = srtt === void 0 ? null : quantity(valueIn(srtt, "us") / 1e3, "ms");
765
+ const base = {
766
+ environment: run.environment,
767
+ index: run.index,
768
+ probes: records.filter((record) => record.ev === "probe" && record.what === "keepalive").length,
769
+ retransmissions: records.filter((record) => record.ev === "retrans").length,
770
+ acks: request === void 0 ? 0 : records.filter((record) => record.ev === "rx" && record.mono > request.t).length,
771
+ rtt
772
+ };
773
+ const nothing = {
774
+ outcome: { kind: "silent" },
775
+ elapsed: null,
776
+ socket: null,
777
+ noticed: null,
778
+ steps: [],
779
+ finding: null
780
+ };
781
+ if (cell.before !== true && ready === void 0) return {
782
+ ...base,
783
+ ...nothing,
784
+ void: "the client never had its reply"
785
+ };
786
+ if (run.lost === null) return {
787
+ ...base,
788
+ ...nothing,
789
+ void: "the prober wrote no clock line after the run, so nothing says what it dropped"
790
+ };
791
+ if (run.lost > 0) return {
792
+ ...base,
793
+ ...nothing,
794
+ void: `the prober dropped ${run.lost} records`
795
+ };
796
+ let voided = null;
797
+ if (cell.probe.mode !== "idle" && cell.before !== true) {
798
+ if (request === void 0) voided = "the operation was never sent into the fault";
799
+ else if (run.fault !== null && run.fault.t1 > request.t) voided = "the operation went out before the fault held";
800
+ }
801
+ const keepalive = records.find((record) => record.ev === "probe" && record.what === "keepalive");
802
+ if (voided === null && run.fault !== null && keepalive !== void 0 && keepalive.mono < run.fault.t1) voided = "a keepalive probe went out before the fault held";
803
+ if (voided === null && cell.hold !== void 0 && run.fault !== null) {
804
+ const heal = run.heal ?? null;
805
+ if (heal === null) voided = "the fault never lifted";
806
+ else {
807
+ const late = (heal.t0 - run.fault.t0) / 1e9 - valueIn(cell.hold, "s");
808
+ 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`;
809
+ }
810
+ }
811
+ const assumed = prediction.rtt;
812
+ 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`;
813
+ const findings = [];
814
+ const noise = valueIn(NOISE, "s");
815
+ const until = prediction.detects ? Infinity : after + valueIn(prediction.window, "s") * 1e9;
816
+ for (const match of driver.quiet ?? []) {
817
+ const line = events.find((event) => event.t > after && event.t <= until && matches(event, match));
818
+ if (line !== void 0) findings.push(`${wordsOf(match)} came ${between$2(after, line.t).value} s after the fault, where none should`);
819
+ }
820
+ if (!prediction.detects && run.middlebox !== void 0) for (const socket of socketsOf(events)) {
821
+ 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);
822
+ 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`);
823
+ }
824
+ const stepTimes = (driver.then ?? []).map((step) => {
825
+ const line = lineAfter(events, step.ends, after);
826
+ const from = referenceOf(run, step.from, step.socket ?? driver.socket, true);
827
+ if (line === void 0 || from === void 0) {
828
+ findings.push(line === void 0 ? `${step.what}: ${wordsOf(step.ends)} never came` : `${step.what}: its reference (${step.from}) never came`);
829
+ return {
830
+ what: step.what,
831
+ name: line === void 0 ? null : nameOf(line, step.ends),
832
+ elapsed: null
833
+ };
834
+ }
835
+ const elapsed = between$2(from, line.t);
836
+ const name = nameOf(line, step.ends);
837
+ if (step.name !== "OK" && name !== step.name) findings.push(`${step.what}: it ended with ${name}, not ${step.name}`);
838
+ const [low, high] = [round(valueIn(step.low, "s") - noise), round(valueIn(step.high, "s") + noise)];
839
+ if (elapsed.value < low || elapsed.value > high) findings.push(`${step.what}: ${elapsed.value} s is outside [${low}, ${high}] s`);
840
+ return {
841
+ what: step.what,
842
+ name,
843
+ elapsed
844
+ };
845
+ });
846
+ const stepped = stepTimes.length === 0 ? {} : { driver: { steps: stepTimes } };
847
+ if (!prediction.detects) {
848
+ if (driver.reply === void 0 || prediction.replies === void 0) return {
849
+ ...base,
850
+ ...nothing,
851
+ ...stepped,
852
+ void: voided,
853
+ finding: findings[0] ?? null
854
+ };
855
+ const line = lineAfter(events, driver.reply.ends, after);
856
+ const from = referenceOf(run, driver.reply.from, driver.socket, true);
857
+ if (line === void 0 || from === void 0) return {
858
+ ...base,
859
+ ...nothing,
860
+ ...stepped,
861
+ void: voided,
862
+ finding: findings[0] ?? `the reply never came: ${wordsOf(driver.reply.ends)}`
863
+ };
864
+ return {
865
+ ...base,
866
+ ...nothing,
867
+ ...stepped,
868
+ outcome: { kind: "answered" },
869
+ elapsed: between$2(from, line.t),
870
+ void: voided,
871
+ finding: findings[0] ?? null
872
+ };
873
+ }
874
+ const line = driver.ends === void 0 ? void 0 : lineAfter(events, driver.ends, after);
875
+ const report = driver.ends === void 0 && place !== void 0 ? reportOf(place, after) : void 0;
876
+ if (driver.ends === void 0 && place === void 0) return {
877
+ ...base,
878
+ ...nothing,
879
+ ...stepped,
880
+ void: voided ?? `the prober saw nothing of the ${driver.socket} socket`,
881
+ finding: findings[0] ?? null
882
+ };
883
+ const ended = line?.t ?? report?.at;
884
+ const outcome = line !== void 0 && driver.ends !== void 0 ? {
885
+ kind: "error",
886
+ errno: nameOf(line, driver.ends)
887
+ } : report?.outcome ?? { kind: "silent" };
888
+ if (ended === void 0) return {
889
+ ...base,
890
+ ...nothing,
891
+ ...stepped,
892
+ void: voided,
893
+ finding: findings[0] ?? `nothing detected it: ${prediction.timer} never fired`
894
+ };
895
+ const reference = referenceOf(run, prediction.from, driver.socket, driver.ends !== void 0);
896
+ if (reference === void 0) return {
897
+ ...base,
898
+ ...nothing,
899
+ ...stepped,
900
+ outcome,
901
+ void: voided,
902
+ finding: `it ended without its reference event (${prediction.from} on the ${driver.socket} socket)`
903
+ };
904
+ if (driver.ends === void 0 && prediction.from === "last-received" && place !== void 0) voided ??= delayed(place, reference, ended);
905
+ const name = outcome.kind === "error" ? outcome.errno : outcome.kind === "eof" ? "EOF" : "silent";
906
+ if (prediction.errno !== "OK" && name !== prediction.errno) findings.unshift(`it ended with ${name}, not ${prediction.errno}`);
907
+ const seen = report === void 0 ? void 0 : events.find((event) => event.t >= report.at && (event.ev === "error" || event.ev === "socket-error"));
908
+ return {
909
+ ...base,
910
+ ...stepped,
911
+ outcome,
912
+ elapsed: between$2(reference, ended),
913
+ socket: seen === void 0 ? null : between$2(reference, seen.t),
914
+ noticed: report === void 0 || seen === void 0 ? null : between$2(report.at, seen.t),
915
+ steps: (prediction.from === "last-received" ? records.filter((record) => record.ev === "probe" && record.what === "keepalive" && record.mono > reference) : []).map((record) => between$2(reference, record.mono)),
916
+ void: voided,
917
+ finding: findings[0] ?? null
918
+ };
919
+ };
920
+ //#endregion
385
921
  //#region src/timeline.ts
386
922
  /**
387
923
  * 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 as its Sentinels announced it. It also gives what
924
+ * after the fault, and the timeline of the group. The Sentinels of a group announce its timeline.
925
+ * The logs of the members of a replica set and its client tell it. The module also gives what
389
926
  * became of each write.
390
927
  */
391
- /** These are the stages of a timeline, in their sequence after a fault. */
928
+ /** These are the stages of a timeline under Sentinels, in their sequence after a fault. */
392
929
  const STAGES = [
393
930
  "+sdown",
394
931
  "+odown",
@@ -418,6 +955,105 @@ const stagesOf = (timeline, from) => {
418
955
  }
419
956
  return out;
420
957
  };
958
+ /** These are the stages of the timeline of a replica set, in their usual sequence after a fault. */
959
+ const REPLICA_SET_STAGES = [
960
+ "member-down",
961
+ "election-start",
962
+ "elected",
963
+ "caught-up",
964
+ "writable",
965
+ "old-primary-steps-down",
966
+ "discovered",
967
+ "written-off",
968
+ "first-write"
969
+ ];
970
+ /** These are the log lines of mongod that mark each stage of a member. */
971
+ const MEMBER_MARKS = {
972
+ "member-down": {
973
+ ids: [21216],
974
+ message: /now in state DOWN/,
975
+ at: "other"
976
+ },
977
+ "election-start": {
978
+ ids: [4615652, 21437],
979
+ message: /^Starting an election|Skipping dry run|Conducting a dry run/,
980
+ at: "other"
981
+ },
982
+ elected: {
983
+ ids: [21450],
984
+ message: /^Election succeeded/,
985
+ at: "other"
986
+ },
987
+ "caught-up": {
988
+ ids: [21363],
989
+ message: /^Exited primary catch-up mode/,
990
+ at: "other"
991
+ },
992
+ writable: {
993
+ ids: [21331],
994
+ message: /^Transition to primary complete/,
995
+ at: "other"
996
+ },
997
+ "old-primary-steps-down": {
998
+ ids: [
999
+ 21809,
1000
+ 21475,
1001
+ 962660
1002
+ ],
1003
+ message: /relinquishing primary|Stepping down from primary/,
1004
+ at: "old"
1005
+ }
1006
+ };
1007
+ /** This function tells if a log line of a member marks a stage, with the old primary of the run. */
1008
+ const marks = (line, stage, old, oldAddress) => {
1009
+ const mark = MEMBER_MARKS[stage];
1010
+ const member = String(line["member"] ?? "");
1011
+ if (mark.at === "old" !== (member === old)) return false;
1012
+ const attr = line["attr"] ?? {};
1013
+ if (stage === "member-down" && !JSON.stringify(attr).includes(oldAddress)) return false;
1014
+ if (stage === "old-primary-steps-down" && Number(line["id"]) === 21358 && attr["oldState"] === "PRIMARY") return true;
1015
+ return mark.ids.includes(Number(line["id"])) || mark.message.test(String(line["msg"] ?? ""));
1016
+ };
1017
+ /**
1018
+ * This function gives the time of each stage of a replica set from an instant, in a run. The stages
1019
+ * of the members come from their log lines (`ev` "mongod"), whose times the lab puts on the
1020
+ * monotonic clock. The stages of the client come from its lines:
1021
+ * - `discovered`: a member other than the old primary becomes RSPrimary in the view of the client.
1022
+ * - `written-off`: the first heartbeat of the client to the old primary that fails.
1023
+ * - `first-write`: the first write that a primary accepts, of a write that the client sent after
1024
+ * the commit of the fault (`sentAfter`). It is on a new primary when `lands` is `new`.
1025
+ */
1026
+ const replicaSetStagesOf = (run, from, sentAfter, lands) => {
1027
+ const out = {};
1028
+ const old = run.group?.primary ?? "";
1029
+ const oldAddress = `${run.group?.members[old]?.ip ?? ""}:`;
1030
+ for (const line of run.group?.timeline ?? []) {
1031
+ if (line.ev !== "mongod" || line.t < from) continue;
1032
+ 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);
1033
+ }
1034
+ const isOld = (address) => typeof address === "string" && address.startsWith(oldAddress);
1035
+ for (const event of run.events) {
1036
+ if (event.t < from) continue;
1037
+ if (out.discovered === void 0 && event.ev === "sdam" && event["event"] === "server-changed" && !isOld(event["address"])) {
1038
+ if (event["next"]?.type === "RSPrimary") out.discovered = between$1(from, event.t);
1039
+ }
1040
+ if (out["written-off"] === void 0 && event.ev === "sdam" && event["event"] === "heartbeat-failed" && isOld(event["address"])) out["written-off"] = between$1(from, event.t);
1041
+ }
1042
+ const accepted = acceptedAfter(run, sentAfter, lands);
1043
+ if (accepted !== void 0) out["first-write"] = between$1(from, accepted.t);
1044
+ return out;
1045
+ };
1046
+ /** 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`. */
1047
+ const acceptedAfter = (run, sentAfter, lands) => {
1048
+ const sent = /* @__PURE__ */ new Map();
1049
+ for (const event of run.events) if (event.ev === "sent") sent.set(Number(event["n"]), event.t);
1050
+ const primary = run.group?.primary ?? "";
1051
+ return run.events.find((event) => {
1052
+ if (event.ev !== "ack" || (sent.get(Number(event["n"])) ?? -Infinity) <= sentAfter) return false;
1053
+ const member = memberAt(run, event["at"]);
1054
+ return member !== null && (lands === "new" ? member !== primary : true);
1055
+ });
1056
+ };
421
1057
  /** 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
1058
  const writesOf = (run, after, primary) => {
423
1059
  const { events } = run;
@@ -490,7 +1126,10 @@ const lagsOf = (run, kernel) => {
490
1126
  * - The reference is the start of the commit of the fault, as the injector stamped it. For a
491
1127
  * stall, it is the first write into the fault, as for the stall of a direct pair.
492
1128
  * - 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.
1129
+ * which primary: a new one, or the same one after the lift. In a replica set, it is the first
1130
+ * accepted write of a write that the client sent after the commit of the fault.
1131
+ * - The stages come from the Sentinels, or from the logs of the members of a replica set and the
1132
+ * lines of its client (`replicaSetStagesOf`).
494
1133
  * - Its time is the last segment with data that the client's socket to that primary received
495
1134
  * before the client saw the acceptance. The stamp of the client is the fallback.
496
1135
  * - A run is void when the prober lost records, or when the client did not become ready. It is
@@ -546,16 +1185,25 @@ const measureGroup = (cell, run) => {
546
1185
  group: empty
547
1186
  };
548
1187
  const timeline = run.group?.timeline ?? [];
549
- const stages = stagesOf(timeline, t0);
550
1188
  const heal = run.heal ?? null;
551
1189
  const lands = expected?.lands ?? "new";
552
- const after = lands === "same" && heal !== null ? heal.t0 : t0;
1190
+ const replica = cell.replicaSet !== void 0;
1191
+ const stall = prediction.detects === false && prediction.recovers !== void 0;
1192
+ const stages = replica ? replicaSetStagesOf(run, t0, run.fault.t1, lands) : stagesOf(timeline, t0);
1193
+ const after = lands === "same" && heal !== null && (!replica || stall) ? heal.t0 : t0;
1194
+ const sentAt = /* @__PURE__ */ new Map();
1195
+ for (const event of events) if (event.ev === "sent") sentAt.set(Number(event["n"]), event.t);
553
1196
  const accepted = events.find((event) => {
554
1197
  if (event.ev !== "ack" || event.t <= after) return false;
1198
+ if (replica && (sentAt.get(Number(event["n"])) ?? -Infinity) <= run.fault.t1) return false;
555
1199
  const member = memberAt(run, event["at"]);
556
1200
  return member !== null && (lands === "new" ? member !== primary : true);
557
1201
  });
558
1202
  const to = accepted === void 0 ? null : memberAt(run, accepted["at"]);
1203
+ if (replica) {
1204
+ delete stages["first-write"];
1205
+ if (accepted !== void 0) stages["first-write"] = between$1(t0, accepted.t);
1206
+ }
559
1207
  const group = {
560
1208
  from: primary,
561
1209
  to,
@@ -580,7 +1228,7 @@ const measureGroup = (cell, run) => {
580
1228
  }
581
1229
  }
582
1230
  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);
1231
+ 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
1232
  const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
585
1233
  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
1234
  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 +1244,12 @@ const measureGroup = (cell, run) => {
596
1244
  }
597
1245
  }
598
1246
  const findings = [];
599
- const switched = stages["+switch-master"];
1247
+ const switched = replica ? stages["elected"] : stages["+switch-master"];
600
1248
  if (expected?.failover === false && switched !== void 0) findings.push(`the group failed over ${switched.value} s after the fault, where it should not`);
1249
+ for (const stage of expected?.quiet ?? []) {
1250
+ const at = stages[stage];
1251
+ if (at !== void 0) findings.push(`${stage} came ${at.value} s after the fault, where it should not`);
1252
+ }
601
1253
  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
1254
  for (const error of expected?.errors ?? []) {
603
1255
  const met = group.errors.find((each) => each.name === error.name);
@@ -615,7 +1267,8 @@ const measureGroup = (cell, run) => {
615
1267
  };
616
1268
  }
617
1269
  const address = String(accepted["at"]);
618
- const segment = kernel.filter((record) => record.netns === run.netns && record.ev === "rx" && record.dst === address && (record.seg?.len.value ?? 0) > 0 && record.mono <= accepted.t && record.mono > after).at(-1);
1270
+ const others = new Set(socketsOf(events).filter((socket) => socket.role !== "pool").map((socket) => socket.local));
1271
+ 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
1272
  const at = segment?.mono ?? accepted.t;
620
1273
  return {
621
1274
  outcome: { kind: "answered" },
@@ -647,9 +1300,10 @@ const newState = (record) => record.new;
647
1300
  const count = (event, field) => event === void 0 ? 0 : Number(event[field] ?? 0);
648
1301
  /** This function gives the view of the client on the same interval: TCP_INFO, with a sample each 2 ms. */
649
1302
  const socketVantage = (cell, events, ready, request) => {
650
- if (!cell.prediction.detects || cell.prediction.from === "peer-closed" || cell.prediction.from === "fault") return null;
1303
+ const { from } = cell.prediction.detects === true ? cell.prediction : { from: void 0 };
1304
+ if (from === void 0 || from === "peer-closed" || from === "fault" || from === "ping-sent") return null;
651
1305
  const ended = events.find((event) => event.ev === "error" || event.ev === "eof");
652
- const reference = cell.prediction.from === "last-received" ? ready : cell.prediction.from === "request-sent" ? request : events.find((event) => event.ev === "kernel" && event.t >= request.t && count(event, "retransmits") > 0);
1306
+ const reference = from === "last-received" ? ready : from === "request-sent" ? request : events.find((event) => event.ev === "kernel" && event.t >= request.t && count(event, "retransmits") > 0);
653
1307
  return ended === void 0 || reference === void 0 ? null : between(reference.t, ended.t);
654
1308
  };
655
1309
  const ipOf = (address) => address.split(":")[0];
@@ -835,7 +1489,7 @@ const requestOf = (events) => {
835
1489
  };
836
1490
  /** This function reads the socket of one run against its cell, as `measure` does without the second request of an HTTP client. */
837
1491
  const measureSocket = (cell, run) => {
838
- if (cell.group !== void 0) {
1492
+ if (cell.group !== void 0 || cell.replicaSet !== void 0) {
839
1493
  const read = measureGroup(cell, run);
840
1494
  return {
841
1495
  environment: run.environment,
@@ -853,6 +1507,7 @@ const measureSocket = (cell, run) => {
853
1507
  group: read.group
854
1508
  };
855
1509
  }
1510
+ if (cell.prediction.detects !== null && cell.prediction.driver !== void 0) return measureDriver(cell, run);
856
1511
  const { events, kernel } = run;
857
1512
  const ready = events.find((event) => event.ev === "ready");
858
1513
  const local = events.find((event) => event.ev === "connected")?.["local"];
@@ -946,20 +1601,28 @@ const measureSocket = (cell, run) => {
946
1601
  const seen = remote ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
947
1602
  const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8);
948
1603
  const own = cell.prediction.detects === true && cell.prediction.errno === "CLOSED" ? socket.find((record) => record.ev === "state" && oldState(record) === 1 && newState(record) === 4) : void 0;
949
- const outcome = reported !== void 0 ? {
1604
+ const lines = programLines(cell, run);
1605
+ const kernelEnded = reported?.mono ?? closeWait?.mono ?? own?.mono ?? seen?.t;
1606
+ const first = dropOf(lines, socket, local);
1607
+ const drop = first !== void 0 && (kernelEnded === void 0 || first.at <= kernelEnded) ? first : void 0;
1608
+ const dropped = drop !== void 0;
1609
+ const outcome = dropped ? { kind: "dropped" } : reported !== void 0 ? {
950
1610
  kind: "error",
951
1611
  errno: ERRNO[reported.err] ?? `E${reported.err}`
952
1612
  } : closeWait !== void 0 || seen?.ev === "eof" ? { kind: "eof" } : own !== void 0 ? { kind: "closed" } : seen !== void 0 ? {
953
1613
  kind: "error",
954
1614
  errno: String(seen["errno"])
955
1615
  } : { kind: "silent" };
956
- const ended = reported?.mono ?? closeWait?.mono ?? own?.mono ?? seen?.t;
957
- const noticed = reported !== void 0 && seen !== void 0 ? between(reported.mono, seen.t) : null;
1616
+ const ended = drop?.at ?? kernelEnded;
1617
+ const noticed = reported !== void 0 && seen !== void 0 && !dropped ? between(reported.mono, seen.t) : null;
958
1618
  const { prediction } = cell;
1619
+ const pings = pingsOf(lines, socket, local);
959
1620
  const before = run.fault?.t1 ?? ready.t;
960
1621
  const referenceAt = (from) => {
961
1622
  if (from === "request-sent") return request.t;
962
1623
  if (from === "fault") return run.fault?.t0;
1624
+ if (from === "client-start") return (events.find((event) => event.ev === "connecting") ?? events[0])?.t;
1625
+ if (from === "ping-sent") return firstUnanswered(pings, socket, ended)?.at;
963
1626
  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
1627
  };
965
1628
  const lastRx = kernel.filter((each) => each.netns === run.netns && each.src === local && each.ev === "rx" && each.mono <= before).at(-1)?.mono ?? ready.t;
@@ -1008,21 +1671,22 @@ const measureSocket = (cell, run) => {
1008
1671
  }
1009
1672
  if (!prediction.detects && prediction.recovers !== void 0) {
1010
1673
  const { recovers } = prediction;
1674
+ const start = cell.probe.mode === "idle" ? lines.find((line) => line.ev === "ping" && line.t >= (run.fault?.t1 ?? 0)) ?? request : request;
1011
1675
  const heal = run.heal ?? null;
1012
1676
  if (voided === null && heal === null) voided = "the stall never ended";
1013
1677
  else if (voided === null && heal !== null) {
1014
- const from = between(request.t, heal.t0);
1015
- const to = between(request.t, heal.t1);
1678
+ const from = between(start.t, heal.t0);
1679
+ const to = between(start.t, heal.t1);
1016
1680
  if (valueIn(from, "s") <= valueIn(recovers.heal.low, "s") || valueIn(to, "s") >= valueIn(recovers.heal.high, "s")) voided = `the stall ended ${from.value}–${to.value} s after the send, outside the ${recovers.heal.low.value}–${recovers.heal.high.value} s its prediction assumed`;
1017
1681
  }
1018
- const steps = retransmitted.filter((record) => record.mono > request.t).map((record) => between(request.t, record.mono));
1682
+ const steps = retransmitted.filter((record) => record.mono > start.t).map((record) => between(start.t, record.mono));
1019
1683
  if (ended !== void 0) return {
1020
1684
  ...base,
1021
1685
  ...nothing,
1022
1686
  outcome,
1023
1687
  steps,
1024
1688
  void: voided,
1025
- finding: `detected after ${between(request.t, ended).value} s, where it should recover`
1689
+ finding: `detected after ${between(start.t, ended).value} s, where it should recover`
1026
1690
  };
1027
1691
  const answer = heal === null ? void 0 : socket.find((record) => record.ev === "rx" && record.mono > heal.t0);
1028
1692
  if (answer === void 0) return {
@@ -1032,13 +1696,13 @@ const measureSocket = (cell, run) => {
1032
1696
  void: voided,
1033
1697
  finding: "it never moved again: nothing came back after the stall ended"
1034
1698
  };
1035
- const reply = events.find((event) => event.ev === "data" && event.t > request.t);
1036
- voided ??= delayed(request.t, answer.mono, "retransmit");
1699
+ const reply = events.find((event) => event.ev === "data" && event.t > start.t);
1700
+ voided ??= delayed(start.t, answer.mono, "retransmit");
1037
1701
  return {
1038
1702
  ...base,
1039
1703
  outcome: { kind: "answered" },
1040
- elapsed: between(request.t, answer.mono),
1041
- socket: reply === void 0 ? null : between(request.t, reply.t),
1704
+ elapsed: between(start.t, answer.mono),
1705
+ socket: reply === void 0 ? null : between(start.t, reply.t),
1042
1706
  noticed: reply === void 0 ? null : between(answer.mono, reply.t),
1043
1707
  steps,
1044
1708
  void: voided,
@@ -1128,23 +1792,41 @@ const measureSocket = (cell, run) => {
1128
1792
  void: voided,
1129
1793
  finding: `it ended without its reference event (${prediction.from})`
1130
1794
  };
1131
- const steps = (prediction.from === "last-received" ? keepalives : prediction.from === "first-retransmission" ? retransmitted.filter((record) => record.why === "rto" && record.mono > reference) : []).map((record) => between(reference, record.mono));
1132
- const finding = (prediction.errno === "EOF" ? outcome.kind === "eof" : prediction.errno === "CLOSED" ? outcome.kind === "closed" : outcome.kind === "error" && outcome.errno === prediction.errno) ? null : `it ended with ${outcome.kind === "error" ? outcome.errno : outcome.kind === "closed" ? "its own close" : "end of file"}, not ${prediction.errno}`;
1795
+ const steps = prediction.from === "ping-sent" ? pings.filter((ping) => ping.at > reference && ping.at <= ended).map((ping) => between(reference, ping.at)) : (prediction.from === "last-received" ? keepalives : prediction.from === "first-retransmission" ? retransmitted.filter((record) => record.why === "rto" && record.mono > reference) : []).map((record) => between(reference, record.mono));
1796
+ const matches = prediction.errno === "EOF" ? outcome.kind === "eof" : prediction.errno === "CLOSED" ? outcome.kind === "closed" : prediction.errno === "DROPPED" ? outcome.kind === "dropped" : outcome.kind === "error" && outcome.errno === prediction.errno;
1797
+ const words = outcome.kind === "error" ? outcome.errno : outcome.kind === "closed" ? "its own close" : outcome.kind === "dropped" ? "a drop by its own program" : "end of file";
1798
+ const reconnect = prediction.reconnects === void 0 ? void 0 : reconnectOf(run, ended, cookie, drop?.line.t ?? ended);
1799
+ const late = reconnect === void 0 || prediction.reconnects === void 0 ? null : reconnectFinding(prediction.reconnects, reconnect);
1800
+ const left = prediction.drop === void 0 || drop === void 0 ? void 0 : leftAfter(socket, drop.line);
1801
+ const how = prediction.drop === void 0 || drop === void 0 || left === void 0 ? null : dropFinding(prediction.drop, drop, left);
1802
+ const found = [
1803
+ matches ? null : `it ended with ${words}, not ${prediction.errno}`,
1804
+ late,
1805
+ how
1806
+ ].filter((each) => each !== null);
1807
+ const finding = found.length === 0 ? null : found.join(". ");
1133
1808
  const programTimer = cell.probe.http !== void 0 && prediction.from === "last-received" && prediction.timer !== "keepalive";
1134
1809
  if (prediction.from === "last-received" && !programTimer || prediction.from === "first-retransmission") voided ??= delayed(reference, ended, prediction.from === "last-received" ? "keepalive" : "retransmit");
1810
+ if (prediction.from === "ping-sent") voided ??= delayed(reference, ended, "retransmit");
1135
1811
  const lag = (valueIn(prediction.high, "s") - valueIn(prediction.low, "s")) * 1e9;
1136
1812
  const slow = prediction.timer.startsWith("expire_") ? (run.middlebox ?? []).find((line) => line.ev === "pump-slow" && line.t <= ended && line.t + Number(line["took"]) * 1e9 >= ended - lag) : void 0;
1137
1813
  if (slow !== void 0) voided ??= `a listing of the router took ${Math.round(Number(slow["took"]) * 1e4) / 10} ms, longer than the bound of its prediction`;
1138
1814
  const commit = run.fault === null ? 0 : (run.fault.t1 - run.fault.t0) / 1e6;
1139
1815
  if (prediction.from === "fault" && commit > valueIn(NOISE, "ms")) voided ??= `the commit of the fault took ${Math.round(commit * 10) / 10} ms, longer than the noise of the pipeline: its start is not known`;
1816
+ const unanswered = prediction.from === "ping-sent" ? firstUnanswered(pings, socket, ended) : void 0;
1817
+ const endLine = drop?.line ?? seen;
1818
+ const view = unanswered !== void 0 ? endLine === void 0 ? null : between(unanswered.line.t, endLine.t) : remote ? null : socketVantage(cell, events, ready, request);
1140
1819
  return {
1141
1820
  ...base,
1142
1821
  ...chain,
1143
1822
  outcome,
1144
1823
  elapsed: between(reference, ended),
1145
- socket: remote ? null : socketVantage(cell, events, ready, request),
1824
+ socket: view,
1146
1825
  noticed,
1147
1826
  steps,
1827
+ ...reconnect === void 0 ? {} : { reconnect },
1828
+ ...left === void 0 ? {} : { left },
1829
+ ...cell.probe.ws !== void 0 && inflight ? { call: callOf(events, request, ended) } : {},
1148
1830
  void: voided,
1149
1831
  finding
1150
1832
  };
@@ -1200,6 +1882,18 @@ const NODE_HTTP = {
1200
1882
  dockerfile: "harness/image/node-http/Dockerfile",
1201
1883
  from: ["harness/image/node-http"]
1202
1884
  };
1885
+ /** This image holds the MongoDB client: the driver mongodb 7.7.0 on Node 24.21.0, and its step-down script. */
1886
+ const MONGO = {
1887
+ name: "loom-harness-mongo",
1888
+ dockerfile: "harness/image/mongo/Dockerfile",
1889
+ from: ["harness/image/mongo"]
1890
+ };
1891
+ /** This image holds `@procedures/client` 0.3.0 on Node 24.21.0: its WebSocket client as a client of the harness, and the script of its server. */
1892
+ const PROCEDURES = {
1893
+ name: "loom-harness-procedures",
1894
+ dockerfile: "harness/image/procedures/Dockerfile",
1895
+ from: ["harness/image/procedures"]
1896
+ };
1203
1897
  /** This image is the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow. */
1204
1898
  const ROUTER = {
1205
1899
  name: "loom-harness-router",
@@ -1270,6 +1964,12 @@ const routerImage = () => build(ROUTER);
1270
1964
  const goHttpImage = () => build(GO_HTTP);
1271
1965
  /** This function gives the image of the Node HTTP clients. */
1272
1966
  const nodeHttpImage = () => build(NODE_HTTP);
1967
+ /** This function gives the image of the MongoDB client. */
1968
+ const mongoClientImage = () => build(MONGO);
1969
+ /** This function gives the image of `@procedures/client`: its WebSocket client and its server. */
1970
+ const proceduresImage = () => build(PROCEDURES);
1971
+ /** This function gives the image that a target operates: its stock image, or an image of the harness (`Target.harness`). */
1972
+ const targetImageOf = (target) => target.harness === "procedures" ? proceduresImage() : Promise.resolve(target.image);
1273
1973
  /**
1274
1974
  * This time is the interval of the listing of a router, when its hop does not set one. A listing
1275
1975
  * evicts each expired flow that it walks past, and the DESTROY event of that flow follows.
@@ -1422,6 +2122,16 @@ const clientOf = async (kind, image) => {
1422
2122
  command: ["node", `/opt/harness/node-http/client-${kind === "undici" ? "undici" : "agent"}.mjs`],
1423
2123
  clientKind: kind
1424
2124
  };
2125
+ if (kind === "mongodb") return {
2126
+ clientImage: await mongoClientImage(),
2127
+ command: ["node", "/opt/harness/mongo/client-mongo.mjs"],
2128
+ clientKind: kind
2129
+ };
2130
+ if (kind === "procedures-ws") return {
2131
+ clientImage: await proceduresImage(),
2132
+ command: ["node", "/opt/harness/procedures/client-ws.mjs"],
2133
+ clientKind: kind
2134
+ };
1425
2135
  return {
1426
2136
  clientImage: image,
1427
2137
  command: ["/opt/harness/client"],
@@ -1482,7 +2192,8 @@ const openLab = async (target, kind = "tcp", topology) => {
1482
2192
  try {
1483
2193
  for (let at = 0; at <= hops.length; at += 1) networks.push(await new Network().start());
1484
2194
  const last = networks[hops.length];
1485
- let stock = new GenericContainer(target.image).withNetwork(last).withWaitStrategy(Wait.forLogMessage(target.ready));
2195
+ const targetImage = await targetImageOf(target);
2196
+ let stock = new GenericContainer(targetImage).withNetwork(last).withWaitStrategy(Wait.forLogMessage(target.ready));
1486
2197
  if (target.files !== void 0) stock = stock.withCopyContentToContainer(Object.entries(target.files).map(([path, content]) => ({
1487
2198
  content,
1488
2199
  target: path
@@ -1675,6 +2386,7 @@ const openLab = async (target, kind = "tcp", topology) => {
1675
2386
  };
1676
2387
  const paused = runtime.container.getById(server.getId());
1677
2388
  let targetAdmin;
2389
+ let signaller;
1678
2390
  const hopNamed = (name) => {
1679
2391
  const found = inChain.find((hop) => hop.hop.name === name);
1680
2392
  if (found === void 0) throw new Error(`the topology has no hop ${name}`);
@@ -1937,6 +2649,19 @@ const openLab = async (target, kind = "tcp", topology) => {
1937
2649
  },
1938
2650
  freeze: () => paused.pause().then(() => void 0),
1939
2651
  thaw: () => paused.unpause().then(() => void 0),
2652
+ async signal(signal, at) {
2653
+ if (target.program === void 0) throw new Error(`${target.image} names no server process to signal`);
2654
+ signaller ??= await withPid(new GenericContainer(image).withNetworkMode(`container:${server.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${server.getId()}`).start();
2655
+ if (!opened.includes(signaller)) opened.push(signaller);
2656
+ const when = at === void 0 ? [] : ["--at", String(Math.round(at))];
2657
+ return stamped(signaller, [
2658
+ "/opt/harness/inject",
2659
+ "signal",
2660
+ target.program,
2661
+ signal,
2662
+ ...when
2663
+ ]);
2664
+ },
1940
2665
  async probe(args) {
1941
2666
  const events = lines();
1942
2667
  const container = await new GenericContainer(clientImage).withNetworkMode(`container:${client.getId()}`).withCommand([...command, ...args]).withLogConsumer(events.consume).start();
@@ -2002,7 +2727,11 @@ const openLab = async (target, kind = "tcp", topology) => {
2002
2727
  boot_id: await output(client, ["cat", "/proc/sys/kernel/random/boot_id"]),
2003
2728
  docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
2004
2729
  target: {
2005
- image: target.image,
2730
+ image: targetImage,
2731
+ ...target.harness === void 0 ? {} : {
2732
+ harness: target.harness,
2733
+ base: target.image
2734
+ },
2006
2735
  id: (await inspect(server)).Image,
2007
2736
  settings,
2008
2737
  sysctls: targetSysctls,
@@ -2153,7 +2882,7 @@ const viewOf = (group, ips, sentinels, members) => {
2153
2882
  why: "healthy"
2154
2883
  };
2155
2884
  };
2156
- const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2885
+ const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2157
2886
  /** This time is the longest that a group takes to converge before a run, or after it. */
2158
2887
  const CONVERGE_MS = 24e4;
2159
2888
  /**
@@ -2348,7 +3077,7 @@ const openGroupLab = async (group, options = {}) => {
2348
3077
  }));
2349
3078
  calm = last.healthy ? calm + 1 : 0;
2350
3079
  if (calm >= 2 && last.primary !== null) return last.primary;
2351
- await sleep$1(1e3);
3080
+ await sleep$2(1e3);
2352
3081
  }
2353
3082
  throw new Error(`the group did not converge in ${within} ms: ${last.why}`);
2354
3083
  },
@@ -2426,7 +3155,7 @@ const openGroupLab = async (group, options = {}) => {
2426
3155
  const started = Date.now();
2427
3156
  while (await cli(member, ["PING"]).catch(() => "") !== "PONG") {
2428
3157
  if (Date.now() - started > 3e4) throw new Error(`${member} did not start again`);
2429
- await sleep$1(200);
3158
+ await sleep$2(200);
2430
3159
  }
2431
3160
  },
2432
3161
  failover(coordinated) {
@@ -2544,9 +3273,9 @@ const openGroupLab = async (group, options = {}) => {
2544
3273
  }
2545
3274
  };
2546
3275
  /** 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;
3276
+ const STEADY$1 = 20;
2548
3277
  /** This time is how long a run continues after its outcome, so that the record shows what came next. */
2549
- const TAIL_MS = 2e3;
3278
+ const TAIL_MS$2 = 2e3;
2550
3279
  /** This function gives the address of a member, with the port of the group. */
2551
3280
  const addressOf = (lab, member) => `${lab.members[member].ip}:${lab.group.port}`;
2552
3281
  /** This function gives the latest time that a group cell waits for its outcome, in ms from the fault. */
@@ -2569,7 +3298,7 @@ const clientArgs = (cell, lab) => {
2569
3298
  ];
2570
3299
  };
2571
3300
  /** 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) => {
3301
+ const waitFor$1 = (client, match, within) => new Promise((resolve) => {
2573
3302
  const started = Date.now();
2574
3303
  const look = () => {
2575
3304
  const found = client.events.find(match);
@@ -2585,7 +3314,7 @@ const subscriptionsOf = (timeline) => {
2585
3314
  return [...latest.values()];
2586
3315
  };
2587
3316
  /** This function gives the count of lost records in the latest clock line of the prober. */
2588
- const droppedSoFar$1 = (records) => {
3317
+ const droppedSoFar$2 = (records) => {
2589
3318
  for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
2590
3319
  return 0;
2591
3320
  };
@@ -2607,7 +3336,7 @@ const groupOnce = async (lab, cell, environment, index) => {
2607
3336
  await lab.forget();
2608
3337
  const from = lab.kernel.length;
2609
3338
  const timelineFrom = lab.timeline.length;
2610
- const lostBefore = droppedSoFar$1(lab.kernel);
3339
+ const lostBefore = droppedSoFar$2(lab.kernel);
2611
3340
  const client = await lab.client(clientArgs(cell, lab));
2612
3341
  const lands = cell.prediction.group?.lands ?? "new";
2613
3342
  const place = cell.faultAt ?? { role: "primary" };
@@ -2617,7 +3346,7 @@ const groupOnce = async (lab, cell, environment, index) => {
2617
3346
  const undo = [];
2618
3347
  try {
2619
3348
  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`);
3349
+ 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
3350
  let healing = null;
2622
3351
  switch (cell.fault) {
2623
3352
  case "blackhole":
@@ -2631,7 +3360,7 @@ const groupOnce = async (lab, cell, environment, index) => {
2631
3360
  undo.push(() => lab.lift(kind, part, cuts));
2632
3361
  if (cell.stall !== void 0) {
2633
3362
  const t1 = fault.t1;
2634
- const sent = await waitFor(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
3363
+ const sent = await waitFor$1(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
2635
3364
  if (sent !== void 0) healing = lab.lift(kind, part, cuts, sent.t + valueIn(cell.stall, "s") * 1e9);
2636
3365
  } else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
2637
3366
  break;
@@ -2665,11 +3394,11 @@ const groupOnce = async (lab, cell, environment, index) => {
2665
3394
  const t0 = fault.t0;
2666
3395
  const old = addressOf(lab, primary);
2667
3396
  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 () => {
3397
+ 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
3398
  await healed;
2670
3399
  const after = heal?.t0 ?? t0;
2671
- return waitFor(client, (event) => event.ev === "ack" && event.t > after, deadline);
2672
- })()) !== void 0) await sleep$1(TAIL_MS);
3400
+ return waitFor$1(client, (event) => event.ev === "ack" && event.t > after, deadline);
3401
+ })()) !== void 0) await sleep$2(TAIL_MS$2);
2673
3402
  await healed;
2674
3403
  } finally {
2675
3404
  for (const step of undo.reverse()) await step().catch(() => void 0);
@@ -2702,34 +3431,675 @@ const groupOnce = async (lab, cell, environment, index) => {
2702
3431
  };
2703
3432
  };
2704
3433
  //#endregion
2705
- //#region src/run.ts
3434
+ //#region src/replset.ts
3435
+ /** These are the components of the log of mongod that a run keeps: replication, its heartbeats, the elections, the rollbacks and the commands. */
3436
+ const KEPT = /* @__PURE__ */ new Set([
3437
+ "REPL",
3438
+ "REPL_HB",
3439
+ "ELECTION",
3440
+ "ROLLBACK",
3441
+ "COMMAND"
3442
+ ]);
3443
+ /** 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. */
3444
+ const memberLineOf = (member, text) => {
3445
+ if (!text.startsWith("{")) return null;
3446
+ let line;
3447
+ try {
3448
+ line = JSON.parse(text);
3449
+ } catch {
3450
+ return null;
3451
+ }
3452
+ const real = Date.parse(line.t?.$date ?? "");
3453
+ if (!Number.isFinite(real) || !KEPT.has(line.c ?? "")) return null;
3454
+ return {
3455
+ ev: "mongod",
3456
+ t: 0,
3457
+ member,
3458
+ real,
3459
+ id: Number(line.id ?? 0),
3460
+ c: line.c ?? "",
3461
+ msg: line.msg ?? "",
3462
+ attr: line.attr ?? {}
3463
+ };
3464
+ };
2706
3465
  /**
2707
- * This function measures and judges the raw records of a cell, with the code of the harness at
2708
- * this time. `runCell` uses it during a cell. A holder of the raw records of a cell can use it
2709
- * again later. Thus the records get a new analysis after a change to observe.ts or to the verdict.
3466
+ * This function gives the offset of the wall clock from the monotonic clock, in ns, from the clock
3467
+ * lines of a client. Each line pairs its stamp with the wall clock in whole ms, which truncates.
3468
+ * Thus each pair is the offset or less, and the largest pair is the nearest. It is null without a clock line.
2710
3469
  */
2711
- const judge = (cell, runs) => {
2712
- const measured = runs.filter((run) => run.warmup !== true).map((run) => measure(cell, run));
2713
- return {
2714
- measured,
2715
- verdict: verdictOf(cell.prediction, measured)
3470
+ const clockOffsetOf = (events) => {
3471
+ const offsets = events.filter((event) => event.ev === "clocks").map((event) => Number(event["real"]) * 1e6 - event.t);
3472
+ return offsets.length === 0 ? null : Math.max(...offsets);
3473
+ };
3474
+ /** This function puts the log lines of the members on the monotonic clock, with the offset of a client. */
3475
+ const onMonotonic = (log, offset) => log.map((line) => ({
3476
+ ...line,
3477
+ t: offset === null ? 0 : Math.round(line.real * 1e6 - offset)
3478
+ }));
3479
+ /**
3480
+ * This function gives the peers that a fault on a member of a replica set cuts it from. `all` is
3481
+ * each other member and the client, `group` is the other members, and `clients` is the client.
3482
+ * `one` is the first other member in the sequence of the set, and `one-way` is the same member for
3483
+ * the segments from the part only.
3484
+ */
3485
+ const cutOf = (set, members, client, part, cuts) => {
3486
+ const others = set.members.filter((name) => name !== part).map((name) => members[name].ip);
3487
+ if (cuts === "clients") return { peers: [client] };
3488
+ if (cuts === "group") return { peers: others };
3489
+ if (cuts === "one") return { peers: others.slice(0, 1) };
3490
+ if (cuts === "one-way") return {
3491
+ peers: others.slice(0, 1),
3492
+ direction: "out"
2716
3493
  };
3494
+ return { peers: [...others, client] };
2717
3495
  };
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;
3496
+ /** 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. */
3497
+ const memberCommand = (set) => `${set.programs.server} --replSet ${set.name} --bind_ip_all --port ${set.port} ${(set.args ?? []).join(" ")}`.trim();
3498
+ /** This function gives the script of the shell that starts the set: each member by its address, and the settings of the set. */
3499
+ const initiateScript = (set, ips) => {
3500
+ const config = {
3501
+ _id: set.name,
3502
+ members: set.members.map((name, at) => ({
3503
+ _id: at,
3504
+ host: `${ips[name]}:${set.port}`
3505
+ })),
3506
+ settings: set.settings
3507
+ };
3508
+ return `print(JSON.stringify(rs.initiate(${JSON.stringify(config)})))`;
2722
3509
  };
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,
3510
+ /** This script prints the view of a member: itself, if it accepts writes, and the state of each member as it sees them. */
3511
+ 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 })) }))";
3512
+ /**
3513
+ * This function judges the view of a member: the primary of the set, and if the set is healthy.
3514
+ * The set is healthy when the member is the one primary and accepts writes. Each other member must
3515
+ * be a healthy secondary at the optime of the primary.
3516
+ */
3517
+ const statusViewOf = (set, ips, status) => {
3518
+ const byAddress = new Map(set.members.map((name) => [`${ips[name]}:${set.port}`, name]));
3519
+ const primaries = status.members.filter((member) => member.state === "PRIMARY");
3520
+ if (primaries.length !== 1) return {
3521
+ primary: null,
3522
+ healthy: false,
3523
+ why: `the set has ${primaries.length} primaries`
3524
+ };
3525
+ const primary = byAddress.get(primaries[0].name) ?? null;
3526
+ if (primary === null) return {
3527
+ primary: null,
3528
+ healthy: false,
3529
+ why: `no member has the address ${primaries[0].name}`
3530
+ };
3531
+ if (status.me !== primaries[0].name || !status.writable) return {
3532
+ primary,
3533
+ healthy: false,
3534
+ why: `${primary} does not accept writes yet`
3535
+ };
3536
+ if (status.members.length !== set.members.length) return {
3537
+ primary,
3538
+ healthy: false,
3539
+ why: `the set has ${status.members.length} members`
3540
+ };
3541
+ for (const member of status.members) {
3542
+ if (member.health !== 1) return {
3543
+ primary,
3544
+ healthy: false,
3545
+ why: `${member.name} is not healthy`
3546
+ };
3547
+ if (member.state !== "PRIMARY" && member.state !== "SECONDARY") return {
3548
+ primary,
3549
+ healthy: false,
3550
+ why: `${member.name} is ${member.state}`
3551
+ };
3552
+ if (member.optime !== primaries[0].optime) return {
3553
+ primary,
3554
+ healthy: false,
3555
+ why: `${member.name} is behind the primary`
3556
+ };
3557
+ }
3558
+ return {
3559
+ primary,
3560
+ healthy: true,
3561
+ why: "healthy"
3562
+ };
3563
+ };
3564
+ const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3565
+ /** These are the arguments of replSetStepDown: the time that the old primary cannot stand again, and the wait for a secondary to catch up. */
3566
+ const STEP_DOWN = {
3567
+ seconds: 60,
3568
+ catchUp: 10
3569
+ };
3570
+ /**
3571
+ * This function opens one environment of a replica set cell:
3572
+ * - one new network, with each member, the client and the sender of a step-down on it.
3573
+ * - each member: the stock image, its server as the child of a shell. A log consumer reads its
3574
+ * JSON log. The first member starts the set with the address of each member, and the lab
3575
+ * waits for a primary.
3576
+ * - an injector sidecar for each member, in its network and process namespaces.
3577
+ * - a client that holds its own network namespace, with a privileged sidecar for its sysctls.
3578
+ * - the prober, which watches the namespace of the client.
3579
+ */
3580
+ const openReplicaSetLab = async (set) => {
3581
+ const image = await harnessImage();
3582
+ const mongo = await mongoClientImage();
3583
+ const { GenericContainer, Network, Wait, getContainerRuntimeClient } = await containers();
3584
+ const runtime = await getContainerRuntimeClient();
3585
+ const opened = [];
3586
+ let network;
3587
+ const close = async () => {
3588
+ for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
3589
+ await network?.stop().catch(() => void 0);
3590
+ };
3591
+ const ipOf = async (container) => {
3592
+ const ip = (await runtime.container.inspect(runtime.container.getById(container.getId()))).NetworkSettings.Networks[network.getName()]?.IPAddress;
3593
+ if (ip === void 0 || ip === "") throw new Error(`${container.getName()} has no address on ${network.getName()}`);
3594
+ return ip;
3595
+ };
3596
+ try {
3597
+ network = await new Network().start();
3598
+ const boxes = /* @__PURE__ */ new Map();
3599
+ const injectors = /* @__PURE__ */ new Map();
3600
+ const members = {};
3601
+ const log = [];
3602
+ for (const name of set.members) {
3603
+ let partial = "";
3604
+ const box = await new GenericContainer(set.image).withNetwork(network).withNetworkAliases(name).withCommand([
3605
+ "sh",
3606
+ "-c",
3607
+ `${memberCommand(set)} & echo $! > /tmp/server.pid; while :; do sleep 3600; done`
3608
+ ]).withWaitStrategy(Wait.forLogMessage(set.ready)).withLogConsumer((stream) => stream.on("data", (chunk) => {
3609
+ partial += chunk.toString();
3610
+ const split = partial.split("\n");
3611
+ partial = split.pop() ?? "";
3612
+ for (const text of split) {
3613
+ const line = memberLineOf(name, text.trim());
3614
+ if (line !== null) log.push(line);
3615
+ }
3616
+ })).start();
3617
+ opened.push(box);
3618
+ boxes.set(name, box);
3619
+ const injector = await withPid(new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${box.getId()}`).start();
3620
+ opened.push(injector);
3621
+ injectors.set(name, injector);
3622
+ members[name] = {
3623
+ ip: await ipOf(box),
3624
+ netns: await netnsOf(injector)
3625
+ };
3626
+ }
3627
+ const ips = Object.fromEntries(Object.entries(members).map(([name, at]) => [name, at.ip]));
3628
+ const holder = await new GenericContainer(image).withNetwork(network).start();
3629
+ opened.push(holder);
3630
+ const admin = await new GenericContainer(image).withNetworkMode(`container:${holder.getId()}`).withPrivilegedMode().start();
3631
+ opened.push(admin);
3632
+ const clientIp = await ipOf(holder);
3633
+ const commander = await new GenericContainer(mongo).withNetwork(network).withCommand([
3634
+ "sh",
3635
+ "-c",
3636
+ "while :; do sleep 3600; done"
3637
+ ]).start();
3638
+ opened.push(commander);
3639
+ const clientNetns = await netnsOf(admin);
3640
+ const records = lines();
3641
+ const prober = await new GenericContainer(image).withPrivilegedMode().withNetworkMode("none").withCommand(["/opt/harness/prober", String(clientNetns)]).withLogConsumer(records.consume).start();
3642
+ opened.push(prober);
3643
+ const begun = await records.until(["begin", "fail"], 3e4, "prober");
3644
+ if (begun.ev === "fail") throw new Error(`the prober failed: ${String(begun["why"])}`);
3645
+ const box = (name) => {
3646
+ const found = boxes.get(name);
3647
+ if (found === void 0) throw new Error(`the set has no member ${name}`);
3648
+ return found;
3649
+ };
3650
+ const injectorOf = (name) => {
3651
+ const found = injectors.get(name);
3652
+ if (found === void 0) throw new Error(`the set has no member ${name}`);
3653
+ return found;
3654
+ };
3655
+ /** This function starts a script of the shell on a member, against its own server only. */
3656
+ const shell = (name, script) => output(box(name), [
3657
+ set.programs.shell,
3658
+ "--quiet",
3659
+ "--norc",
3660
+ `mongodb://127.0.0.1:${set.port}/?directConnection=true&serverSelectionTimeoutMS=3000`,
3661
+ "--eval",
3662
+ script
3663
+ ]);
3664
+ const lastLine = (text) => text.split("\n").filter((line) => line.trim() !== "").at(-1) ?? "";
3665
+ const stamped = async (where, argv) => {
3666
+ const stamp = JSON.parse(lastLine(await output(where, argv)));
3667
+ return {
3668
+ t0: stamp.t0,
3669
+ t1: stamp.t1
3670
+ };
3671
+ };
3672
+ const after = (at) => at === void 0 ? [] : ["--at", String(Math.round(at))];
3673
+ let known = set.members[0];
3674
+ /** This function gives the view of the set: from the member that was the primary last, then from each other member. */
3675
+ const view = async () => {
3676
+ let last = {
3677
+ primary: null,
3678
+ healthy: false,
3679
+ why: "no member answered"
3680
+ };
3681
+ for (const name of [known, ...set.members.filter((each) => each !== known)]) {
3682
+ const read = await shell(name, STATUS_SCRIPT).then((text) => statusViewOf(set, ips, JSON.parse(lastLine(text)))).catch((error) => ({
3683
+ primary: null,
3684
+ healthy: false,
3685
+ why: `${name}: ${error.message.slice(0, 200)}`
3686
+ }));
3687
+ if (read.primary !== null && read.primary === name) return read;
3688
+ last = read;
3689
+ }
3690
+ return last;
3691
+ };
3692
+ const converge = async (within = CONVERGE_MS) => {
3693
+ const started = Date.now();
3694
+ let calm = 0;
3695
+ let last = {
3696
+ primary: null,
3697
+ healthy: false,
3698
+ why: "no view yet"
3699
+ };
3700
+ while (Date.now() - started < within) {
3701
+ last = await view();
3702
+ calm = last.healthy ? calm + 1 : 0;
3703
+ if (last.primary !== null) known = last.primary;
3704
+ if (calm >= 2 && last.primary !== null) return last.primary;
3705
+ await sleep$1(1e3);
3706
+ }
3707
+ throw new Error(`the set did not converge in ${within} ms: ${last.why}`);
3708
+ };
3709
+ const initiated = await shell(set.members[0], initiateScript(set, ips));
3710
+ if (!lastLine(initiated).includes("\"ok\":1")) throw new Error(`rs.initiate failed: ${lastLine(initiated)}`);
3711
+ await converge();
3712
+ return {
3713
+ set,
3714
+ clientIp,
3715
+ members,
3716
+ netns: { client: clientNetns },
3717
+ kernel: records.seen,
3718
+ log,
3719
+ async sysctl(values) {
3720
+ const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
3721
+ if (settings.length > 0) await output(admin, [
3722
+ "sysctl",
3723
+ "-q",
3724
+ "-w",
3725
+ ...settings
3726
+ ]);
3727
+ const read = {};
3728
+ for (const key of Object.keys(values)) read[key] = await output(admin, [
3729
+ "sysctl",
3730
+ "-n",
3731
+ key
3732
+ ]);
3733
+ return read;
3734
+ },
3735
+ converge,
3736
+ async flush(primary) {
3737
+ const script = `const r = db.getSiblingDB("cells").w.deleteMany({}, { writeConcern: { w: ${set.members.length}, wtimeout: 10000 } }); print(JSON.stringify({ ok: r.acknowledged === true }))`;
3738
+ const reply = lastLine(await shell(primary, script));
3739
+ if (!reply.includes("\"ok\":true")) throw new Error(`the flush failed: ${reply}`);
3740
+ },
3741
+ async unfreeze(primary) {
3742
+ for (const name of set.members) {
3743
+ if (name === primary) continue;
3744
+ const reply = lastLine(await shell(name, "print(JSON.stringify(rs.freeze(0)))"));
3745
+ if (!reply.includes("\"ok\":1")) throw new Error(`${name} did not unfreeze: ${reply}`);
3746
+ }
3747
+ },
3748
+ async holds(primary) {
3749
+ 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);
3750
+ },
3751
+ partOf(role, primary) {
3752
+ if (role === "primary") return primary;
3753
+ if (role === "sentinel") throw new Error("a replica set has no Sentinel");
3754
+ const secondary = set.members.find((name) => name !== primary);
3755
+ if (secondary === void 0) throw new Error("the set has no secondary");
3756
+ return secondary;
3757
+ },
3758
+ async client(args) {
3759
+ const events = lines();
3760
+ const container = await new GenericContainer(mongo).withNetworkMode(`container:${holder.getId()}`).withCommand([
3761
+ "node",
3762
+ "/opt/harness/mongo/client-mongo.mjs",
3763
+ ...args
3764
+ ]).withLogConsumer(events.consume).start();
3765
+ return {
3766
+ events: events.seen,
3767
+ until: (kinds, within) => events.until(kinds, within, "client"),
3768
+ stop: () => container.stop({ timeout: 2 }).then(() => void 0).catch(() => void 0)
3769
+ };
3770
+ },
3771
+ fault(kind, part, cuts, at) {
3772
+ const cut = cutOf(set, members, clientIp, part, cuts);
3773
+ const direction = cut.direction === void 0 ? [] : ["--direction", cut.direction];
3774
+ return stamped(injectorOf(part), [
3775
+ "/opt/harness/inject",
3776
+ kind,
3777
+ cut.peers.join(","),
3778
+ ...direction,
3779
+ ...after(at)
3780
+ ]);
3781
+ },
3782
+ lift(kind, part, cuts, at) {
3783
+ const cut = cutOf(set, members, clientIp, part, cuts);
3784
+ const direction = cut.direction === void 0 ? [] : ["--direction", cut.direction];
3785
+ return stamped(injectorOf(part), [
3786
+ "/opt/harness/inject",
3787
+ kind === "rst" ? "clear-rst" : "clear",
3788
+ cut.peers.join(","),
3789
+ ...direction,
3790
+ ...after(at)
3791
+ ]);
3792
+ },
3793
+ signal(part, signal, at) {
3794
+ return stamped(injectorOf(part), [
3795
+ "/opt/harness/inject",
3796
+ "signal",
3797
+ set.programs.server,
3798
+ signal,
3799
+ ...after(at)
3800
+ ]);
3801
+ },
3802
+ async restart(member) {
3803
+ await output(box(member), [
3804
+ "sh",
3805
+ "-c",
3806
+ `${memberCommand(set)} > /proc/1/fd/1 2>&1 & echo $! > /tmp/server.pid`
3807
+ ]);
3808
+ const started = Date.now();
3809
+ while (!(await shell(member, "print(db.hello().ok)").catch(() => "")).includes("1")) {
3810
+ if (Date.now() - started > 6e4) throw new Error(`${member} did not start again`);
3811
+ await sleep$1(500);
3812
+ }
3813
+ },
3814
+ stepDown(primary) {
3815
+ return stamped(commander, [
3816
+ "node",
3817
+ "/opt/harness/mongo/stepdown-mongo.mjs",
3818
+ `${members[primary].ip}:${set.port}`,
3819
+ String(STEP_DOWN.seconds),
3820
+ String(STEP_DOWN.catchUp)
3821
+ ]);
3822
+ },
3823
+ async forget() {
3824
+ await output(admin, [
3825
+ "ip",
3826
+ "tcp_metrics",
3827
+ "flush",
3828
+ "all"
3829
+ ]);
3830
+ },
3831
+ async settle() {
3832
+ const from = records.seen.length;
3833
+ const clock = await new Promise((resolve) => {
3834
+ const started = Date.now();
3835
+ const look = () => {
3836
+ const line = records.seen.slice(from).find((record) => record.ev === "clock");
3837
+ if (line !== void 0) resolve(line);
3838
+ else if (Date.now() - started > 5e3) resolve(null);
3839
+ else setTimeout(look, 50);
3840
+ };
3841
+ look();
3842
+ });
3843
+ return clock === null ? null : Number(clock["lost"] ?? 0);
3844
+ },
3845
+ async manifest(sysctls) {
3846
+ const config = await output(admin, ["zcat", "/proc/config.gz"]).catch(() => "");
3847
+ const inspect = async (container) => runtime.container.inspect(runtime.container.getById(container.getId()));
3848
+ const script = "print(JSON.stringify({ version: db.version(), settings: rs.conf().settings }))";
3849
+ const list = [];
3850
+ for (const name of set.members) {
3851
+ const read = JSON.parse(lastLine(await shell(name, script)));
3852
+ const settings = { version: read.version };
3853
+ for (const key of Object.keys(set.settings)) settings[key] = String(read.settings[key] ?? "");
3854
+ list.push({
3855
+ name,
3856
+ ip: members[name].ip,
3857
+ id: (await inspect(box(name))).Image,
3858
+ settings
3859
+ });
3860
+ }
3861
+ return {
3862
+ kernel: await output(holder, ["uname", "-r"]),
3863
+ architecture: await output(holder, ["uname", "-m"]),
3864
+ hz: /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1] ?? null,
3865
+ boot_id: await output(holder, ["cat", "/proc/sys/kernel/random/boot_id"]),
3866
+ docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
3867
+ target: {
3868
+ image: set.image,
3869
+ id: list[0].id,
3870
+ settings: list[0].settings,
3871
+ sysctls: {}
3872
+ },
3873
+ harness: {
3874
+ image,
3875
+ id: (await inspect(holder)).Image,
3876
+ packages: (await output(holder, [
3877
+ "apk",
3878
+ "info",
3879
+ "-v"
3880
+ ])).split("\n").sort()
3881
+ },
3882
+ client: {
3883
+ kind: "mongodb",
3884
+ image: mongo
3885
+ },
3886
+ netns: {
3887
+ client: clientNetns,
3888
+ target: members[set.members[0]].netns
3889
+ },
3890
+ sysctls,
3891
+ group: {
3892
+ members: list,
3893
+ sentinels: [],
3894
+ observer: { image: mongo }
3895
+ }
3896
+ };
3897
+ },
3898
+ close
3899
+ };
3900
+ } catch (error) {
3901
+ await close();
3902
+ throw error;
3903
+ }
3904
+ };
3905
+ /** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
3906
+ const STEADY = 20;
3907
+ /** This time is how long a run continues after its outcome, so that the record shows what came next. */
3908
+ const TAIL_MS$1 = 2e3;
3909
+ /** This function gives the arguments of the client of a replica set cell: each member, and the stream of writes. */
3910
+ const replicaSetArgs = (cell, members) => {
3911
+ const set = cell.replicaSet;
3912
+ const hosts = set.members.map((name) => `${members[name].ip}:${set.port}`);
3913
+ return [
3914
+ members[set.members[0]].ip,
3915
+ String(set.port),
3916
+ "--mode",
3917
+ "stream",
3918
+ "--hosts",
3919
+ hosts.join(","),
3920
+ "--set",
3921
+ set.name,
3922
+ "--interval-ms",
3923
+ String(cell.probe.interval === void 0 ? 50 : valueIn(cell.probe.interval, "ms")),
3924
+ "--max-s",
3925
+ String(Math.ceil(deadlineOf(cell) / 1e3 + (cell.hold === void 0 ? 0 : valueIn(cell.hold, "s")) + 120)),
3926
+ "--w",
3927
+ String(cell.writeConcern ?? "majority"),
3928
+ ...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)]
3929
+ ];
3930
+ };
3931
+ /** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
3932
+ const waitFor = (client, match, within) => new Promise((resolve) => {
3933
+ const started = Date.now();
3934
+ const look = () => {
3935
+ const found = client.events.find(match);
3936
+ if (found !== void 0 || Date.now() - started > within) resolve(found);
3937
+ else setTimeout(look, 20);
3938
+ };
3939
+ look();
3940
+ });
3941
+ /** This function gives the count of lost records in the latest clock line of the prober. */
3942
+ const droppedSoFar$1 = (records) => {
3943
+ for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
3944
+ return 0;
3945
+ };
3946
+ /**
3947
+ * This function does one run of a replica set cell:
3948
+ * - The set converges, and its writes go. The client finds the primary with its monitors, and its
3949
+ * stream of writes gets steady.
3950
+ * - The fault occurs on its member, as the primary of this run makes the roles. A fault with a
3951
+ * hold lifts at its instant in the VM. A stall lifts at its length after the first write into
3952
+ * it. A step-down is replSetStepDown to the primary.
3953
+ * - The run waits for its outcome: the first accepted write of a write sent after the commit of
3954
+ * the fault. It is on a new primary, or on any primary. Then the run waits for the end of the
3955
+ * hold, thus the record shows what the set did in the whole hold.
3956
+ * - Each fault goes, the client stops, and the set converges again. The writes that it then holds
3957
+ * tell which writes it kept. The log lines of the members go onto the monotonic clock.
3958
+ */
3959
+ const replicaSetOnce = async (lab, cell, environment, index) => {
3960
+ const set = cell.replicaSet;
3961
+ const primary = await lab.converge();
3962
+ await lab.flush(primary);
3963
+ await lab.unfreeze(primary);
3964
+ await lab.forget();
3965
+ const from = lab.kernel.length;
3966
+ const logFrom = lab.log.length;
3967
+ const lostBefore = droppedSoFar$1(lab.kernel);
3968
+ const client = await lab.client(replicaSetArgs(cell, lab.members));
3969
+ const lands = cell.prediction.group?.lands ?? "new";
3970
+ const place = cell.faultAt ?? { role: "primary" };
3971
+ let fault = null;
3972
+ let heal = null;
3973
+ let at = null;
3974
+ const undo = [];
3975
+ try {
3976
+ await client.until(["ready"], 6e4);
3977
+ 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`);
3978
+ let healing = null;
3979
+ switch (cell.fault) {
3980
+ case "blackhole":
3981
+ case "partition":
3982
+ case "rst": {
3983
+ const kind = cell.fault === "rst" ? "rst" : "blackhole";
3984
+ const cuts = place.cuts ?? (cell.fault === "partition" ? "group" : "all");
3985
+ const part = lab.partOf(place.role, primary);
3986
+ at = part;
3987
+ fault = await lab.fault(kind, part, cuts);
3988
+ undo.push(() => lab.lift(kind, part, cuts));
3989
+ if (cell.stall !== void 0) {
3990
+ const t1 = fault.t1;
3991
+ const sent = await waitFor(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
3992
+ healing = lab.lift(kind, part, cuts, (sent?.t ?? t1) + valueIn(cell.stall, "s") * 1e9);
3993
+ } else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
3994
+ break;
3995
+ }
3996
+ case "freeze-process": {
3997
+ const part = lab.partOf(place.role, primary);
3998
+ at = part;
3999
+ fault = await lab.signal(part, "STOP");
4000
+ undo.push(() => lab.signal(part, "CONT"));
4001
+ if (cell.hold !== void 0) healing = lab.signal(part, "CONT", fault.t0 + valueIn(cell.hold, "s") * 1e9);
4002
+ break;
4003
+ }
4004
+ case "fin": {
4005
+ const part = lab.partOf(place.role, primary);
4006
+ at = part;
4007
+ fault = await lab.signal(part, "KILL");
4008
+ undo.push(() => lab.restart(part));
4009
+ break;
4010
+ }
4011
+ case "step-down":
4012
+ at = primary;
4013
+ fault = await lab.stepDown(primary);
4014
+ break;
4015
+ default: throw new Error(`a replica set cell has no fault ${cell.fault}`);
4016
+ }
4017
+ const healed = healing === null ? Promise.resolve(null) : healing.then((stamp) => {
4018
+ heal = stamp;
4019
+ undo.length = 0;
4020
+ return stamp;
4021
+ });
4022
+ healed.catch(() => void 0);
4023
+ const t1 = fault.t1;
4024
+ const old = `${lab.members[primary].ip}:${set.port}`;
4025
+ const sentAt = (n) => client.events.find((event) => event.ev === "sent" && Number(event["n"]) === n)?.t ?? -Infinity;
4026
+ 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);
4027
+ if (healing !== null) {
4028
+ await healed.catch(() => void 0);
4029
+ await sleep$1(TAIL_MS$1);
4030
+ }
4031
+ } finally {
4032
+ for (const step of undo.reverse()) await step().catch(() => void 0);
4033
+ await client.stop();
4034
+ }
4035
+ const settled = await lab.settle();
4036
+ const lost = settled === null ? null : settled - lostBefore;
4037
+ const after = await lab.converge().catch(() => null);
4038
+ const holds = after === null ? null : await lab.holds(after);
4039
+ return {
4040
+ environment,
4041
+ index,
4042
+ events: [...client.events],
4043
+ fault,
4044
+ ...heal === null ? {} : { heal },
4045
+ kernel: lab.kernel.slice(from),
4046
+ netns: lab.netns.client,
4047
+ targetNetns: lab.members[primary].netns,
4048
+ lost,
4049
+ group: {
4050
+ members: lab.members,
4051
+ sentinels: {},
4052
+ primary,
4053
+ at,
4054
+ timeline: onMonotonic(lab.log.slice(logFrom), clockOffsetOf(client.events)),
4055
+ after,
4056
+ holds
4057
+ }
4058
+ };
4059
+ };
4060
+ //#endregion
4061
+ //#region src/run.ts
4062
+ /**
4063
+ * This function measures and judges the raw records of a cell, with the code of the harness at
4064
+ * this time. `runCell` uses it during a cell. A holder of the raw records of a cell can use it
4065
+ * again later. Thus the records get a new analysis after a change to observe.ts or to the verdict.
4066
+ */
4067
+ const judge = (cell, runs) => {
4068
+ const measured = runs.filter((run) => run.warmup !== true).map((run) => measure(cell, run));
4069
+ return {
4070
+ measured,
4071
+ verdict: verdictOf(cell.prediction, measured)
4072
+ };
4073
+ };
4074
+ /** This function gives the count of lost records in the latest clock line of the prober, since it began. */
4075
+ const droppedSoFar = (records) => {
4076
+ for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
4077
+ return 0;
4078
+ };
4079
+ /** This function gives the argv of the client for a cell, with a deadline far after all that it waits for. */
4080
+ const probeArgs = (cell, lab) => {
4081
+ const { prediction, probe } = cell;
4082
+ 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"));
4083
+ const args = [
4084
+ lab.targetIp,
2729
4085
  String(lab.targetPort),
2730
4086
  "--mode",
2731
4087
  probe.mode
2732
4088
  ];
4089
+ if (probe.ws !== void 0) {
4090
+ const { ws } = probe;
4091
+ const waits = (prediction.detects === true ? prediction.reconnects?.waits ?? [] : []).reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
4092
+ args.push("--websocket", ws.socket);
4093
+ args.push("--heartbeat", ws.heartbeat === null ? "off" : "on");
4094
+ if (ws.heartbeat !== null) args.push("--heartbeat-interval-ms", String(valueIn(ws.heartbeat.interval, "ms")), "--heartbeat-timeout-ms", String(valueIn(ws.heartbeat.timeout, "ms")), "--heartbeat-misses", String(ws.heartbeat.misses));
4095
+ args.push("--reconnect", ws.reconnect === null ? "off" : "on");
4096
+ if (ws.reconnect !== null) args.push("--reconnect-initial-ms", String(valueIn(ws.reconnect.initialDelay, "ms")), "--reconnect-multiplier", String(ws.reconnect.multiplier), "--reconnect-max-ms", String(valueIn(ws.reconnect.maxDelay, "ms")), "--reconnect-attempts", String(ws.reconnect.attempts), "--reconnect-jitter", String(ws.reconnect.jitter));
4097
+ args.push("--request-timeout-ms", String(valueIn(ws.requestTimeout, "ms")));
4098
+ args.push("--connection-timeout-ms", String(valueIn(ws.connectionTimeout, "ms")));
4099
+ if (ws.hold !== void 0) args.push("--hold-ms", String(valueIn(ws.hold, "ms")));
4100
+ args.push("--max-s", String(Math.ceil((longest + waits) * 2 + 10)));
4101
+ return args;
4102
+ }
2733
4103
  if (probe.http !== void 0) {
2734
4104
  const { http } = probe;
2735
4105
  const gap = http.gap === void 0 ? 0 : valueIn(http.gap, "s");
@@ -2745,6 +4115,15 @@ const probeArgs = (cell, lab) => {
2745
4115
  if (probe.request !== void 0) args.push("--request", probe.request);
2746
4116
  if (probe.count !== void 0) args.push("--count", String(probe.count));
2747
4117
  if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
4118
+ if (probe.driver?.name === "mongodb") {
4119
+ args.push("--set", probe.driver.set);
4120
+ if (probe.driver.op !== void 0) args.push("--op", probe.driver.op);
4121
+ if (cell.writeConcern !== void 0) args.push("--w", String(cell.writeConcern));
4122
+ if (cell.before === true) args.push("--before");
4123
+ if (cell.driverOptions !== void 0) args.push("--options", JSON.stringify(cell.driverOptions));
4124
+ args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
4125
+ return args;
4126
+ }
2748
4127
  if (probe.driver !== void 0) {
2749
4128
  args.push("--keepalive-ms", String(valueIn(probe.driver.keepAlive, "ms")));
2750
4129
  args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
@@ -2758,14 +4137,15 @@ const probeArgs = (cell, lab) => {
2758
4137
  /**
2759
4138
  * This function waits for an error on the socket of the observer, or for its end of file
2760
4139
  * (CLOSE_WAIT, state 8). The prober records both. A cell whose observer is the server, or a proxy
2761
- * of a topology, waits for that. The promise resolves after `within` ms at the latest.
4140
+ * of a topology, waits for that. A target with its own detector can also drop the connection
4141
+ * itself (`dropped`). The promise resolves after `within` ms at the latest.
2762
4142
  */
2763
- const observerError = (cell, partial, within) => new Promise((resolve) => {
4143
+ const observerError = (cell, partial, within, dropped = () => false) => new Promise((resolve) => {
2764
4144
  const started = Date.now();
2765
4145
  const look = () => {
2766
4146
  const run = partial();
2767
4147
  const at = socketOf(cell, run);
2768
- 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();
4148
+ 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)) || dropped() || Date.now() - started > within) resolve();
2769
4149
  else setTimeout(look, 50);
2770
4150
  };
2771
4151
  look();
@@ -2801,6 +4181,94 @@ const requestSent = (probe, within) => new Promise((resolve, reject) => {
2801
4181
  };
2802
4182
  look();
2803
4183
  });
4184
+ /** This time is how long a run of a driver cell continues after its outcome, so that the record shows what came next. */
4185
+ const TAIL_MS = 1e3;
4186
+ /** 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. */
4187
+ const driverOutcome = async (cell, current, within) => {
4188
+ const started = Date.now();
4189
+ while (Date.now() - started < within) {
4190
+ if (settled(cell, current())) return;
4191
+ await sleep(250);
4192
+ }
4193
+ };
4194
+ /** This function gives the longest wait of a run of a driver cell for its outcome, in ms. */
4195
+ const driverWithin = (cell) => (longestOf(cell) * 1.2 + 10) * 1e3;
4196
+ /**
4197
+ * This function does one run of a cell whose fault holds before the client starts (`Cell.before`).
4198
+ * The fault is a network fault at its place. The client then starts into it, and the run waits for
4199
+ * each band of the driver. Then the fault lifts.
4200
+ */
4201
+ const beforeOnce = async (lab, cell, environment, index) => {
4202
+ 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}`);
4203
+ await lab.forget();
4204
+ const from = lab.kernel.length;
4205
+ const middleboxFrom = lab.middlebox.length;
4206
+ const lostBefore = droppedSoFar(lab.kernel);
4207
+ const faultAt = cell.faultAt ?? "target";
4208
+ const kind = cell.fault === "unreachable" ? "reject" : cell.fault;
4209
+ const options = cell.fault === "unreachable" ? { code: cell.icmp ?? "host" } : {};
4210
+ const fault = await lab.fault(kind, faultAt, options);
4211
+ let probe;
4212
+ try {
4213
+ probe = await lab.probe(probeArgs(cell, lab));
4214
+ const started = probe;
4215
+ const current = () => ({
4216
+ environment,
4217
+ index,
4218
+ events: started.events,
4219
+ fault,
4220
+ kernel: lab.kernel.slice(from),
4221
+ netns: lab.netns.client,
4222
+ targetNetns: lab.netns.target,
4223
+ lost: 0
4224
+ });
4225
+ await driverOutcome(cell, current, driverWithin(cell));
4226
+ await sleep(TAIL_MS);
4227
+ } finally {
4228
+ await lab.lift(kind, faultAt, options);
4229
+ await probe?.stop();
4230
+ }
4231
+ const settled = await lab.settle();
4232
+ return {
4233
+ environment,
4234
+ index,
4235
+ events: [...probe?.events ?? []],
4236
+ fault,
4237
+ kernel: lab.kernel.slice(from),
4238
+ netns: lab.netns.client,
4239
+ targetNetns: lab.netns.target,
4240
+ lost: settled === null ? null : settled - lostBefore,
4241
+ ...lab.topology === null ? {} : {
4242
+ topology: lab.topology,
4243
+ middlebox: lab.middlebox.slice(middleboxFrom)
4244
+ }
4245
+ };
4246
+ };
4247
+ /** This function waits for the first ping of the heartbeat of the client at the instant `after` or later: its first ping into a stall. */
4248
+ const pingSent = (probe, after, within) => new Promise((resolve, reject) => {
4249
+ const started = Date.now();
4250
+ const look = () => {
4251
+ const ping = probe.events.find((event) => event.ev === "ping" && event.t >= after);
4252
+ if (ping !== void 0) resolve(ping);
4253
+ else if (Date.now() - started > within) reject(/* @__PURE__ */ new Error(`the client sent no ping into the stall within ${within} ms`));
4254
+ else setTimeout(look, 1);
4255
+ };
4256
+ look();
4257
+ });
4258
+ /**
4259
+ * This function waits until the client wrote `count` lines `dial`, one before each connection
4260
+ * that it tries, or until its deadline. It resolves after `within` ms at the latest.
4261
+ */
4262
+ const dials = (probe, count, within) => new Promise((resolve) => {
4263
+ const started = Date.now();
4264
+ const look = () => {
4265
+ const tried = probe.events.filter((event) => event.ev === "dial").length;
4266
+ const ended = probe.events.some((event) => event.ev === "deadline");
4267
+ if (tried >= count || ended || Date.now() - started > within) resolve();
4268
+ else setTimeout(look, 20);
4269
+ };
4270
+ look();
4271
+ });
2804
4272
  /** This function gives the netem specification of a cell: its latency, then the netem of its fault when it has one. */
2805
4273
  const netemSpec = (cell, fault) => {
2806
4274
  const parts = [...cell.latency === void 0 ? [] : [`delay ${valueIn(cell.latency, "ms")}ms`], ...fault === void 0 ? [] : [fault]];
@@ -2820,6 +4288,7 @@ const netemSpec = (cell, fault) => {
2820
4288
  * A dry run does all of this but the fault: the warm-up of a cell whose fault ends its environment.
2821
4289
  */
2822
4290
  const once = async (lab, cell, environment, index, dry = false) => {
4291
+ if (cell.before === true) return beforeOnce(lab, cell, environment, index);
2823
4292
  await lab.forget();
2824
4293
  const from = lab.kernel.length;
2825
4294
  const middleboxFrom = lab.middlebox.length;
@@ -2853,6 +4322,8 @@ const once = async (lab, cell, environment, index, dry = false) => {
2853
4322
  });
2854
4323
  let fault = null;
2855
4324
  let heal = null;
4325
+ const signalled = !server && cell.target.program !== void 0;
4326
+ let thawing = null;
2856
4327
  try {
2857
4328
  await probe.until(["ready"], 15e3);
2858
4329
  if (dry) {
@@ -2879,6 +4350,17 @@ const once = async (lab, cell, environment, index, dry = false) => {
2879
4350
  const apply = async () => {
2880
4351
  switch (cell.fault) {
2881
4352
  case "freeze-process":
4353
+ if (signalled) {
4354
+ const stamp = await lab.signal("STOP");
4355
+ if (cell.hold !== void 0) {
4356
+ thawing = lab.signal("CONT", stamp.t0 + valueIn(cell.hold, "s") * 1e9).then((thawed) => {
4357
+ heal = thawed;
4358
+ return thawed;
4359
+ });
4360
+ thawing.catch(() => void 0);
4361
+ }
4362
+ return stamp;
4363
+ }
2882
4364
  await peer.freeze();
2883
4365
  return null;
2884
4366
  case "fin":
@@ -2904,7 +4386,13 @@ const once = async (lab, cell, environment, index, dry = false) => {
2904
4386
  };
2905
4387
  const lift = async () => {
2906
4388
  switch (cell.fault) {
2907
- case "freeze-process": return peer.thaw();
4389
+ case "freeze-process":
4390
+ if (signalled) {
4391
+ if (thawing !== null) await thawing.catch(() => lab.signal("CONT"));
4392
+ else await lab.signal("CONT");
4393
+ return;
4394
+ }
4395
+ return peer.thaw();
2908
4396
  case "fin":
2909
4397
  case "middlebox-forget":
2910
4398
  case "proxy-idle":
@@ -2930,19 +4418,27 @@ const once = async (lab, cell, environment, index, dry = false) => {
2930
4418
  if ((cell.probe.mode === "inflight" || cell.probe.mode === "stream") && !replyInFlight) await go();
2931
4419
  const { prediction } = cell;
2932
4420
  const http = cell.probe.http !== void 0;
2933
- if (cell.probe.mode === "gap") {
4421
+ if (prediction.detects !== null && prediction.driver !== void 0) {
4422
+ if (prediction.detects) await driverOutcome(cell, () => ({
4423
+ ...partial(),
4424
+ fault
4425
+ }), driverWithin(cell));
4426
+ else await sleep(valueIn(prediction.window, "s") * 1e3);
4427
+ await sleep(TAIL_MS);
4428
+ } else if (cell.probe.mode === "gap") {
2934
4429
  const gap = cell.probe.http?.gap === void 0 ? 0 : valueIn(cell.probe.http.gap, "s");
2935
4430
  const window = valueIn(prediction.detects ? prediction.high : prediction.window, "s");
2936
4431
  await probe.until(HTTP_DONE, (gap + window) * 2e3 + 1e4).catch(() => void 0);
2937
4432
  } else if (cell.fault === "stall") {
2938
4433
  if (cell.stall === void 0) throw new Error(`${cell.name} says no stall's length`);
2939
- const sent = await requestSent(probe, 5e3);
2940
- heal = await lab.heal(faultAt, sent.t + valueIn(cell.stall, "s") * 1e9);
2941
4434
  const window = valueIn(prediction.detects ? prediction.high : prediction.window, "s");
4435
+ const sent = cell.probe.mode === "idle" ? await pingSent(probe, fault?.t1 ?? 0, window * 1e3) : await requestSent(probe, 5e3);
4436
+ heal = await lab.heal(faultAt, sent.t + valueIn(cell.stall, "s") * 1e9);
2942
4437
  await probe.until([
2943
4438
  "data",
2944
4439
  "error",
2945
- "eof"
4440
+ "eof",
4441
+ "drop"
2946
4442
  ], window * 1e3).catch(() => void 0);
2947
4443
  } else if (prediction.detects === null) {
2948
4444
  const window = valueIn(prediction.window, "s") * 1e3;
@@ -2954,12 +4450,13 @@ const once = async (lab, cell, environment, index, dry = false) => {
2954
4450
  ], window).catch(() => void 0);
2955
4451
  } else if (prediction.detects) {
2956
4452
  const within = valueIn(prediction.high, "s") * 2e3 + 1e4;
2957
- if (cell.observer !== void 0 && cell.observer !== "client") await observerError(cell, partial, within);
4453
+ if (cell.observer !== void 0 && cell.observer !== "client") await observerError(cell, partial, within, () => lab.server.slice(serverFrom).some((line) => line.ev === "drop"));
2958
4454
  else if (http && cell.probe.mode !== "idle") await probe.until(HTTP_DONE, within).catch(() => void 0);
2959
4455
  else await probe.until([
2960
4456
  "error",
2961
4457
  "eof",
2962
4458
  "deadline",
4459
+ "drop",
2963
4460
  ...http ? ["closed"] : []
2964
4461
  ], within).catch(() => void 0);
2965
4462
  } else await sleep(valueIn(prediction.window, "s") * 1e3);
@@ -2967,7 +4464,12 @@ const once = async (lab, cell, environment, index, dry = false) => {
2967
4464
  await lift();
2968
4465
  if (toxicFirst) await lab.toxic(null);
2969
4466
  }
2970
- if (cell.fault === "freeze-process" && !server) await probe.until(["data"], 5e3).catch(() => void 0);
4467
+ if (cell.fault === "freeze-process" && !server && cell.probe.mode !== "idle") await probe.until(["data"], 5e3).catch(() => void 0);
4468
+ const expected = cell.prediction.detects === true ? cell.prediction.reconnects : void 0;
4469
+ if (expected !== void 0) {
4470
+ const waits = expected.waits.reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
4471
+ await dials(probe, 1 + expected.waits.length, waits * 2e3 + 5e3);
4472
+ }
2971
4473
  }
2972
4474
  } finally {
2973
4475
  await probe.stop();
@@ -2979,7 +4481,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
2979
4481
  index,
2980
4482
  events: [...probe.events],
2981
4483
  fault,
2982
- ...cell.fault === "stall" ? { heal } : {},
4484
+ ...cell.fault === "stall" || thawing !== null ? { heal } : {},
2983
4485
  kernel: lab.kernel.slice(from),
2984
4486
  netns: lab.netns.client,
2985
4487
  targetNetns: lab.netns.target,
@@ -3003,6 +4505,7 @@ const line = (cell, run, read) => `${cell.name}: env ${run.environment} ${run.wa
3003
4505
  */
3004
4506
  const inEnvironment = async (cell, environment, count, say) => {
3005
4507
  if (cell.group !== void 0) return inGroupEnvironment(cell, environment, count, say);
4508
+ if (cell.replicaSet !== void 0) return inReplicaSetEnvironment(cell, environment, count, say);
3006
4509
  const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
3007
4510
  try {
3008
4511
  const settings = await lab.configure(cell.targetSettings ?? {});
@@ -3067,6 +4570,34 @@ const inGroupEnvironment = async (cell, environment, count, say) => {
3067
4570
  }
3068
4571
  };
3069
4572
  /**
4573
+ * This function does the part of the runs of a replica set cell in one environment. The warm-up
4574
+ * is a whole run with its fault, as for a group under Sentinels. The set converges before each run.
4575
+ */
4576
+ const inReplicaSetEnvironment = async (cell, environment, count, say) => {
4577
+ const lab = await openReplicaSetLab(cell.replicaSet);
4578
+ try {
4579
+ const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
4580
+ const warmup = {
4581
+ ...await replicaSetOnce(lab, cell, environment, -1),
4582
+ warmup: true
4583
+ };
4584
+ say(line(cell, warmup, measure(cell, warmup)));
4585
+ const runs = [];
4586
+ for (let index = 0; index < count; index += 1) {
4587
+ const run = await replicaSetOnce(lab, cell, environment, index);
4588
+ runs.push(run);
4589
+ say(line(cell, run, measure(cell, run)));
4590
+ }
4591
+ return {
4592
+ manifest,
4593
+ runs,
4594
+ warmup
4595
+ };
4596
+ } finally {
4597
+ await lab.close();
4598
+ }
4599
+ };
4600
+ /**
3070
4601
  * This function does the runs of a cell. They go across new environments, and each environment records
3071
4602
  * its manifest. Then the verdict judges the runs. A median across the edge of the band extends
3072
4603
  * the cell from 11 runs to 22, and then to 44, each extension in new environments. With a
@@ -3115,6 +4646,6 @@ const runCell = async (cell, say = () => {}) => {
3115
4646
  }
3116
4647
  };
3117
4648
  //#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 };
4649
+ export { CONVERGE_MS, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, NOISE, PUMP, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, callOf, clockOffsetOf, crossingsOf, cutOf, deadlineOf, deviceOf, dropFinding, dropOf, endpointAfter, errorsOf, fieldOf, fieldsOf, firstUnanswered, goHttpImage, groupOnce, groupVerdictOf, harnessImage, infoOf, initiateScript, isHopPlace, judge, lagsOf, leftAfter, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, nodeClientImage, nodeHttpImage, onMonotonic, openGroupLab, openLab, openReplicaSetLab, peersOf, pingsOf, proceduresImage, programLines, quantileOf, reconnectFinding, reconnectOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subscriptionsOf, targetImageOf, templated, toxicBody, verdictOf, viewOf, writesOf };
3119
4650
 
3120
4651
  //# sourceMappingURL=index.js.map