@mark1russell7/harness 0.5.0 → 0.7.0

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