@mark1russell7/harness 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -159,9 +159,10 @@ const groupVerdictOf = (prediction, counted) => {
159
159
  for (const run of counted) {
160
160
  const at = run.group?.stages[band.stage];
161
161
  if (at === void 0) {
162
+ const who = band.stage.startsWith("+") ? "no Sentinel announced" : "no member or client marked";
162
163
  outside.push({
163
164
  run: name(run),
164
- why: `no Sentinel announced ${band.stage}`
165
+ why: `${who} ${band.stage}`
165
166
  });
166
167
  continue;
167
168
  }
@@ -382,13 +383,367 @@ const verdictOfBand = (prediction, runs) => {
382
383
  };
383
384
  };
384
385
  //#endregion
386
+ //#region src/driver.ts
387
+ /** These are the errno numbers that the kernel reports to a socket, by the names that predictions use. */
388
+ const ERRNO$1 = {
389
+ 32: "EPIPE",
390
+ 101: "ENETUNREACH",
391
+ 103: "ECONNABORTED",
392
+ 104: "ECONNRESET",
393
+ 110: "ETIMEDOUT",
394
+ 111: "ECONNREFUSED",
395
+ 113: "EHOSTUNREACH"
396
+ };
397
+ const between$2 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
398
+ const round = (seconds) => Math.round(seconds * 1e6) / 1e6;
399
+ /** This function gives the value of a field of a line by its name. A dotted name is a field in a field. */
400
+ const fieldOf = (line, path) => path.split(".").reduce((value, key) => value !== null && typeof value === "object" ? value[key] : void 0, line);
401
+ /** This function tells if a line of the client matches a line match: its `ev`, and each field that the match names. */
402
+ const matches = (line, match) => line.ev === match.ev && Object.entries(match.where ?? {}).every(([path, value]) => fieldOf(line, path) === value);
403
+ /** This function says a line match in words, for a finding. */
404
+ const wordsOf = (match) => {
405
+ const where = Object.entries(match.where ?? {}).map(([path, value]) => `${path} ${String(value)}`);
406
+ return where.length === 0 ? `a ${match.ev} line` : `a ${match.ev} line with ${where.join(", ")}`;
407
+ };
408
+ /** This function gives the sockets of the driver in a run, from the `socket` and `socket-end` lines of the client. */
409
+ const socketsOf = (events) => events.filter((event) => event.ev === "socket").map((event) => {
410
+ const local = String(event["local"]);
411
+ const end = events.find((each) => each.ev === "socket-end" && each["local"] === local && each.t >= event.t);
412
+ return {
413
+ role: String(event["role"]),
414
+ local,
415
+ server: String(event["server"]),
416
+ opened: event.t,
417
+ ended: end?.t ?? null
418
+ };
419
+ });
420
+ /**
421
+ * This function finds the socket of a role at an instant. The pool's socket is the first one, which
422
+ * the client names in its `connected` line. Each other role has the latest socket that opened at
423
+ * the instant or before it.
424
+ */
425
+ const socketAt = (events, role, at) => {
426
+ const sockets = socketsOf(events);
427
+ if (role === "pool") {
428
+ const local = events.find((event) => event.ev === "connected")?.["local"];
429
+ return sockets.find((socket) => socket.local === local);
430
+ }
431
+ return sockets.filter((socket) => socket.role === role && socket.opened <= at).at(-1);
432
+ };
433
+ const placeOf = (run, netns, find) => {
434
+ const cookie = run.kernel.find((record) => record.netns === netns && find(record))?.cookie;
435
+ if (cookie === void 0) return void 0;
436
+ return {
437
+ netns,
438
+ cookie,
439
+ records: run.kernel.filter((record) => record.netns === netns && record.cookie === cookie)
440
+ };
441
+ };
442
+ const portOf = (address) => address.split(":").at(-1) ?? "";
443
+ /**
444
+ * This function gives the socket of the target for a socket of the client. In a direct pair, its
445
+ * peer is the address of the client socket. Behind a router, its peer is the address of the router
446
+ * with the port of the client socket. The router keeps the port when it is free.
447
+ */
448
+ const peerPlace = (run, local) => {
449
+ const peer = run.topology === void 0 ? local : `${run.topology.targetPeer}:${portOf(local)}`;
450
+ return placeOf(run, run.targetNetns, (record) => record.dst === peer);
451
+ };
452
+ /** This function gives the kernel place of a socket of a role at an instant. The role `server` is a socket of the target. */
453
+ const kernelOf = (run, role, at) => {
454
+ if (role === "server") {
455
+ const pool = socketAt(run.events, "pool", at);
456
+ return {
457
+ socket: pool,
458
+ place: pool === void 0 ? void 0 : peerPlace(run, pool.local)
459
+ };
460
+ }
461
+ const socket = socketAt(run.events, role, at);
462
+ return {
463
+ socket,
464
+ place: socket === void 0 ? void 0 : placeOf(run, run.netns, (record) => record.src === socket.local)
465
+ };
466
+ };
467
+ const newState$1 = (record) => record.new;
468
+ const oldState$1 = (record) => record.old;
469
+ /** This function gives the instant that a run starts to look for its outcome. It is the fault, the reply before it, or the start of the client. */
470
+ const outsetOf = (run) => {
471
+ const ready = run.events.find((event) => event.ev === "ready");
472
+ return run.fault?.t0 ?? ready?.t ?? run.events[0]?.t ?? 0;
473
+ };
474
+ /**
475
+ * This function gives the instant of a reference in a run, on a socket of the driver:
476
+ * - `last-received`: the last segment that the socket received before the fault held. A band of
477
+ * the kernel counts from the last segment of any kind. A band that a line ends is a timer of
478
+ * the driver. It counts from the last segment with data that the socket received or sent.
479
+ * - `request-sent`: the send of the operation into the fault, the second `sent` line.
480
+ * - `first-retransmission`: the first retransmission of the RTO timer on the socket.
481
+ * - `peer-closed`: the change of state of the socket of the target, when its process dies.
482
+ * - `fault`: the start of the commit of the fault. `client-start`: the `connecting` line.
483
+ */
484
+ const referenceOf = (run, from, role, data) => {
485
+ const ready = run.events.find((event) => event.ev === "ready");
486
+ const before = run.fault?.t1 ?? ready?.t ?? Infinity;
487
+ if (from === "fault") return run.fault?.t0;
488
+ if (from === "client-start") return (run.events.find((event) => event.ev === "connecting") ?? run.events[0])?.t;
489
+ if (from === "request-sent") return run.events.filter((event) => event.ev === "sent")[1]?.t;
490
+ const { socket, place } = kernelOf(run, role, before);
491
+ if (place === void 0) return void 0;
492
+ if (from === "last-received") {
493
+ const counts = (record) => data ? (record.ev === "rx" || record.ev === "tx") && (record.seg?.len.value ?? 0) > 0 : record.ev === "rx";
494
+ return place.records.filter((record) => record.mono <= before && counts(record)).at(-1)?.mono;
495
+ }
496
+ if (from === "first-retransmission") {
497
+ const request = run.events.filter((event) => event.ev === "sent")[1]?.t ?? before;
498
+ return place.records.find((record) => record.ev === "retrans" && record.why === "rto" && record.mono > request)?.mono;
499
+ }
500
+ return (socket === void 0 ? void 0 : peerPlace(run, socket.local))?.records.find((record) => record.ev === "state" && oldState$1(record) === 1)?.mono;
501
+ };
502
+ /** This function gives the first line of the client that matches, after an instant. */
503
+ const lineAfter = (events, match, after) => events.find((event) => event.t > after && matches(event, match));
504
+ /** This function gives the name of the error of a line: the field that the match names, or `name`. */
505
+ const nameOf = (line, match) => String(fieldOf(line, match.name ?? "name") ?? line.ev);
506
+ /** This function gives the report of the kernel on a socket after an instant: an error, or the end of the file (CLOSE_WAIT). */
507
+ const reportOf = (place, after) => {
508
+ const report = place.records.find((record) => record.mono > after && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && newState$1(record) === 8));
509
+ if (report === void 0) return void 0;
510
+ return report.ev === "error" ? {
511
+ at: report.mono,
512
+ outcome: {
513
+ kind: "error",
514
+ errno: ERRNO$1[report.err] ?? `E${report.err}`
515
+ }
516
+ } : {
517
+ at: report.mono,
518
+ outcome: { kind: "eof" }
519
+ };
520
+ };
521
+ /** This function gives the latest instant of the bands of a driver prediction, in seconds from their references. */
522
+ const longestOf = (cell) => {
523
+ const { prediction } = cell;
524
+ if (prediction.detects === null) return valueIn(prediction.window, "s");
525
+ const steps = (prediction.driver?.then ?? []).map((step) => valueIn(step.high, "s"));
526
+ const own = prediction.detects ? valueIn(prediction.high, "s") : valueIn(prediction.window, "s");
527
+ return Math.max(own, ...steps);
528
+ };
529
+ /**
530
+ * This function tells if a run of a driver cell has its outcome, so that its wait can stop. For a
531
+ * detection, the outcome is the end of its band and of each later band. A silent prediction waits
532
+ * for its window.
533
+ */
534
+ const settled = (cell, run) => {
535
+ const { prediction } = cell;
536
+ if (prediction.detects !== true || prediction.driver === void 0) return false;
537
+ const driver = prediction.driver;
538
+ const after = outsetOf(run);
539
+ return (driver.ends === void 0 ? (() => {
540
+ const { place } = kernelOf(run, driver.socket, run.fault?.t1 ?? after);
541
+ return place !== void 0 && reportOf(place, after) !== void 0;
542
+ })() : lineAfter(run.events, driver.ends, after) !== void 0) && (driver.then ?? []).every((step) => lineAfter(run.events, step.ends, after) !== void 0);
543
+ };
544
+ /**
545
+ * This function examines the keepalive timer of a socket between two instants. A firing two
546
+ * jiffies or more behind its wheel slot was delayed by the host, which stopped a vCPU. A firing
547
+ * that found the socket held by its owner put itself off by HZ/20. Either way, the run measured
548
+ * something other than the timer.
549
+ */
550
+ const delayed = (place, from, to) => {
551
+ const firings = place.records.filter((record) => record.ev === "timer" && record.what === "keepalive" && record.mono >= from && record.mono <= to);
552
+ const late = firings.find((record) => (record.behind?.value ?? 0) >= 2);
553
+ if (late !== void 0) return `its keepalive timer ran ${late.behind.value} jiffies behind its wheel slot: the VM delayed it, not the kernel`;
554
+ return firings.find((record) => record.owned === true) === void 0 ? null : "its keepalive timer found the socket held by its owner and put itself off by HZ/20: the run measured the owner, not the timer";
555
+ };
556
+ /**
557
+ * This function reads one run of a driver cell.
558
+ * - The band of the cell counts from its reference on its socket. It ends at its line, or at the
559
+ * report of the kernel on that socket.
560
+ * - Each later band (`then`) gets the same test. A later band outside its band, or absent, is a finding.
561
+ * - A line that the prediction names as quiet, after the fault and in its window, is a finding.
562
+ * So is a flow of an open socket of the driver that a router forgot, for a silent prediction.
563
+ * - A silent prediction can time a reply (`reply`), and the verdict tests it against `replies`.
564
+ *
565
+ * A run is void when the prober lost records, or when the client did not get its first reply. It
566
+ * is also void when the operation went out before the fault held. A late lift of a hold and a
567
+ * keepalive probe before the fault also make it void. So do a delayed timer and a round trip
568
+ * above the assumption.
569
+ */
570
+ const measureDriver = (cell, run) => {
571
+ const { prediction } = cell;
572
+ if (prediction.detects === null || prediction.driver === void 0) throw new Error(`${cell.name} has no driver band`);
573
+ const driver = prediction.driver;
574
+ const { events } = run;
575
+ const ready = events.find((event) => event.ev === "ready");
576
+ const request = events.filter((event) => event.ev === "sent")[1];
577
+ const after = outsetOf(run);
578
+ const before = run.fault?.t1 ?? ready?.t ?? after;
579
+ const { place } = kernelOf(run, driver.socket, before);
580
+ const records = place?.records ?? [];
581
+ const srtt = kernelOf(run, "pool", before).place?.records.find((record) => record.tcp?.srtt !== void 0)?.tcp?.srtt;
582
+ const rtt = srtt === void 0 ? null : quantity(valueIn(srtt, "us") / 1e3, "ms");
583
+ const base = {
584
+ environment: run.environment,
585
+ index: run.index,
586
+ probes: records.filter((record) => record.ev === "probe" && record.what === "keepalive").length,
587
+ retransmissions: records.filter((record) => record.ev === "retrans").length,
588
+ acks: request === void 0 ? 0 : records.filter((record) => record.ev === "rx" && record.mono > request.t).length,
589
+ rtt
590
+ };
591
+ const nothing = {
592
+ outcome: { kind: "silent" },
593
+ elapsed: null,
594
+ socket: null,
595
+ noticed: null,
596
+ steps: [],
597
+ finding: null
598
+ };
599
+ if (cell.before !== true && ready === void 0) return {
600
+ ...base,
601
+ ...nothing,
602
+ void: "the client never had its reply"
603
+ };
604
+ if (run.lost === null) return {
605
+ ...base,
606
+ ...nothing,
607
+ void: "the prober wrote no clock line after the run, so nothing says what it dropped"
608
+ };
609
+ if (run.lost > 0) return {
610
+ ...base,
611
+ ...nothing,
612
+ void: `the prober dropped ${run.lost} records`
613
+ };
614
+ let voided = null;
615
+ if (cell.probe.mode !== "idle" && cell.before !== true) {
616
+ if (request === void 0) voided = "the operation was never sent into the fault";
617
+ else if (run.fault !== null && run.fault.t1 > request.t) voided = "the operation went out before the fault held";
618
+ }
619
+ const keepalive = records.find((record) => record.ev === "probe" && record.what === "keepalive");
620
+ if (voided === null && run.fault !== null && keepalive !== void 0 && keepalive.mono < run.fault.t1) voided = "a keepalive probe went out before the fault held";
621
+ if (voided === null && cell.hold !== void 0 && run.fault !== null) {
622
+ const heal = run.heal ?? null;
623
+ if (heal === null) voided = "the fault never lifted";
624
+ else {
625
+ const late = (heal.t0 - run.fault.t0) / 1e9 - valueIn(cell.hold, "s");
626
+ if (late > valueIn(NOISE, "s")) voided = `the lift came ${Math.round(late * 1e4) / 10} ms after its hold: the injector was late, not the client`;
627
+ }
628
+ }
629
+ const assumed = prediction.rtt;
630
+ if (voided === null && assumed !== void 0 && rtt !== null && valueIn(rtt, "ms") > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt.value} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
631
+ const findings = [];
632
+ const noise = valueIn(NOISE, "s");
633
+ const until = prediction.detects ? Infinity : after + valueIn(prediction.window, "s") * 1e9;
634
+ for (const match of driver.quiet ?? []) {
635
+ const line = events.find((event) => event.t > after && event.t <= until && matches(event, match));
636
+ if (line !== void 0) findings.push(`${wordsOf(match)} came ${between$2(after, line.t).value} s after the fault, where none should`);
637
+ }
638
+ if (!prediction.detects && run.middlebox !== void 0) for (const socket of socketsOf(events)) {
639
+ const forgot = run.middlebox.find((line) => line.ev === "ct" && line["type"] === "DESTROY" && String(line["orig"] ?? "").startsWith(`${socket.local}>`) && line.t > socket.opened && (socket.ended === null || line.t < socket.ended) && line.t <= until);
640
+ if (forgot !== void 0) findings.push(`a router forgot the flow of the ${socket.role} socket ${between$2(after, forgot.t).value} s after the fault, where it should keep it`);
641
+ }
642
+ const stepTimes = (driver.then ?? []).map((step) => {
643
+ const line = lineAfter(events, step.ends, after);
644
+ const from = referenceOf(run, step.from, step.socket ?? driver.socket, true);
645
+ if (line === void 0 || from === void 0) {
646
+ findings.push(line === void 0 ? `${step.what}: ${wordsOf(step.ends)} never came` : `${step.what}: its reference (${step.from}) never came`);
647
+ return {
648
+ what: step.what,
649
+ name: line === void 0 ? null : nameOf(line, step.ends),
650
+ elapsed: null
651
+ };
652
+ }
653
+ const elapsed = between$2(from, line.t);
654
+ const name = nameOf(line, step.ends);
655
+ if (step.name !== "OK" && name !== step.name) findings.push(`${step.what}: it ended with ${name}, not ${step.name}`);
656
+ const [low, high] = [round(valueIn(step.low, "s") - noise), round(valueIn(step.high, "s") + noise)];
657
+ if (elapsed.value < low || elapsed.value > high) findings.push(`${step.what}: ${elapsed.value} s is outside [${low}, ${high}] s`);
658
+ return {
659
+ what: step.what,
660
+ name,
661
+ elapsed
662
+ };
663
+ });
664
+ const stepped = stepTimes.length === 0 ? {} : { driver: { steps: stepTimes } };
665
+ if (!prediction.detects) {
666
+ if (driver.reply === void 0 || prediction.replies === void 0) return {
667
+ ...base,
668
+ ...nothing,
669
+ ...stepped,
670
+ void: voided,
671
+ finding: findings[0] ?? null
672
+ };
673
+ const line = lineAfter(events, driver.reply.ends, after);
674
+ const from = referenceOf(run, driver.reply.from, driver.socket, true);
675
+ if (line === void 0 || from === void 0) return {
676
+ ...base,
677
+ ...nothing,
678
+ ...stepped,
679
+ void: voided,
680
+ finding: findings[0] ?? `the reply never came: ${wordsOf(driver.reply.ends)}`
681
+ };
682
+ return {
683
+ ...base,
684
+ ...nothing,
685
+ ...stepped,
686
+ outcome: { kind: "answered" },
687
+ elapsed: between$2(from, line.t),
688
+ void: voided,
689
+ finding: findings[0] ?? null
690
+ };
691
+ }
692
+ const line = driver.ends === void 0 ? void 0 : lineAfter(events, driver.ends, after);
693
+ const report = driver.ends === void 0 && place !== void 0 ? reportOf(place, after) : void 0;
694
+ if (driver.ends === void 0 && place === void 0) return {
695
+ ...base,
696
+ ...nothing,
697
+ ...stepped,
698
+ void: voided ?? `the prober saw nothing of the ${driver.socket} socket`,
699
+ finding: findings[0] ?? null
700
+ };
701
+ const ended = line?.t ?? report?.at;
702
+ const outcome = line !== void 0 && driver.ends !== void 0 ? {
703
+ kind: "error",
704
+ errno: nameOf(line, driver.ends)
705
+ } : report?.outcome ?? { kind: "silent" };
706
+ if (ended === void 0) return {
707
+ ...base,
708
+ ...nothing,
709
+ ...stepped,
710
+ void: voided,
711
+ finding: findings[0] ?? `nothing detected it: ${prediction.timer} never fired`
712
+ };
713
+ const reference = referenceOf(run, prediction.from, driver.socket, driver.ends !== void 0);
714
+ if (reference === void 0) return {
715
+ ...base,
716
+ ...nothing,
717
+ ...stepped,
718
+ outcome,
719
+ void: voided,
720
+ finding: `it ended without its reference event (${prediction.from} on the ${driver.socket} socket)`
721
+ };
722
+ if (driver.ends === void 0 && prediction.from === "last-received" && place !== void 0) voided ??= delayed(place, reference, ended);
723
+ const name = outcome.kind === "error" ? outcome.errno : outcome.kind === "eof" ? "EOF" : "silent";
724
+ if (prediction.errno !== "OK" && name !== prediction.errno) findings.unshift(`it ended with ${name}, not ${prediction.errno}`);
725
+ const seen = report === void 0 ? void 0 : events.find((event) => event.t >= report.at && (event.ev === "error" || event.ev === "socket-error"));
726
+ return {
727
+ ...base,
728
+ ...stepped,
729
+ outcome,
730
+ elapsed: between$2(reference, ended),
731
+ socket: seen === void 0 ? null : between$2(reference, seen.t),
732
+ noticed: report === void 0 || seen === void 0 ? null : between$2(report.at, seen.t),
733
+ steps: (prediction.from === "last-received" ? records.filter((record) => record.ev === "probe" && record.what === "keepalive" && record.mono > reference) : []).map((record) => between$2(reference, record.mono)),
734
+ void: voided,
735
+ finding: findings[0] ?? null
736
+ };
737
+ };
738
+ //#endregion
385
739
  //#region src/timeline.ts
386
740
  /**
387
741
  * This module measures a run of a group cell. It gives the first write that a primary accepted
388
- * after the fault, and the timeline of the group 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
389
744
  * became of each write.
390
745
  */
391
- /** These are the stages of a timeline, in their sequence after a fault. */
746
+ /** These are the stages of a timeline under Sentinels, in their sequence after a fault. */
392
747
  const STAGES = [
393
748
  "+sdown",
394
749
  "+odown",
@@ -418,6 +773,105 @@ const stagesOf = (timeline, from) => {
418
773
  }
419
774
  return out;
420
775
  };
776
+ /** These are the stages of the timeline of a replica set, in their usual sequence after a fault. */
777
+ const REPLICA_SET_STAGES = [
778
+ "member-down",
779
+ "election-start",
780
+ "elected",
781
+ "caught-up",
782
+ "writable",
783
+ "old-primary-steps-down",
784
+ "discovered",
785
+ "written-off",
786
+ "first-write"
787
+ ];
788
+ /** These are the log lines of mongod that mark each stage of a member. */
789
+ const MEMBER_MARKS = {
790
+ "member-down": {
791
+ ids: [21216],
792
+ message: /now in state DOWN/,
793
+ at: "other"
794
+ },
795
+ "election-start": {
796
+ ids: [4615652, 21437],
797
+ message: /^Starting an election|Skipping dry run|Conducting a dry run/,
798
+ at: "other"
799
+ },
800
+ elected: {
801
+ ids: [21450],
802
+ message: /^Election succeeded/,
803
+ at: "other"
804
+ },
805
+ "caught-up": {
806
+ ids: [21363],
807
+ message: /^Exited primary catch-up mode/,
808
+ at: "other"
809
+ },
810
+ writable: {
811
+ ids: [21331],
812
+ message: /^Transition to primary complete/,
813
+ at: "other"
814
+ },
815
+ "old-primary-steps-down": {
816
+ ids: [
817
+ 21809,
818
+ 21475,
819
+ 962660
820
+ ],
821
+ message: /relinquishing primary|Stepping down from primary/,
822
+ at: "old"
823
+ }
824
+ };
825
+ /** This function tells if a log line of a member marks a stage, with the old primary of the run. */
826
+ const marks = (line, stage, old, oldAddress) => {
827
+ const mark = MEMBER_MARKS[stage];
828
+ const member = String(line["member"] ?? "");
829
+ if (mark.at === "old" !== (member === old)) return false;
830
+ const attr = line["attr"] ?? {};
831
+ if (stage === "member-down" && !JSON.stringify(attr).includes(oldAddress)) return false;
832
+ if (stage === "old-primary-steps-down" && Number(line["id"]) === 21358 && attr["oldState"] === "PRIMARY") return true;
833
+ return mark.ids.includes(Number(line["id"])) || mark.message.test(String(line["msg"] ?? ""));
834
+ };
835
+ /**
836
+ * This function gives the time of each stage of a replica set from an instant, in a run. The stages
837
+ * of the members come from their log lines (`ev` "mongod"), whose times the lab puts on the
838
+ * monotonic clock. The stages of the client come from its lines:
839
+ * - `discovered`: a member other than the old primary becomes RSPrimary in the view of the client.
840
+ * - `written-off`: the first heartbeat of the client to the old primary that fails.
841
+ * - `first-write`: the first write that a primary accepts, of a write that the client sent after
842
+ * the commit of the fault (`sentAfter`). It is on a new primary when `lands` is `new`.
843
+ */
844
+ const replicaSetStagesOf = (run, from, sentAfter, lands) => {
845
+ const out = {};
846
+ const old = run.group?.primary ?? "";
847
+ const oldAddress = `${run.group?.members[old]?.ip ?? ""}:`;
848
+ for (const line of run.group?.timeline ?? []) {
849
+ if (line.ev !== "mongod" || line.t < from) continue;
850
+ for (const stage of Object.keys(MEMBER_MARKS)) if (out[stage] === void 0 && marks(line, stage, old, oldAddress)) out[stage] = between$1(from, line.t);
851
+ }
852
+ const isOld = (address) => typeof address === "string" && address.startsWith(oldAddress);
853
+ for (const event of run.events) {
854
+ if (event.t < from) continue;
855
+ if (out.discovered === void 0 && event.ev === "sdam" && event["event"] === "server-changed" && !isOld(event["address"])) {
856
+ if (event["next"]?.type === "RSPrimary") out.discovered = between$1(from, event.t);
857
+ }
858
+ if (out["written-off"] === void 0 && event.ev === "sdam" && event["event"] === "heartbeat-failed" && isOld(event["address"])) out["written-off"] = between$1(from, event.t);
859
+ }
860
+ const accepted = acceptedAfter(run, sentAfter, lands);
861
+ if (accepted !== void 0) out["first-write"] = between$1(from, accepted.t);
862
+ return out;
863
+ };
864
+ /** This function gives the first write that a primary accepted, of a write sent after an instant. It is on a new primary for `new`. */
865
+ const acceptedAfter = (run, sentAfter, lands) => {
866
+ const sent = /* @__PURE__ */ new Map();
867
+ for (const event of run.events) if (event.ev === "sent") sent.set(Number(event["n"]), event.t);
868
+ const primary = run.group?.primary ?? "";
869
+ return run.events.find((event) => {
870
+ if (event.ev !== "ack" || (sent.get(Number(event["n"])) ?? -Infinity) <= sentAfter) return false;
871
+ const member = memberAt(run, event["at"]);
872
+ return member !== null && (lands === "new" ? member !== primary : true);
873
+ });
874
+ };
421
875
  /** This function gives what became of each write of a run, from the lines of the client and the keys that the group kept. */
422
876
  const writesOf = (run, after, primary) => {
423
877
  const { events } = run;
@@ -490,7 +944,10 @@ const lagsOf = (run, kernel) => {
490
944
  * - The reference is the start of the commit of the fault, as the injector stamped it. For a
491
945
  * stall, it is the first write into the fault, as for the stall of a direct pair.
492
946
  * - The outcome is the first write that a primary accepted after the fault. The prediction says
493
- * which primary: a new one, or the same one after the lift.
947
+ * which primary: a new one, or the same one after the lift. In a replica set, it is the first
948
+ * accepted write of a write that the client sent after the commit of the fault.
949
+ * - The stages come from the Sentinels, or from the logs of the members of a replica set and the
950
+ * lines of its client (`replicaSetStagesOf`).
494
951
  * - Its time is the last segment with data that the client's socket to that primary received
495
952
  * before the client saw the acceptance. The stamp of the client is the fallback.
496
953
  * - A run is void when the prober lost records, or when the client did not become ready. It is
@@ -546,16 +1003,25 @@ const measureGroup = (cell, run) => {
546
1003
  group: empty
547
1004
  };
548
1005
  const timeline = run.group?.timeline ?? [];
549
- const stages = stagesOf(timeline, t0);
550
1006
  const heal = run.heal ?? null;
551
1007
  const lands = expected?.lands ?? "new";
552
- const 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);
553
1014
  const accepted = events.find((event) => {
554
1015
  if (event.ev !== "ack" || event.t <= after) return false;
1016
+ if (replica && (sentAt.get(Number(event["n"])) ?? -Infinity) <= run.fault.t1) return false;
555
1017
  const member = memberAt(run, event["at"]);
556
1018
  return member !== null && (lands === "new" ? member !== primary : true);
557
1019
  });
558
1020
  const to = accepted === void 0 ? null : memberAt(run, accepted["at"]);
1021
+ if (replica) {
1022
+ delete stages["first-write"];
1023
+ if (accepted !== void 0) stages["first-write"] = between$1(t0, accepted.t);
1024
+ }
559
1025
  const group = {
560
1026
  from: primary,
561
1027
  to,
@@ -580,7 +1046,7 @@ const measureGroup = (cell, run) => {
580
1046
  }
581
1047
  }
582
1048
  const assumed = prediction.detects === null ? void 0 : prediction.rtt;
583
- const first = kernel.find((record) => record.netns === run.netns && record.dst === `${run.group?.members[primary]?.ip}:${cell.group?.port}` && record.tcp?.srtt !== void 0);
1049
+ const first = kernel.find((record) => record.netns === run.netns && record.dst === `${run.group?.members[primary]?.ip}:${(cell.group ?? cell.replicaSet)?.port}` && record.tcp?.srtt !== void 0);
584
1050
  const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
585
1051
  if (voided === null && assumed !== void 0 && rtt !== null && rtt > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
586
1052
  if (voided === null && prediction.detects === true && prediction.from === "fault" && (run.fault.t1 - run.fault.t0) / 1e6 > valueIn(NOISE, "ms")) voided = `the commit of the fault took ${Math.round((run.fault.t1 - run.fault.t0) / 1e5) / 10} ms, longer than the noise of the pipeline: its start is not known`;
@@ -596,8 +1062,12 @@ const measureGroup = (cell, run) => {
596
1062
  }
597
1063
  }
598
1064
  const findings = [];
599
- const switched = stages["+switch-master"];
1065
+ const switched = replica ? stages["elected"] : stages["+switch-master"];
600
1066
  if (expected?.failover === false && switched !== void 0) findings.push(`the group failed over ${switched.value} s after the fault, where it should not`);
1067
+ for (const stage of expected?.quiet ?? []) {
1068
+ const at = stages[stage];
1069
+ if (at !== void 0) findings.push(`${stage} came ${at.value} s after the fault, where it should not`);
1070
+ }
601
1071
  if (accepted !== void 0 && lands === "same" && to !== primary) findings.push(`the client wrote to ${to ?? String(accepted["at"])}, where it should stay on ${primary}`);
602
1072
  for (const error of expected?.errors ?? []) {
603
1073
  const met = group.errors.find((each) => each.name === error.name);
@@ -615,7 +1085,8 @@ const measureGroup = (cell, run) => {
615
1085
  };
616
1086
  }
617
1087
  const address = String(accepted["at"]);
618
- const 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);
619
1090
  const at = segment?.mono ?? accepted.t;
620
1091
  return {
621
1092
  outcome: { kind: "answered" },
@@ -835,7 +1306,7 @@ const requestOf = (events) => {
835
1306
  };
836
1307
  /** This function reads the socket of one run against its cell, as `measure` does without the second request of an HTTP client. */
837
1308
  const measureSocket = (cell, run) => {
838
- if (cell.group !== void 0) {
1309
+ if (cell.group !== void 0 || cell.replicaSet !== void 0) {
839
1310
  const read = measureGroup(cell, run);
840
1311
  return {
841
1312
  environment: run.environment,
@@ -853,6 +1324,7 @@ const measureSocket = (cell, run) => {
853
1324
  group: read.group
854
1325
  };
855
1326
  }
1327
+ if (cell.prediction.detects !== null && cell.prediction.driver !== void 0) return measureDriver(cell, run);
856
1328
  const { events, kernel } = run;
857
1329
  const ready = events.find((event) => event.ev === "ready");
858
1330
  const local = events.find((event) => event.ev === "connected")?.["local"];
@@ -960,6 +1432,7 @@ const measureSocket = (cell, run) => {
960
1432
  const referenceAt = (from) => {
961
1433
  if (from === "request-sent") return request.t;
962
1434
  if (from === "fault") return run.fault?.t0;
1435
+ if (from === "client-start") return (events.find((event) => event.ev === "connecting") ?? events[0])?.t;
963
1436
  return (from === "last-received" ? socket.filter((each) => each.ev === "rx" && each.mono <= before).at(-1) : from === "first-retransmission" ? retransmitted.find((each) => each.why === "rto") : kernel.find((each) => each.netns === peerNetns && each.cookie === peerCookie && each.ev === "state" && oldState(each) === 1))?.mono;
964
1437
  };
965
1438
  const lastRx = kernel.filter((each) => each.netns === run.netns && each.src === local && each.ev === "rx" && each.mono <= before).at(-1)?.mono ?? ready.t;
@@ -1200,6 +1673,12 @@ const NODE_HTTP = {
1200
1673
  dockerfile: "harness/image/node-http/Dockerfile",
1201
1674
  from: ["harness/image/node-http"]
1202
1675
  };
1676
+ /** This image holds the MongoDB client: the driver mongodb 7.7.0 on Node 24.21.0, and its step-down script. */
1677
+ const MONGO = {
1678
+ name: "loom-harness-mongo",
1679
+ dockerfile: "harness/image/mongo/Dockerfile",
1680
+ from: ["harness/image/mongo"]
1681
+ };
1203
1682
  /** This image is the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow. */
1204
1683
  const ROUTER = {
1205
1684
  name: "loom-harness-router",
@@ -1270,6 +1749,8 @@ const routerImage = () => build(ROUTER);
1270
1749
  const goHttpImage = () => build(GO_HTTP);
1271
1750
  /** This function gives the image of the Node HTTP clients. */
1272
1751
  const nodeHttpImage = () => build(NODE_HTTP);
1752
+ /** This function gives the image of the MongoDB client. */
1753
+ const mongoClientImage = () => build(MONGO);
1273
1754
  /**
1274
1755
  * This time is the interval of the listing of a router, when its hop does not set one. A listing
1275
1756
  * evicts each expired flow that it walks past, and the DESTROY event of that flow follows.
@@ -1422,6 +1903,11 @@ const clientOf = async (kind, image) => {
1422
1903
  command: ["node", `/opt/harness/node-http/client-${kind === "undici" ? "undici" : "agent"}.mjs`],
1423
1904
  clientKind: kind
1424
1905
  };
1906
+ if (kind === "mongodb") return {
1907
+ clientImage: await mongoClientImage(),
1908
+ command: ["node", "/opt/harness/mongo/client-mongo.mjs"],
1909
+ clientKind: kind
1910
+ };
1425
1911
  return {
1426
1912
  clientImage: image,
1427
1913
  command: ["/opt/harness/client"],
@@ -1675,6 +2161,7 @@ const openLab = async (target, kind = "tcp", topology) => {
1675
2161
  };
1676
2162
  const paused = runtime.container.getById(server.getId());
1677
2163
  let targetAdmin;
2164
+ let signaller;
1678
2165
  const hopNamed = (name) => {
1679
2166
  const found = inChain.find((hop) => hop.hop.name === name);
1680
2167
  if (found === void 0) throw new Error(`the topology has no hop ${name}`);
@@ -1937,6 +2424,19 @@ const openLab = async (target, kind = "tcp", topology) => {
1937
2424
  },
1938
2425
  freeze: () => paused.pause().then(() => void 0),
1939
2426
  thaw: () => paused.unpause().then(() => void 0),
2427
+ async signal(signal, at) {
2428
+ if (target.program === void 0) throw new Error(`${target.image} names no server process to signal`);
2429
+ signaller ??= await withPid(new GenericContainer(image).withNetworkMode(`container:${server.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${server.getId()}`).start();
2430
+ if (!opened.includes(signaller)) opened.push(signaller);
2431
+ const when = at === void 0 ? [] : ["--at", String(Math.round(at))];
2432
+ return stamped(signaller, [
2433
+ "/opt/harness/inject",
2434
+ "signal",
2435
+ target.program,
2436
+ signal,
2437
+ ...when
2438
+ ]);
2439
+ },
1940
2440
  async probe(args) {
1941
2441
  const events = lines();
1942
2442
  const container = await new GenericContainer(clientImage).withNetworkMode(`container:${client.getId()}`).withCommand([...command, ...args]).withLogConsumer(events.consume).start();
@@ -2153,7 +2653,7 @@ const viewOf = (group, ips, sentinels, members) => {
2153
2653
  why: "healthy"
2154
2654
  };
2155
2655
  };
2156
- const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2656
+ const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2157
2657
  /** This time is the longest that a group takes to converge before a run, or after it. */
2158
2658
  const CONVERGE_MS = 24e4;
2159
2659
  /**
@@ -2348,7 +2848,7 @@ const openGroupLab = async (group, options = {}) => {
2348
2848
  }));
2349
2849
  calm = last.healthy ? calm + 1 : 0;
2350
2850
  if (calm >= 2 && last.primary !== null) return last.primary;
2351
- await sleep$1(1e3);
2851
+ await sleep$2(1e3);
2352
2852
  }
2353
2853
  throw new Error(`the group did not converge in ${within} ms: ${last.why}`);
2354
2854
  },
@@ -2426,7 +2926,7 @@ const openGroupLab = async (group, options = {}) => {
2426
2926
  const started = Date.now();
2427
2927
  while (await cli(member, ["PING"]).catch(() => "") !== "PONG") {
2428
2928
  if (Date.now() - started > 3e4) throw new Error(`${member} did not start again`);
2429
- await sleep$1(200);
2929
+ await sleep$2(200);
2430
2930
  }
2431
2931
  },
2432
2932
  failover(coordinated) {
@@ -2544,9 +3044,9 @@ const openGroupLab = async (group, options = {}) => {
2544
3044
  }
2545
3045
  };
2546
3046
  /** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
2547
- const STEADY = 20;
3047
+ const STEADY$1 = 20;
2548
3048
  /** This time is how long a run continues after its outcome, so that the record shows what came next. */
2549
- const TAIL_MS = 2e3;
3049
+ const TAIL_MS$2 = 2e3;
2550
3050
  /** This function gives the address of a member, with the port of the group. */
2551
3051
  const addressOf = (lab, member) => `${lab.members[member].ip}:${lab.group.port}`;
2552
3052
  /** This function gives the latest time that a group cell waits for its outcome, in ms from the fault. */
@@ -2569,7 +3069,7 @@ const clientArgs = (cell, lab) => {
2569
3069
  ];
2570
3070
  };
2571
3071
  /** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
2572
- const waitFor = (client, match, within) => new Promise((resolve) => {
3072
+ const waitFor$1 = (client, match, within) => new Promise((resolve) => {
2573
3073
  const started = Date.now();
2574
3074
  const look = () => {
2575
3075
  const found = client.events.find(match);
@@ -2585,7 +3085,7 @@ const subscriptionsOf = (timeline) => {
2585
3085
  return [...latest.values()];
2586
3086
  };
2587
3087
  /** This function gives the count of lost records in the latest clock line of the prober. */
2588
- const droppedSoFar$1 = (records) => {
3088
+ const droppedSoFar$2 = (records) => {
2589
3089
  for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
2590
3090
  return 0;
2591
3091
  };
@@ -2607,7 +3107,7 @@ const groupOnce = async (lab, cell, environment, index) => {
2607
3107
  await lab.forget();
2608
3108
  const from = lab.kernel.length;
2609
3109
  const timelineFrom = lab.timeline.length;
2610
- const lostBefore = droppedSoFar$1(lab.kernel);
3110
+ const lostBefore = droppedSoFar$2(lab.kernel);
2611
3111
  const client = await lab.client(clientArgs(cell, lab));
2612
3112
  const lands = cell.prediction.group?.lands ?? "new";
2613
3113
  const place = cell.faultAt ?? { role: "primary" };
@@ -2617,7 +3117,7 @@ const groupOnce = async (lab, cell, environment, index) => {
2617
3117
  const undo = [];
2618
3118
  try {
2619
3119
  await client.until(["ready"], 6e4);
2620
- if (await waitFor(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY, 3e4) === void 0) throw new Error(`the client did not write ${STEADY} times in 30 s`);
3120
+ if (await waitFor$1(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY$1, 3e4) === void 0) throw new Error(`the client did not write ${STEADY$1} times in 30 s`);
2621
3121
  let healing = null;
2622
3122
  switch (cell.fault) {
2623
3123
  case "blackhole":
@@ -2631,7 +3131,7 @@ const groupOnce = async (lab, cell, environment, index) => {
2631
3131
  undo.push(() => lab.lift(kind, part, cuts));
2632
3132
  if (cell.stall !== void 0) {
2633
3133
  const t1 = fault.t1;
2634
- const sent = await waitFor(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
3134
+ const sent = await waitFor$1(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
2635
3135
  if (sent !== void 0) healing = lab.lift(kind, part, cuts, sent.t + valueIn(cell.stall, "s") * 1e9);
2636
3136
  } else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
2637
3137
  break;
@@ -2665,11 +3165,11 @@ const groupOnce = async (lab, cell, environment, index) => {
2665
3165
  const t0 = fault.t0;
2666
3166
  const old = addressOf(lab, primary);
2667
3167
  const deadline = deadlineOf(cell);
2668
- if ((lands === "new" ? await waitFor(client, (event) => event.ev === "ack" && event.t > t0 && typeof event["at"] === "string" && event["at"] !== old, deadline) : await (async () => {
3168
+ if ((lands === "new" ? await waitFor$1(client, (event) => event.ev === "ack" && event.t > t0 && typeof event["at"] === "string" && event["at"] !== old, deadline) : await (async () => {
2669
3169
  await healed;
2670
3170
  const after = heal?.t0 ?? t0;
2671
- return waitFor(client, (event) => event.ev === "ack" && event.t > after, deadline);
2672
- })()) !== void 0) await sleep$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);
2673
3173
  await healed;
2674
3174
  } finally {
2675
3175
  for (const step of undo.reverse()) await step().catch(() => void 0);
@@ -2702,91 +3202,727 @@ const groupOnce = async (lab, cell, environment, index) => {
2702
3202
  };
2703
3203
  };
2704
3204
  //#endregion
2705
- //#region src/run.ts
2706
- /**
2707
- * This function measures and judges the raw records of a cell, with the code of the harness at
2708
- * this time. `runCell` uses it during a cell. A holder of the raw records of a cell can use it
2709
- * again later. Thus the records get a new analysis after a change to observe.ts or to the verdict.
2710
- */
2711
- const judge = (cell, runs) => {
2712
- 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;
2713
3225
  return {
2714
- measured,
2715
- 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 ?? {}
2716
3234
  };
2717
3235
  };
2718
- /** This function gives the count of lost records in the latest clock line of the prober, since it began. */
2719
- const droppedSoFar = (records) => {
2720
- for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
2721
- return 0;
2722
- };
2723
- /** This function gives the argv of the client for a cell, with a deadline far after all that it waits for. */
2724
- const probeArgs = (cell, lab) => {
2725
- const { prediction, probe } = cell;
2726
- const longest = (prediction.detects ? valueIn(prediction.high, "s") : valueIn(prediction.window, "s")) + (cell.idleBefore === void 0 ? 0 : valueIn(cell.idleBefore, "s"));
2727
- const args = [
2728
- lab.targetIp,
2729
- String(lab.targetPort),
2730
- "--mode",
2731
- probe.mode
2732
- ];
2733
- if (probe.http !== void 0) {
2734
- const { http } = probe;
2735
- const gap = http.gap === void 0 ? 0 : valueIn(http.gap, "s");
2736
- args.push("--method", http.method);
2737
- if (http.gap !== void 0) args.push("--gap-ms", String(valueIn(http.gap, "ms")));
2738
- if (http.hold !== void 0) args.push("--hold-ms", String(valueIn(http.hold, "ms")));
2739
- if (http.bytes !== void 0) args.push("--bytes", String(http.bytes));
2740
- for (const [name, value] of Object.entries(http.options ?? {})) args.push(`--${name}`, String(value));
2741
- args.push("--linger-ms", String(LINGER_MS));
2742
- args.push("--max-s", String(Math.ceil(longest * 2 + gap + 10)));
2743
- return args;
2744
- }
2745
- if (probe.request !== void 0) args.push("--request", probe.request);
2746
- if (probe.count !== void 0) args.push("--count", String(probe.count));
2747
- if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
2748
- if (probe.driver !== void 0) {
2749
- args.push("--keepalive-ms", String(valueIn(probe.driver.keepAlive, "ms")));
2750
- args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
2751
- return args;
2752
- }
2753
- if (probe.keepalive) args.push("--keepalive");
2754
- if (probe.userTimeout !== void 0) args.push("--user-timeout-ms", String(valueIn(probe.userTimeout, "ms")));
2755
- args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
2756
- return args;
2757
- };
2758
3236
  /**
2759
- * This function waits for an error on the socket of the observer, or for its end of file
2760
- * (CLOSE_WAIT, state 8). The prober records both. A cell whose observer is the server, or a proxy
2761
- * of a topology, waits for that. The promise resolves after `within` ms at the latest.
3237
+ * This function gives the offset of the wall clock from the monotonic clock, in ns, from the clock
3238
+ * lines of a client. Each line pairs its stamp with the wall clock in whole ms, which truncates.
3239
+ * Thus each pair is the offset or less, and the largest pair is the nearest. It is null without a clock line.
2762
3240
  */
2763
- const observerError = (cell, partial, within) => new Promise((resolve) => {
2764
- const started = Date.now();
2765
- const look = () => {
2766
- const run = partial();
2767
- const at = socketOf(cell, run);
2768
- if (at !== void 0 && run.kernel.some((record) => record.netns === at.netns && record.cookie === at.cookie && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && record.new === 8)) || Date.now() - started > within) resolve();
2769
- else setTimeout(look, 50);
2770
- };
2771
- look();
2772
- });
2773
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3241
+ const clockOffsetOf = (events) => {
3242
+ const offsets = events.filter((event) => event.ev === "clocks").map((event) => Number(event["real"]) * 1e6 - event.t);
3243
+ return offsets.length === 0 ? null : Math.max(...offsets);
3244
+ };
3245
+ /** This function puts the log lines of the members on the monotonic clock, with the offset of a client. */
3246
+ const onMonotonic = (log, offset) => log.map((line) => ({
3247
+ ...line,
3248
+ t: offset === null ? 0 : Math.round(line.real * 1e6 - offset)
3249
+ }));
2774
3250
  /**
2775
- * This time is the pause between the client's first reply and the fault. It is more than Linux's
2776
- * TCP_DELACK_MAX of 200 ms. Thus the delayed ACK of the reply is out before 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.
2777
3255
  */
2778
- const QUIET_MS = 250;
3256
+ const cutOf = (set, members, client, part, cuts) => {
3257
+ const others = set.members.filter((name) => name !== part).map((name) => members[name].ip);
3258
+ if (cuts === "clients") return { peers: [client] };
3259
+ if (cuts === "group") return { peers: others };
3260
+ if (cuts === "one") return { peers: others.slice(0, 1) };
3261
+ if (cuts === "one-way") return {
3262
+ peers: others.slice(0, 1),
3263
+ direction: "out"
3264
+ };
3265
+ return { peers: [...others, client] };
3266
+ };
3267
+ /** This function gives the command of the server of each member. The server is the child of a shell, thus a fin can stop it and the container stays up. */
3268
+ const memberCommand = (set) => `${set.programs.server} --replSet ${set.name} --bind_ip_all --port ${set.port} ${(set.args ?? []).join(" ")}`.trim();
3269
+ /** This function gives the script of the shell that starts the set: each member by its address, and the settings of the set. */
3270
+ const initiateScript = (set, ips) => {
3271
+ const config = {
3272
+ _id: set.name,
3273
+ members: set.members.map((name, at) => ({
3274
+ _id: at,
3275
+ host: `${ips[name]}:${set.port}`
3276
+ })),
3277
+ settings: set.settings
3278
+ };
3279
+ return `print(JSON.stringify(rs.initiate(${JSON.stringify(config)})))`;
3280
+ };
3281
+ /** This script prints the view of a member: itself, if it accepts writes, and the state of each member as it sees them. */
3282
+ const STATUS_SCRIPT = "const s = rs.status(); const h = db.hello(); print(JSON.stringify({ me: h.me, writable: h.isWritablePrimary === true, members: s.members.map((m) => ({ name: m.name, state: m.stateStr, health: m.health, optime: m.optimeDate ? m.optimeDate.getTime() : null })) }))";
2779
3283
  /**
2780
- * This time is how long an HTTP client waits after its second request, before it exits. The exit
2781
- * closes its sockets, thus that close comes after the window of the cell.
3284
+ * This function judges the view of a member: the primary of the set, and if the set is healthy.
3285
+ * The set is healthy when the member is the one primary and accepts writes. Each other member must
3286
+ * be a healthy secondary at the optime of the primary.
2782
3287
  */
2783
- const LINGER_MS = 1e3;
2784
- /** These are the lines that end the workload of an HTTP client: the end of its second request, its deadline, or its failure. */
2785
- const HTTP_DONE = [
2786
- "done",
2787
- "deadline",
2788
- "fail"
2789
- ];
3288
+ const statusViewOf = (set, ips, status) => {
3289
+ const byAddress = new Map(set.members.map((name) => [`${ips[name]}:${set.port}`, name]));
3290
+ const primaries = status.members.filter((member) => member.state === "PRIMARY");
3291
+ if (primaries.length !== 1) return {
3292
+ primary: null,
3293
+ healthy: false,
3294
+ why: `the set has ${primaries.length} primaries`
3295
+ };
3296
+ const primary = byAddress.get(primaries[0].name) ?? null;
3297
+ if (primary === null) return {
3298
+ primary: null,
3299
+ healthy: false,
3300
+ why: `no member has the address ${primaries[0].name}`
3301
+ };
3302
+ if (status.me !== primaries[0].name || !status.writable) return {
3303
+ primary,
3304
+ healthy: false,
3305
+ why: `${primary} does not accept writes yet`
3306
+ };
3307
+ if (status.members.length !== set.members.length) return {
3308
+ primary,
3309
+ healthy: false,
3310
+ why: `the set has ${status.members.length} members`
3311
+ };
3312
+ for (const member of status.members) {
3313
+ if (member.health !== 1) return {
3314
+ primary,
3315
+ healthy: false,
3316
+ why: `${member.name} is not healthy`
3317
+ };
3318
+ if (member.state !== "PRIMARY" && member.state !== "SECONDARY") return {
3319
+ primary,
3320
+ healthy: false,
3321
+ why: `${member.name} is ${member.state}`
3322
+ };
3323
+ if (member.optime !== primaries[0].optime) return {
3324
+ primary,
3325
+ healthy: false,
3326
+ why: `${member.name} is behind the primary`
3327
+ };
3328
+ }
3329
+ return {
3330
+ primary,
3331
+ healthy: true,
3332
+ why: "healthy"
3333
+ };
3334
+ };
3335
+ const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3336
+ /** These are the arguments of replSetStepDown: the time that the old primary cannot stand again, and the wait for a secondary to catch up. */
3337
+ const STEP_DOWN = {
3338
+ seconds: 60,
3339
+ catchUp: 10
3340
+ };
3341
+ /**
3342
+ * This function opens one environment of a replica set cell:
3343
+ * - one new network, with each member, the client and the sender of a step-down on it.
3344
+ * - each member: the stock image, its server as the child of a shell. A log consumer reads its
3345
+ * JSON log. The first member starts the set with the address of each member, and the lab
3346
+ * waits for a primary.
3347
+ * - an injector sidecar for each member, in its network and process namespaces.
3348
+ * - a client that holds its own network namespace, with a privileged sidecar for its sysctls.
3349
+ * - the prober, which watches the namespace of the client.
3350
+ */
3351
+ const openReplicaSetLab = async (set) => {
3352
+ const image = await harnessImage();
3353
+ const mongo = await mongoClientImage();
3354
+ const { GenericContainer, Network, Wait, getContainerRuntimeClient } = await containers();
3355
+ const runtime = await getContainerRuntimeClient();
3356
+ const opened = [];
3357
+ let network;
3358
+ const close = async () => {
3359
+ for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
3360
+ await network?.stop().catch(() => void 0);
3361
+ };
3362
+ const ipOf = async (container) => {
3363
+ const ip = (await runtime.container.inspect(runtime.container.getById(container.getId()))).NetworkSettings.Networks[network.getName()]?.IPAddress;
3364
+ if (ip === void 0 || ip === "") throw new Error(`${container.getName()} has no address on ${network.getName()}`);
3365
+ return ip;
3366
+ };
3367
+ try {
3368
+ network = await new Network().start();
3369
+ const boxes = /* @__PURE__ */ new Map();
3370
+ const injectors = /* @__PURE__ */ new Map();
3371
+ const members = {};
3372
+ const log = [];
3373
+ for (const name of set.members) {
3374
+ let partial = "";
3375
+ const box = await new GenericContainer(set.image).withNetwork(network).withNetworkAliases(name).withCommand([
3376
+ "sh",
3377
+ "-c",
3378
+ `${memberCommand(set)} & echo $! > /tmp/server.pid; while :; do sleep 3600; done`
3379
+ ]).withWaitStrategy(Wait.forLogMessage(set.ready)).withLogConsumer((stream) => stream.on("data", (chunk) => {
3380
+ partial += chunk.toString();
3381
+ const split = partial.split("\n");
3382
+ partial = split.pop() ?? "";
3383
+ for (const text of split) {
3384
+ const line = memberLineOf(name, text.trim());
3385
+ if (line !== null) log.push(line);
3386
+ }
3387
+ })).start();
3388
+ opened.push(box);
3389
+ boxes.set(name, box);
3390
+ const injector = await withPid(new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${box.getId()}`).start();
3391
+ opened.push(injector);
3392
+ injectors.set(name, injector);
3393
+ members[name] = {
3394
+ ip: await ipOf(box),
3395
+ netns: await netnsOf(injector)
3396
+ };
3397
+ }
3398
+ const ips = Object.fromEntries(Object.entries(members).map(([name, at]) => [name, at.ip]));
3399
+ const holder = await new GenericContainer(image).withNetwork(network).start();
3400
+ opened.push(holder);
3401
+ const admin = await new GenericContainer(image).withNetworkMode(`container:${holder.getId()}`).withPrivilegedMode().start();
3402
+ opened.push(admin);
3403
+ const clientIp = await ipOf(holder);
3404
+ const commander = await new GenericContainer(mongo).withNetwork(network).withCommand([
3405
+ "sh",
3406
+ "-c",
3407
+ "while :; do sleep 3600; done"
3408
+ ]).start();
3409
+ opened.push(commander);
3410
+ const clientNetns = await netnsOf(admin);
3411
+ const records = lines();
3412
+ const prober = await new GenericContainer(image).withPrivilegedMode().withNetworkMode("none").withCommand(["/opt/harness/prober", String(clientNetns)]).withLogConsumer(records.consume).start();
3413
+ opened.push(prober);
3414
+ const begun = await records.until(["begin", "fail"], 3e4, "prober");
3415
+ if (begun.ev === "fail") throw new Error(`the prober failed: ${String(begun["why"])}`);
3416
+ const box = (name) => {
3417
+ const found = boxes.get(name);
3418
+ if (found === void 0) throw new Error(`the set has no member ${name}`);
3419
+ return found;
3420
+ };
3421
+ const injectorOf = (name) => {
3422
+ const found = injectors.get(name);
3423
+ if (found === void 0) throw new Error(`the set has no member ${name}`);
3424
+ return found;
3425
+ };
3426
+ /** This function starts a script of the shell on a member, against its own server only. */
3427
+ const shell = (name, script) => output(box(name), [
3428
+ set.programs.shell,
3429
+ "--quiet",
3430
+ "--norc",
3431
+ `mongodb://127.0.0.1:${set.port}/?directConnection=true&serverSelectionTimeoutMS=3000`,
3432
+ "--eval",
3433
+ script
3434
+ ]);
3435
+ const lastLine = (text) => text.split("\n").filter((line) => line.trim() !== "").at(-1) ?? "";
3436
+ const stamped = async (where, argv) => {
3437
+ const stamp = JSON.parse(lastLine(await output(where, argv)));
3438
+ return {
3439
+ t0: stamp.t0,
3440
+ t1: stamp.t1
3441
+ };
3442
+ };
3443
+ const after = (at) => at === void 0 ? [] : ["--at", String(Math.round(at))];
3444
+ let known = set.members[0];
3445
+ /** This function gives the view of the set: from the member that was the primary last, then from each other member. */
3446
+ const view = async () => {
3447
+ let last = {
3448
+ primary: null,
3449
+ healthy: false,
3450
+ why: "no member answered"
3451
+ };
3452
+ for (const name of [known, ...set.members.filter((each) => each !== known)]) {
3453
+ const read = await shell(name, STATUS_SCRIPT).then((text) => statusViewOf(set, ips, JSON.parse(lastLine(text)))).catch((error) => ({
3454
+ primary: null,
3455
+ healthy: false,
3456
+ why: `${name}: ${error.message.slice(0, 200)}`
3457
+ }));
3458
+ if (read.primary !== null && read.primary === name) return read;
3459
+ last = read;
3460
+ }
3461
+ return last;
3462
+ };
3463
+ const converge = async (within = CONVERGE_MS) => {
3464
+ const started = Date.now();
3465
+ let calm = 0;
3466
+ let last = {
3467
+ primary: null,
3468
+ healthy: false,
3469
+ why: "no view yet"
3470
+ };
3471
+ while (Date.now() - started < within) {
3472
+ last = await view();
3473
+ calm = last.healthy ? calm + 1 : 0;
3474
+ if (last.primary !== null) known = last.primary;
3475
+ if (calm >= 2 && last.primary !== null) return last.primary;
3476
+ await sleep$1(1e3);
3477
+ }
3478
+ throw new Error(`the set did not converge in ${within} ms: ${last.why}`);
3479
+ };
3480
+ const initiated = await shell(set.members[0], initiateScript(set, ips));
3481
+ if (!lastLine(initiated).includes("\"ok\":1")) throw new Error(`rs.initiate failed: ${lastLine(initiated)}`);
3482
+ await converge();
3483
+ return {
3484
+ set,
3485
+ clientIp,
3486
+ members,
3487
+ netns: { client: clientNetns },
3488
+ kernel: records.seen,
3489
+ log,
3490
+ async sysctl(values) {
3491
+ const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
3492
+ if (settings.length > 0) await output(admin, [
3493
+ "sysctl",
3494
+ "-q",
3495
+ "-w",
3496
+ ...settings
3497
+ ]);
3498
+ const read = {};
3499
+ for (const key of Object.keys(values)) read[key] = await output(admin, [
3500
+ "sysctl",
3501
+ "-n",
3502
+ key
3503
+ ]);
3504
+ return read;
3505
+ },
3506
+ converge,
3507
+ async flush(primary) {
3508
+ const script = `const r = db.getSiblingDB("cells").w.deleteMany({}, { writeConcern: { w: ${set.members.length}, wtimeout: 10000 } }); print(JSON.stringify({ ok: r.acknowledged === true }))`;
3509
+ const reply = lastLine(await shell(primary, script));
3510
+ if (!reply.includes("\"ok\":true")) throw new Error(`the flush failed: ${reply}`);
3511
+ },
3512
+ async unfreeze(primary) {
3513
+ for (const name of set.members) {
3514
+ if (name === primary) continue;
3515
+ const reply = lastLine(await shell(name, "print(JSON.stringify(rs.freeze(0)))"));
3516
+ if (!reply.includes("\"ok\":1")) throw new Error(`${name} did not unfreeze: ${reply}`);
3517
+ }
3518
+ },
3519
+ async holds(primary) {
3520
+ return JSON.parse(lastLine(await shell(primary, "print(JSON.stringify(db.getSiblingDB(\"cells\").w.find({}, { n: 1, _id: 0 }).toArray().map((d) => d.n)))"))).map(Number).filter((n) => Number.isInteger(n) && n > 0).sort((a, b) => a - b);
3521
+ },
3522
+ partOf(role, primary) {
3523
+ if (role === "primary") return primary;
3524
+ if (role === "sentinel") throw new Error("a replica set has no Sentinel");
3525
+ const secondary = set.members.find((name) => name !== primary);
3526
+ if (secondary === void 0) throw new Error("the set has no secondary");
3527
+ return secondary;
3528
+ },
3529
+ async client(args) {
3530
+ const events = lines();
3531
+ const container = await new GenericContainer(mongo).withNetworkMode(`container:${holder.getId()}`).withCommand([
3532
+ "node",
3533
+ "/opt/harness/mongo/client-mongo.mjs",
3534
+ ...args
3535
+ ]).withLogConsumer(events.consume).start();
3536
+ return {
3537
+ events: events.seen,
3538
+ until: (kinds, within) => events.until(kinds, within, "client"),
3539
+ stop: () => container.stop({ timeout: 2 }).then(() => void 0).catch(() => void 0)
3540
+ };
3541
+ },
3542
+ fault(kind, part, cuts, at) {
3543
+ const cut = cutOf(set, members, clientIp, part, cuts);
3544
+ const direction = cut.direction === void 0 ? [] : ["--direction", cut.direction];
3545
+ return stamped(injectorOf(part), [
3546
+ "/opt/harness/inject",
3547
+ kind,
3548
+ cut.peers.join(","),
3549
+ ...direction,
3550
+ ...after(at)
3551
+ ]);
3552
+ },
3553
+ lift(kind, part, cuts, at) {
3554
+ const cut = cutOf(set, members, clientIp, part, cuts);
3555
+ const direction = cut.direction === void 0 ? [] : ["--direction", cut.direction];
3556
+ return stamped(injectorOf(part), [
3557
+ "/opt/harness/inject",
3558
+ kind === "rst" ? "clear-rst" : "clear",
3559
+ cut.peers.join(","),
3560
+ ...direction,
3561
+ ...after(at)
3562
+ ]);
3563
+ },
3564
+ signal(part, signal, at) {
3565
+ return stamped(injectorOf(part), [
3566
+ "/opt/harness/inject",
3567
+ "signal",
3568
+ set.programs.server,
3569
+ signal,
3570
+ ...after(at)
3571
+ ]);
3572
+ },
3573
+ async restart(member) {
3574
+ await output(box(member), [
3575
+ "sh",
3576
+ "-c",
3577
+ `${memberCommand(set)} > /proc/1/fd/1 2>&1 & echo $! > /tmp/server.pid`
3578
+ ]);
3579
+ const started = Date.now();
3580
+ while (!(await shell(member, "print(db.hello().ok)").catch(() => "")).includes("1")) {
3581
+ if (Date.now() - started > 6e4) throw new Error(`${member} did not start again`);
3582
+ await sleep$1(500);
3583
+ }
3584
+ },
3585
+ stepDown(primary) {
3586
+ return stamped(commander, [
3587
+ "node",
3588
+ "/opt/harness/mongo/stepdown-mongo.mjs",
3589
+ `${members[primary].ip}:${set.port}`,
3590
+ String(STEP_DOWN.seconds),
3591
+ String(STEP_DOWN.catchUp)
3592
+ ]);
3593
+ },
3594
+ async forget() {
3595
+ await output(admin, [
3596
+ "ip",
3597
+ "tcp_metrics",
3598
+ "flush",
3599
+ "all"
3600
+ ]);
3601
+ },
3602
+ async settle() {
3603
+ const from = records.seen.length;
3604
+ const clock = await new Promise((resolve) => {
3605
+ const started = Date.now();
3606
+ const look = () => {
3607
+ const line = records.seen.slice(from).find((record) => record.ev === "clock");
3608
+ if (line !== void 0) resolve(line);
3609
+ else if (Date.now() - started > 5e3) resolve(null);
3610
+ else setTimeout(look, 50);
3611
+ };
3612
+ look();
3613
+ });
3614
+ return clock === null ? null : Number(clock["lost"] ?? 0);
3615
+ },
3616
+ async manifest(sysctls) {
3617
+ const config = await output(admin, ["zcat", "/proc/config.gz"]).catch(() => "");
3618
+ const inspect = async (container) => runtime.container.inspect(runtime.container.getById(container.getId()));
3619
+ const script = "print(JSON.stringify({ version: db.version(), settings: rs.conf().settings }))";
3620
+ const list = [];
3621
+ for (const name of set.members) {
3622
+ const read = JSON.parse(lastLine(await shell(name, script)));
3623
+ const settings = { version: read.version };
3624
+ for (const key of Object.keys(set.settings)) settings[key] = String(read.settings[key] ?? "");
3625
+ list.push({
3626
+ name,
3627
+ ip: members[name].ip,
3628
+ id: (await inspect(box(name))).Image,
3629
+ settings
3630
+ });
3631
+ }
3632
+ return {
3633
+ kernel: await output(holder, ["uname", "-r"]),
3634
+ architecture: await output(holder, ["uname", "-m"]),
3635
+ hz: /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1] ?? null,
3636
+ boot_id: await output(holder, ["cat", "/proc/sys/kernel/random/boot_id"]),
3637
+ docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
3638
+ target: {
3639
+ image: set.image,
3640
+ id: list[0].id,
3641
+ settings: list[0].settings,
3642
+ sysctls: {}
3643
+ },
3644
+ harness: {
3645
+ image,
3646
+ id: (await inspect(holder)).Image,
3647
+ packages: (await output(holder, [
3648
+ "apk",
3649
+ "info",
3650
+ "-v"
3651
+ ])).split("\n").sort()
3652
+ },
3653
+ client: {
3654
+ kind: "mongodb",
3655
+ image: mongo
3656
+ },
3657
+ netns: {
3658
+ client: clientNetns,
3659
+ target: members[set.members[0]].netns
3660
+ },
3661
+ sysctls,
3662
+ group: {
3663
+ members: list,
3664
+ sentinels: [],
3665
+ observer: { image: mongo }
3666
+ }
3667
+ };
3668
+ },
3669
+ close
3670
+ };
3671
+ } catch (error) {
3672
+ await close();
3673
+ throw error;
3674
+ }
3675
+ };
3676
+ /** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
3677
+ const STEADY = 20;
3678
+ /** This time is how long a run continues after its outcome, so that the record shows what came next. */
3679
+ const TAIL_MS$1 = 2e3;
3680
+ /** This function gives the arguments of the client of a replica set cell: each member, and the stream of writes. */
3681
+ const replicaSetArgs = (cell, members) => {
3682
+ const set = cell.replicaSet;
3683
+ const hosts = set.members.map((name) => `${members[name].ip}:${set.port}`);
3684
+ return [
3685
+ members[set.members[0]].ip,
3686
+ String(set.port),
3687
+ "--mode",
3688
+ "stream",
3689
+ "--hosts",
3690
+ hosts.join(","),
3691
+ "--set",
3692
+ set.name,
3693
+ "--interval-ms",
3694
+ String(cell.probe.interval === void 0 ? 50 : valueIn(cell.probe.interval, "ms")),
3695
+ "--max-s",
3696
+ String(Math.ceil(deadlineOf(cell) / 1e3 + (cell.hold === void 0 ? 0 : valueIn(cell.hold, "s")) + 120)),
3697
+ "--w",
3698
+ String(cell.writeConcern ?? "majority"),
3699
+ ...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)]
3700
+ ];
3701
+ };
3702
+ /** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
3703
+ const waitFor = (client, match, within) => new Promise((resolve) => {
3704
+ const started = Date.now();
3705
+ const look = () => {
3706
+ const found = client.events.find(match);
3707
+ if (found !== void 0 || Date.now() - started > within) resolve(found);
3708
+ else setTimeout(look, 20);
3709
+ };
3710
+ look();
3711
+ });
3712
+ /** This function gives the count of lost records in the latest clock line of the prober. */
3713
+ const droppedSoFar$1 = (records) => {
3714
+ for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
3715
+ return 0;
3716
+ };
3717
+ /**
3718
+ * This function does one run of a replica set cell:
3719
+ * - The set converges, and its writes go. The client finds the primary with its monitors, and its
3720
+ * stream of writes gets steady.
3721
+ * - The fault occurs on its member, as the primary of this run makes the roles. A fault with a
3722
+ * hold lifts at its instant in the VM. A stall lifts at its length after the first write into
3723
+ * it. A step-down is replSetStepDown to the primary.
3724
+ * - The run waits for its outcome: the first accepted write of a write sent after the commit of
3725
+ * the fault. It is on a new primary, or on any primary. Then the run waits for the end of the
3726
+ * hold, thus the record shows what the set did in the whole hold.
3727
+ * - Each fault goes, the client stops, and the set converges again. The writes that it then holds
3728
+ * tell which writes it kept. The log lines of the members go onto the monotonic clock.
3729
+ */
3730
+ const replicaSetOnce = async (lab, cell, environment, index) => {
3731
+ const set = cell.replicaSet;
3732
+ const primary = await lab.converge();
3733
+ await lab.flush(primary);
3734
+ await lab.unfreeze(primary);
3735
+ await lab.forget();
3736
+ const from = lab.kernel.length;
3737
+ const logFrom = lab.log.length;
3738
+ const lostBefore = droppedSoFar$1(lab.kernel);
3739
+ const client = await lab.client(replicaSetArgs(cell, lab.members));
3740
+ const lands = cell.prediction.group?.lands ?? "new";
3741
+ const place = cell.faultAt ?? { role: "primary" };
3742
+ let fault = null;
3743
+ let heal = null;
3744
+ let at = null;
3745
+ const undo = [];
3746
+ try {
3747
+ await client.until(["ready"], 6e4);
3748
+ if (await waitFor(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY, 3e4) === void 0) throw new Error(`the client did not write ${STEADY} times in 30 s`);
3749
+ let healing = null;
3750
+ switch (cell.fault) {
3751
+ case "blackhole":
3752
+ case "partition":
3753
+ case "rst": {
3754
+ const kind = cell.fault === "rst" ? "rst" : "blackhole";
3755
+ const cuts = place.cuts ?? (cell.fault === "partition" ? "group" : "all");
3756
+ const part = lab.partOf(place.role, primary);
3757
+ at = part;
3758
+ fault = await lab.fault(kind, part, cuts);
3759
+ undo.push(() => lab.lift(kind, part, cuts));
3760
+ if (cell.stall !== void 0) {
3761
+ const t1 = fault.t1;
3762
+ const sent = await waitFor(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
3763
+ healing = lab.lift(kind, part, cuts, (sent?.t ?? t1) + valueIn(cell.stall, "s") * 1e9);
3764
+ } else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
3765
+ break;
3766
+ }
3767
+ case "freeze-process": {
3768
+ const part = lab.partOf(place.role, primary);
3769
+ at = part;
3770
+ fault = await lab.signal(part, "STOP");
3771
+ undo.push(() => lab.signal(part, "CONT"));
3772
+ if (cell.hold !== void 0) healing = lab.signal(part, "CONT", fault.t0 + valueIn(cell.hold, "s") * 1e9);
3773
+ break;
3774
+ }
3775
+ case "fin": {
3776
+ const part = lab.partOf(place.role, primary);
3777
+ at = part;
3778
+ fault = await lab.signal(part, "KILL");
3779
+ undo.push(() => lab.restart(part));
3780
+ break;
3781
+ }
3782
+ case "step-down":
3783
+ at = primary;
3784
+ fault = await lab.stepDown(primary);
3785
+ break;
3786
+ default: throw new Error(`a replica set cell has no fault ${cell.fault}`);
3787
+ }
3788
+ const healed = healing === null ? Promise.resolve(null) : healing.then((stamp) => {
3789
+ heal = stamp;
3790
+ undo.length = 0;
3791
+ return stamp;
3792
+ });
3793
+ healed.catch(() => void 0);
3794
+ const t1 = fault.t1;
3795
+ const old = `${lab.members[primary].ip}:${set.port}`;
3796
+ const sentAt = (n) => client.events.find((event) => event.ev === "sent" && Number(event["n"]) === n)?.t ?? -Infinity;
3797
+ if (await waitFor(client, (event) => event.ev === "ack" && sentAt(Number(event["n"])) > t1 && (lands === "same" || typeof event["at"] === "string" && event["at"] !== old), deadlineOf(cell)) !== void 0) await sleep$1(TAIL_MS$1);
3798
+ if (healing !== null) {
3799
+ await healed.catch(() => void 0);
3800
+ await sleep$1(TAIL_MS$1);
3801
+ }
3802
+ } finally {
3803
+ for (const step of undo.reverse()) await step().catch(() => void 0);
3804
+ await client.stop();
3805
+ }
3806
+ const settled = await lab.settle();
3807
+ const lost = settled === null ? null : settled - lostBefore;
3808
+ const after = await lab.converge().catch(() => null);
3809
+ const holds = after === null ? null : await lab.holds(after);
3810
+ return {
3811
+ environment,
3812
+ index,
3813
+ events: [...client.events],
3814
+ fault,
3815
+ ...heal === null ? {} : { heal },
3816
+ kernel: lab.kernel.slice(from),
3817
+ netns: lab.netns.client,
3818
+ targetNetns: lab.members[primary].netns,
3819
+ lost,
3820
+ group: {
3821
+ members: lab.members,
3822
+ sentinels: {},
3823
+ primary,
3824
+ at,
3825
+ timeline: onMonotonic(lab.log.slice(logFrom), clockOffsetOf(client.events)),
3826
+ after,
3827
+ holds
3828
+ }
3829
+ };
3830
+ };
3831
+ //#endregion
3832
+ //#region src/run.ts
3833
+ /**
3834
+ * This function measures and judges the raw records of a cell, with the code of the harness at
3835
+ * this time. `runCell` uses it during a cell. A holder of the raw records of a cell can use it
3836
+ * again later. Thus the records get a new analysis after a change to observe.ts or to the verdict.
3837
+ */
3838
+ const judge = (cell, runs) => {
3839
+ const measured = runs.filter((run) => run.warmup !== true).map((run) => measure(cell, run));
3840
+ return {
3841
+ measured,
3842
+ verdict: verdictOf(cell.prediction, measured)
3843
+ };
3844
+ };
3845
+ /** This function gives the count of lost records in the latest clock line of the prober, since it began. */
3846
+ const droppedSoFar = (records) => {
3847
+ for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
3848
+ return 0;
3849
+ };
3850
+ /** This function gives the argv of the client for a cell, with a deadline far after all that it waits for. */
3851
+ const probeArgs = (cell, lab) => {
3852
+ const { prediction, probe } = cell;
3853
+ const longest = (prediction.detects === true && prediction.driver !== void 0 ? longestOf(cell) : prediction.detects ? valueIn(prediction.high, "s") : valueIn(prediction.window, "s")) + (cell.idleBefore === void 0 ? 0 : valueIn(cell.idleBefore, "s"));
3854
+ const args = [
3855
+ lab.targetIp,
3856
+ String(lab.targetPort),
3857
+ "--mode",
3858
+ probe.mode
3859
+ ];
3860
+ if (probe.http !== void 0) {
3861
+ const { http } = probe;
3862
+ const gap = http.gap === void 0 ? 0 : valueIn(http.gap, "s");
3863
+ args.push("--method", http.method);
3864
+ if (http.gap !== void 0) args.push("--gap-ms", String(valueIn(http.gap, "ms")));
3865
+ if (http.hold !== void 0) args.push("--hold-ms", String(valueIn(http.hold, "ms")));
3866
+ if (http.bytes !== void 0) args.push("--bytes", String(http.bytes));
3867
+ for (const [name, value] of Object.entries(http.options ?? {})) args.push(`--${name}`, String(value));
3868
+ args.push("--linger-ms", String(LINGER_MS));
3869
+ args.push("--max-s", String(Math.ceil(longest * 2 + gap + 10)));
3870
+ return args;
3871
+ }
3872
+ if (probe.request !== void 0) args.push("--request", probe.request);
3873
+ if (probe.count !== void 0) args.push("--count", String(probe.count));
3874
+ if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
3875
+ if (probe.driver?.name === "mongodb") {
3876
+ args.push("--set", probe.driver.set);
3877
+ if (probe.driver.op !== void 0) args.push("--op", probe.driver.op);
3878
+ if (cell.writeConcern !== void 0) args.push("--w", String(cell.writeConcern));
3879
+ if (cell.before === true) args.push("--before");
3880
+ if (cell.driverOptions !== void 0) args.push("--options", JSON.stringify(cell.driverOptions));
3881
+ args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
3882
+ return args;
3883
+ }
3884
+ if (probe.driver !== void 0) {
3885
+ args.push("--keepalive-ms", String(valueIn(probe.driver.keepAlive, "ms")));
3886
+ args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
3887
+ return args;
3888
+ }
3889
+ if (probe.keepalive) args.push("--keepalive");
3890
+ if (probe.userTimeout !== void 0) args.push("--user-timeout-ms", String(valueIn(probe.userTimeout, "ms")));
3891
+ args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
3892
+ return args;
3893
+ };
3894
+ /**
3895
+ * This function waits for an error on the socket of the observer, or for its end of file
3896
+ * (CLOSE_WAIT, state 8). The prober records both. A cell whose observer is the server, or a proxy
3897
+ * of a topology, waits for that. The promise resolves after `within` ms at the latest.
3898
+ */
3899
+ const observerError = (cell, partial, within) => new Promise((resolve) => {
3900
+ const started = Date.now();
3901
+ const look = () => {
3902
+ const run = partial();
3903
+ const at = socketOf(cell, run);
3904
+ if (at !== void 0 && run.kernel.some((record) => record.netns === at.netns && record.cookie === at.cookie && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && record.new === 8)) || Date.now() - started > within) resolve();
3905
+ else setTimeout(look, 50);
3906
+ };
3907
+ look();
3908
+ });
3909
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3910
+ /**
3911
+ * This time is the pause between the client's first reply and the fault. It is more than Linux's
3912
+ * TCP_DELACK_MAX of 200 ms. Thus the delayed ACK of the reply is out before the fault.
3913
+ */
3914
+ const QUIET_MS = 250;
3915
+ /**
3916
+ * This time is how long an HTTP client waits after its second request, before it exits. The exit
3917
+ * closes its sockets, thus that close comes after the window of the cell.
3918
+ */
3919
+ const LINGER_MS = 1e3;
3920
+ /** These are the lines that end the workload of an HTTP client: the end of its second request, its deadline, or its failure. */
3921
+ const HTTP_DONE = [
3922
+ "done",
3923
+ "deadline",
3924
+ "fail"
3925
+ ];
2790
3926
  /**
2791
3927
  * This function waits for the request that an in-flight client sends on the signal: its second
2792
3928
  * send, after the PING that made it ready. Its stamp is when the request first met the fault.
@@ -2801,6 +3937,69 @@ const requestSent = (probe, within) => new Promise((resolve, reject) => {
2801
3937
  };
2802
3938
  look();
2803
3939
  });
3940
+ /** This time is how long a run of a driver cell continues after its outcome, so that the record shows what came next. */
3941
+ const TAIL_MS = 1e3;
3942
+ /** This function waits for the outcome of a run of a driver cell, or for `within` ms. The outcome is the end of its band and of each later band. */
3943
+ const driverOutcome = async (cell, current, within) => {
3944
+ const started = Date.now();
3945
+ while (Date.now() - started < within) {
3946
+ if (settled(cell, current())) return;
3947
+ await sleep(250);
3948
+ }
3949
+ };
3950
+ /** This function gives the longest wait of a run of a driver cell for its outcome, in ms. */
3951
+ const driverWithin = (cell) => (longestOf(cell) * 1.2 + 10) * 1e3;
3952
+ /**
3953
+ * This function does one run of a cell whose fault holds before the client starts (`Cell.before`).
3954
+ * The fault is a network fault at its place. The client then starts into it, and the run waits for
3955
+ * each band of the driver. Then the fault lifts.
3956
+ */
3957
+ const beforeOnce = async (lab, cell, environment, index) => {
3958
+ if (cell.fault !== "rst" && cell.fault !== "blackhole" && cell.fault !== "unreachable") throw new Error(`${cell.name}: a fault before the client starts is a network fault, not ${cell.fault}`);
3959
+ await lab.forget();
3960
+ const from = lab.kernel.length;
3961
+ const middleboxFrom = lab.middlebox.length;
3962
+ const lostBefore = droppedSoFar(lab.kernel);
3963
+ const faultAt = cell.faultAt ?? "target";
3964
+ const kind = cell.fault === "unreachable" ? "reject" : cell.fault;
3965
+ const options = cell.fault === "unreachable" ? { code: cell.icmp ?? "host" } : {};
3966
+ const fault = await lab.fault(kind, faultAt, options);
3967
+ let probe;
3968
+ try {
3969
+ probe = await lab.probe(probeArgs(cell, lab));
3970
+ const started = probe;
3971
+ const current = () => ({
3972
+ environment,
3973
+ index,
3974
+ events: started.events,
3975
+ fault,
3976
+ kernel: lab.kernel.slice(from),
3977
+ netns: lab.netns.client,
3978
+ targetNetns: lab.netns.target,
3979
+ lost: 0
3980
+ });
3981
+ await driverOutcome(cell, current, driverWithin(cell));
3982
+ await sleep(TAIL_MS);
3983
+ } finally {
3984
+ await lab.lift(kind, faultAt, options);
3985
+ await probe?.stop();
3986
+ }
3987
+ const settled = await lab.settle();
3988
+ return {
3989
+ environment,
3990
+ index,
3991
+ events: [...probe?.events ?? []],
3992
+ fault,
3993
+ kernel: lab.kernel.slice(from),
3994
+ netns: lab.netns.client,
3995
+ targetNetns: lab.netns.target,
3996
+ lost: settled === null ? null : settled - lostBefore,
3997
+ ...lab.topology === null ? {} : {
3998
+ topology: lab.topology,
3999
+ middlebox: lab.middlebox.slice(middleboxFrom)
4000
+ }
4001
+ };
4002
+ };
2804
4003
  /** This function gives the netem specification of a cell: its latency, then the netem of its fault when it has one. */
2805
4004
  const netemSpec = (cell, fault) => {
2806
4005
  const parts = [...cell.latency === void 0 ? [] : [`delay ${valueIn(cell.latency, "ms")}ms`], ...fault === void 0 ? [] : [fault]];
@@ -2820,6 +4019,7 @@ const netemSpec = (cell, fault) => {
2820
4019
  * A dry run does all of this but the fault: the warm-up of a cell whose fault ends its environment.
2821
4020
  */
2822
4021
  const once = async (lab, cell, environment, index, dry = false) => {
4022
+ if (cell.before === true) return beforeOnce(lab, cell, environment, index);
2823
4023
  await lab.forget();
2824
4024
  const from = lab.kernel.length;
2825
4025
  const middleboxFrom = lab.middlebox.length;
@@ -2853,6 +4053,8 @@ const once = async (lab, cell, environment, index, dry = false) => {
2853
4053
  });
2854
4054
  let fault = null;
2855
4055
  let heal = null;
4056
+ const signalled = !server && cell.target.program !== void 0;
4057
+ let thawing = null;
2856
4058
  try {
2857
4059
  await probe.until(["ready"], 15e3);
2858
4060
  if (dry) {
@@ -2879,6 +4081,17 @@ const once = async (lab, cell, environment, index, dry = false) => {
2879
4081
  const apply = async () => {
2880
4082
  switch (cell.fault) {
2881
4083
  case "freeze-process":
4084
+ if (signalled) {
4085
+ const stamp = await lab.signal("STOP");
4086
+ if (cell.hold !== void 0) {
4087
+ thawing = lab.signal("CONT", stamp.t0 + valueIn(cell.hold, "s") * 1e9).then((thawed) => {
4088
+ heal = thawed;
4089
+ return thawed;
4090
+ });
4091
+ thawing.catch(() => void 0);
4092
+ }
4093
+ return stamp;
4094
+ }
2882
4095
  await peer.freeze();
2883
4096
  return null;
2884
4097
  case "fin":
@@ -2904,7 +4117,13 @@ const once = async (lab, cell, environment, index, dry = false) => {
2904
4117
  };
2905
4118
  const lift = async () => {
2906
4119
  switch (cell.fault) {
2907
- case "freeze-process": 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();
2908
4127
  case "fin":
2909
4128
  case "middlebox-forget":
2910
4129
  case "proxy-idle":
@@ -2930,7 +4149,14 @@ const once = async (lab, cell, environment, index, dry = false) => {
2930
4149
  if ((cell.probe.mode === "inflight" || cell.probe.mode === "stream") && !replyInFlight) await go();
2931
4150
  const { prediction } = cell;
2932
4151
  const http = cell.probe.http !== void 0;
2933
- if (cell.probe.mode === "gap") {
4152
+ if (prediction.detects !== null && prediction.driver !== void 0) {
4153
+ if (prediction.detects) await driverOutcome(cell, () => ({
4154
+ ...partial(),
4155
+ fault
4156
+ }), driverWithin(cell));
4157
+ else await sleep(valueIn(prediction.window, "s") * 1e3);
4158
+ await sleep(TAIL_MS);
4159
+ } else if (cell.probe.mode === "gap") {
2934
4160
  const gap = cell.probe.http?.gap === void 0 ? 0 : valueIn(cell.probe.http.gap, "s");
2935
4161
  const window = valueIn(prediction.detects ? prediction.high : prediction.window, "s");
2936
4162
  await probe.until(HTTP_DONE, (gap + window) * 2e3 + 1e4).catch(() => void 0);
@@ -2979,7 +4205,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
2979
4205
  index,
2980
4206
  events: [...probe.events],
2981
4207
  fault,
2982
- ...cell.fault === "stall" ? { heal } : {},
4208
+ ...cell.fault === "stall" || thawing !== null ? { heal } : {},
2983
4209
  kernel: lab.kernel.slice(from),
2984
4210
  netns: lab.netns.client,
2985
4211
  targetNetns: lab.netns.target,
@@ -3003,6 +4229,7 @@ const line = (cell, run, read) => `${cell.name}: env ${run.environment} ${run.wa
3003
4229
  */
3004
4230
  const inEnvironment = async (cell, environment, count, say) => {
3005
4231
  if (cell.group !== void 0) return inGroupEnvironment(cell, environment, count, say);
4232
+ if (cell.replicaSet !== void 0) return inReplicaSetEnvironment(cell, environment, count, say);
3006
4233
  const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
3007
4234
  try {
3008
4235
  const settings = await lab.configure(cell.targetSettings ?? {});
@@ -3067,6 +4294,34 @@ const inGroupEnvironment = async (cell, environment, count, say) => {
3067
4294
  }
3068
4295
  };
3069
4296
  /**
4297
+ * This function does the part of the runs of a replica set cell in one environment. The warm-up
4298
+ * is a whole run with its fault, as for a group under Sentinels. The set converges before each run.
4299
+ */
4300
+ const inReplicaSetEnvironment = async (cell, environment, count, say) => {
4301
+ const lab = await openReplicaSetLab(cell.replicaSet);
4302
+ try {
4303
+ const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
4304
+ const warmup = {
4305
+ ...await replicaSetOnce(lab, cell, environment, -1),
4306
+ warmup: true
4307
+ };
4308
+ say(line(cell, warmup, measure(cell, warmup)));
4309
+ const runs = [];
4310
+ for (let index = 0; index < count; index += 1) {
4311
+ const run = await replicaSetOnce(lab, cell, environment, index);
4312
+ runs.push(run);
4313
+ say(line(cell, run, measure(cell, run)));
4314
+ }
4315
+ return {
4316
+ manifest,
4317
+ runs,
4318
+ warmup
4319
+ };
4320
+ } finally {
4321
+ await lab.close();
4322
+ }
4323
+ };
4324
+ /**
3070
4325
  * This function does the runs of a cell. They go across new environments, and each environment records
3071
4326
  * its manifest. Then the verdict judges the runs. A median across the edge of the band extends
3072
4327
  * the cell from 11 runs to 22, and then to 44, each extension in new environments. With a
@@ -3115,6 +4370,6 @@ const runCell = async (cell, say = () => {}) => {
3115
4370
  }
3116
4371
  };
3117
4372
  //#endregion
3118
- export { CONVERGE_MS, LINGER_MS, LISTING, NOISE, PUMP, QUIET_MS, RELATIVE, STAGES, STEPS, crossingsOf, deadlineOf, deviceOf, endpointAfter, errorsOf, fieldsOf, goHttpImage, groupOnce, groupVerdictOf, harnessImage, infoOf, isHopPlace, judge, lagsOf, measure, measureGroup, medianInterval, memberConfig, nodeClientImage, nodeHttpImage, openGroupLab, openLab, peersOf, quantileOf, reorderingOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketOf, spread, spreadOf, stagesOf, subscriptionsOf, templated, toxicBody, verdictOf, viewOf, writesOf };
4373
+ export { CONVERGE_MS, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, NOISE, PUMP, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, clockOffsetOf, crossingsOf, cutOf, deadlineOf, deviceOf, endpointAfter, errorsOf, fieldOf, fieldsOf, goHttpImage, groupOnce, groupVerdictOf, harnessImage, infoOf, initiateScript, isHopPlace, judge, lagsOf, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, nodeClientImage, nodeHttpImage, onMonotonic, openGroupLab, openLab, openReplicaSetLab, peersOf, quantileOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subscriptionsOf, templated, toxicBody, verdictOf, viewOf, writesOf };
3119
4374
 
3120
4375
  //# sourceMappingURL=index.js.map