@mark1russell7/harness 0.4.1 → 0.6.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
@@ -5,7 +5,13 @@ import { createRequire } from "node:module";
5
5
  import { tmpdir } from "node:os";
6
6
  import { dirname, join, relative } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ //#region src/cell.ts
9
+ /** This function tells if a fault place is a hop of a topology. */
10
+ const isHopPlace = (place) => typeof place === "object" && "hop" in place;
11
+ //#endregion
8
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"}`;
9
15
  /**
10
16
  * This amount is the noise of the pipeline itself, δ_abs, as the calibration measured it. It is
11
17
  * the 99.9th percentile of the whole latency of a reset, at the concurrency cap or less. Its
@@ -111,6 +117,112 @@ const middle = (sorted) => {
111
117
  * which does not exist at this time.
112
118
  */
113
119
  const verdictOf = (prediction, runs) => {
120
+ const band = verdictOfBand(prediction, runs);
121
+ const counted = runs.filter((run) => run.void === null);
122
+ const verdict = counted.some((run) => run.request !== void 0) ? withRequests(band, counted) : band;
123
+ if (prediction.group === void 0 && !counted.some((run) => run.group !== void 0)) return verdict;
124
+ return {
125
+ ...verdict,
126
+ group: groupVerdictOf(prediction, counted)
127
+ };
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
+ };
146
+ /** This function gives the median of some counts, or null for none. */
147
+ const medianCount = (values) => values.length === 0 ? null : middle([...values].sort((a, b) => a - b));
148
+ /** This function sums what the group of a group cell did in its counted runs, and tests each stage that has a band. */
149
+ const groupVerdictOf = (prediction, counted) => {
150
+ const noise = valueIn(NOISE, "s");
151
+ const stamps = prediction.group?.stamps === void 0 ? 0 : valueIn(prediction.group.stamps, "s");
152
+ const stages = (prediction.group?.stages ?? []).map((band) => {
153
+ const interval = {
154
+ low: s(valueIn(band.low, "s") - noise),
155
+ high: s(valueIn(band.high, "s") + stamps + noise)
156
+ };
157
+ const outside = [];
158
+ const values = [];
159
+ for (const run of counted) {
160
+ const at = run.group?.stages[band.stage];
161
+ if (at === void 0) {
162
+ outside.push({
163
+ run: name(run),
164
+ why: `no Sentinel announced ${band.stage}`
165
+ });
166
+ continue;
167
+ }
168
+ const value = valueIn(at, "s");
169
+ values.push(value);
170
+ if (value < valueIn(interval.low, "s") || value > valueIn(interval.high, "s")) outside.push({
171
+ run: name(run),
172
+ why: `${band.stage} at ${value} s is outside [${interval.low.value}, ${interval.high.value}] s`
173
+ });
174
+ }
175
+ values.sort((a, b) => a - b);
176
+ return {
177
+ stage: band.stage,
178
+ band: interval,
179
+ median: values.length === 0 ? null : s(middle(values)),
180
+ outside,
181
+ decision: outside.length === 0 && values.length > 0 ? "holds" : "fails"
182
+ };
183
+ });
184
+ const medians = {};
185
+ const seen = new Set(counted.flatMap((run) => Object.keys(run.group?.stages ?? {})));
186
+ for (const stage of seen) {
187
+ const values = counted.flatMap((run) => {
188
+ const at = run.group?.stages[stage];
189
+ return at === void 0 ? [] : [valueIn(at, "s")];
190
+ });
191
+ medians[stage] = s(middle(values.sort((a, b) => a - b)));
192
+ }
193
+ const lostCounts = counted.flatMap((run) => run.group?.writes.lost === null || run.group === void 0 ? [] : [run.group.writes.lost.length]);
194
+ const oldCounts = counted.flatMap((run) => run.group === void 0 ? [] : [run.group.writes.old]);
195
+ const unknown = counted.flatMap((run) => run.group?.writes.unknown ?? []);
196
+ const errors = /* @__PURE__ */ new Map();
197
+ for (const run of counted) for (const error of run.group?.errors ?? []) errors.set(error.name, [...errors.get(error.name) ?? [], valueIn(error.first, "s")]);
198
+ const lags = counted.flatMap((run) => run.group?.lags ?? []);
199
+ return {
200
+ stages,
201
+ medians,
202
+ lost: {
203
+ runs: lostCounts.length,
204
+ total: lostCounts.reduce((sum, n) => sum + n, 0),
205
+ median: medianCount(lostCounts),
206
+ max: lostCounts.length === 0 ? null : Math.max(...lostCounts)
207
+ },
208
+ old: {
209
+ median: medianCount(oldCounts),
210
+ max: oldCounts.length === 0 ? null : Math.max(...oldCounts)
211
+ },
212
+ unknown: {
213
+ total: unknown.length,
214
+ held: unknown.filter((each) => each.held === true).length
215
+ },
216
+ errors: [...errors].map(([error, firsts]) => ({
217
+ name: error,
218
+ runs: firsts.length,
219
+ first: s(middle(firsts.sort((a, b) => a - b)))
220
+ })),
221
+ ...lags.length === 0 ? {} : { lags: spreadOf(lags) }
222
+ };
223
+ };
224
+ /** This function gives the verdict of the band of a cell: its containment and the equivalence of its median. */
225
+ const verdictOfBand = (prediction, runs) => {
114
226
  const counted = runs.filter((run) => run.void === null);
115
227
  const voided = runs.length - counted.length;
116
228
  const outside = counted.filter((run) => run.finding !== null).map((run) => ({
@@ -270,6 +382,252 @@ const verdictOf = (prediction, runs) => {
270
382
  };
271
383
  };
272
384
  //#endregion
385
+ //#region src/timeline.ts
386
+ /**
387
+ * 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
389
+ * became of each write.
390
+ */
391
+ /** These are the stages of a timeline, in their sequence after a fault. */
392
+ const STAGES = [
393
+ "+sdown",
394
+ "+odown",
395
+ "+try-failover",
396
+ "+elected-leader",
397
+ "+selected-slave",
398
+ "+promoted-slave",
399
+ "+switch-master",
400
+ "+convert-to-slave"
401
+ ];
402
+ const between$1 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
403
+ /** This function gives the member whose address and port is `at`, or null. */
404
+ const memberAt = (run, at) => {
405
+ if (typeof at !== "string") return null;
406
+ const ip = at.split(":")[0];
407
+ return Object.entries(run.group?.members ?? {}).find(([, part]) => part.ip === ip)?.[0] ?? null;
408
+ };
409
+ /** This function gives the time of each stage of a timeline from an instant: the first announcement of each, after the instant. */
410
+ const stagesOf = (timeline, from) => {
411
+ const out = {};
412
+ for (const line of timeline) {
413
+ if (line.ev !== "sentinel" || line.t < from) continue;
414
+ const stage = STAGES.find((each) => each === line.event);
415
+ if (stage === void 0 || out[stage] !== void 0) continue;
416
+ if ((stage === "+sdown" || stage === "+odown") && !String(line.detail ?? "").startsWith("master ")) continue;
417
+ out[stage] = between$1(from, line.t);
418
+ }
419
+ return out;
420
+ };
421
+ /** 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
+ const writesOf = (run, after, primary) => {
423
+ const { events } = run;
424
+ const sent = events.filter((event) => event.ev === "sent");
425
+ const done = events.find((event) => event.ev === "done")?.t ?? sent.at(-1)?.t ?? Infinity;
426
+ const acks = /* @__PURE__ */ new Map();
427
+ for (const event of events) if (event.ev === "ack") acks.set(Number(event["n"]), event);
428
+ const holds = run.group?.holds ?? null;
429
+ const kept = holds === null ? null : new Set(holds);
430
+ const old = [...acks.values()].filter((event) => event.t > after && memberAt(run, event["at"]) === primary).length;
431
+ const lost = kept === null ? null : [...acks.keys()].filter((n) => !kept.has(n)).sort((a, b) => a - b);
432
+ const unknown = sent.filter((event) => event.t < done && !acks.has(Number(event["n"]))).map((event) => {
433
+ const n = Number(event["n"]);
434
+ return {
435
+ n,
436
+ held: kept === null ? null : kept.has(n)
437
+ };
438
+ });
439
+ return {
440
+ sent: sent.length,
441
+ acked: acks.size,
442
+ old,
443
+ lost,
444
+ unknown
445
+ };
446
+ };
447
+ /** This function gives the errors that the client met after an instant: each kind, its first time, and its count. */
448
+ const errorsOf = (events, after) => {
449
+ const done = events.find((event) => event.ev === "done")?.t ?? Infinity;
450
+ const kinds = /* @__PURE__ */ new Map();
451
+ for (const event of events) {
452
+ if (event.t <= after || event.t >= done) continue;
453
+ const name = event.ev === "error" ? String(event["errno"] ?? event["name"] ?? "error") : event.ev === "fail" ? String(event["name"] ?? "fail") : event.ev === "sentinel-error" ? "sentinel-error" : null;
454
+ if (name === null) continue;
455
+ const seen = kinds.get(name);
456
+ if (seen === void 0) kinds.set(name, {
457
+ first: event.t,
458
+ count: 1
459
+ });
460
+ else seen.count += 1;
461
+ }
462
+ return [...kinds].map(([name, { first, count }]) => ({
463
+ name,
464
+ first: between$1(after, first),
465
+ count
466
+ }));
467
+ };
468
+ /**
469
+ * This function gives the delay of each stamp of the observer after the write of its Sentinel that
470
+ * the observer read. That write is the latest write of the Sentinel's socket toward the observer,
471
+ * at the stamp or before it. The prober records each write (`tx`) on the clock of the stamps.
472
+ */
473
+ const lagsOf = (run, kernel) => {
474
+ const timeline = run.group?.timeline ?? [];
475
+ const locals = /* @__PURE__ */ new Map();
476
+ for (const line of timeline) if (line.ev === "subscribed") locals.set(String(line.sentinel), String(line["local"]));
477
+ const out = [];
478
+ for (const line of timeline) {
479
+ if (line.ev !== "sentinel") continue;
480
+ const sentinel = String(line.sentinel);
481
+ const local = locals.get(sentinel);
482
+ if (local === void 0) continue;
483
+ const write = kernel.filter((record) => record.ev === "tx" && record.src === sentinel && record.dst === local && record.mono <= line.t).at(-1);
484
+ if (write !== void 0) out.push(between$1(write.mono, line.t));
485
+ }
486
+ return out;
487
+ };
488
+ /**
489
+ * This function reads one run of a group cell.
490
+ * - The reference is the start of the commit of the fault, as the injector stamped it. For a
491
+ * stall, it is the first write into the fault, as for the stall of a direct pair.
492
+ * - 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.
494
+ * - Its time is the last segment with data that the client's socket to that primary received
495
+ * before the client saw the acceptance. The stamp of the client is the fallback.
496
+ * - A run is void when the prober lost records, or when the client did not become ready. It is
497
+ * also void when a Sentinel entered its TILT mode, or when the election split its votes. A band
498
+ * from the fault whose commit took longer than δ_abs is void too. So is a band whose hold lifted
499
+ * more than δ_abs late.
500
+ */
501
+ const measureGroup = (cell, run) => {
502
+ const { events, kernel } = run;
503
+ const { prediction } = cell;
504
+ const expected = prediction.group;
505
+ const primary = run.group?.primary ?? "";
506
+ const ready = events.find((event) => event.ev === "ready");
507
+ const t0 = run.fault?.t0;
508
+ const empty = {
509
+ from: primary,
510
+ to: null,
511
+ stages: {},
512
+ writes: {
513
+ sent: 0,
514
+ acked: 0,
515
+ old: 0,
516
+ lost: null,
517
+ unknown: []
518
+ },
519
+ errors: []
520
+ };
521
+ const silent = {
522
+ outcome: { kind: "silent" },
523
+ elapsed: null,
524
+ socket: null,
525
+ noticed: null,
526
+ finding: null
527
+ };
528
+ if (ready === void 0) return {
529
+ ...silent,
530
+ void: "the client never became ready",
531
+ group: empty
532
+ };
533
+ if (t0 === void 0 || run.fault === null) return {
534
+ ...silent,
535
+ void: "the fault never committed",
536
+ group: empty
537
+ };
538
+ if (run.lost === null) return {
539
+ ...silent,
540
+ void: "the prober wrote no clock line after the run, so nothing says what it dropped",
541
+ group: empty
542
+ };
543
+ if (run.lost > 0) return {
544
+ ...silent,
545
+ void: `the prober dropped ${run.lost} records`,
546
+ group: empty
547
+ };
548
+ const timeline = run.group?.timeline ?? [];
549
+ const stages = stagesOf(timeline, t0);
550
+ const heal = run.heal ?? null;
551
+ const lands = expected?.lands ?? "new";
552
+ const after = lands === "same" && heal !== null ? heal.t0 : t0;
553
+ const accepted = events.find((event) => {
554
+ if (event.ev !== "ack" || event.t <= after) return false;
555
+ const member = memberAt(run, event["at"]);
556
+ return member !== null && (lands === "new" ? member !== primary : true);
557
+ });
558
+ const to = accepted === void 0 ? null : memberAt(run, accepted["at"]);
559
+ const group = {
560
+ from: primary,
561
+ to,
562
+ stages,
563
+ writes: writesOf(run, t0, primary),
564
+ errors: errorsOf(events, t0),
565
+ ...cell.calibrates === "sentinels" ? { lags: lagsOf(run, kernel) } : {}
566
+ };
567
+ let voided = null;
568
+ const tilt = timeline.find((line) => line.ev === "sentinel" && line.event === "+tilt" && line.t >= t0);
569
+ if (tilt !== void 0) voided = `a Sentinel entered TILT ${between$1(t0, tilt.t).value} s after the fault: the host delayed it, not its timers`;
570
+ const split = timeline.find((line) => line.ev === "sentinel" && line.event === "-failover-abort-not-elected" && line.t >= t0);
571
+ const elected = timeline.find((line) => line.ev === "sentinel" && line.event === "+elected-leader" && line.t >= t0);
572
+ if (voided === null && split !== void 0 && (elected === void 0 || elected.t > split.t)) voided = "the election split its votes and runs again after 2 × failover-timeout, which the band does not bound";
573
+ const odown = stages["+odown"];
574
+ if (voided === null && odown !== void 0 && stages["+try-failover"] === void 0) voided = "no Sentinel started a failover after +odown: each was in the hold-off of 2 × failover-timeout after an earlier attempt";
575
+ if (voided === null && cell.hold !== void 0 && prediction.detects !== null) {
576
+ if (heal === null) voided = "the fault never lifted";
577
+ else {
578
+ const late = (heal.t0 - t0) / 1e9 - valueIn(cell.hold, "s");
579
+ if (late > valueIn(NOISE, "s")) voided = `the lift came ${Math.round(late * 1e4) / 10} ms after its hold: the injector was late, not the group`;
580
+ }
581
+ }
582
+ 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);
584
+ const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
585
+ 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
+ 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`;
587
+ let reference = t0;
588
+ if (prediction.detects === false && prediction.recovers !== void 0) {
589
+ const into = events.find((event) => event.ev === "sent" && event.t > run.fault.t1);
590
+ if (into === void 0 || heal !== null && into.t > heal.t0) voided ??= "the write in flight went out before the fault held";
591
+ else {
592
+ reference = into.t;
593
+ const { heal: bounds } = prediction.recovers;
594
+ if (heal === null) voided ??= "the stall never ended";
595
+ else if (valueIn(between$1(reference, heal.t0), "s") <= valueIn(bounds.low, "s") || valueIn(between$1(reference, heal.t1), "s") >= valueIn(bounds.high, "s")) voided ??= `the stall ended ${between$1(reference, heal.t0).value}–${between$1(reference, heal.t1).value} s after the write, outside the ${bounds.low.value}–${bounds.high.value} s its prediction assumed`;
596
+ }
597
+ }
598
+ const findings = [];
599
+ const switched = stages["+switch-master"];
600
+ if (expected?.failover === false && switched !== void 0) findings.push(`the group failed over ${switched.value} s after the fault, where it should not`);
601
+ 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
+ for (const error of expected?.errors ?? []) {
603
+ const met = group.errors.find((each) => each.name === error.name);
604
+ const noise = valueIn(NOISE, "s");
605
+ if (met === void 0) findings.push(`the client never met ${error.name}`);
606
+ else if (met.first.value < valueIn(error.low, "s") - noise || met.first.value > valueIn(error.high, "s") + noise) findings.push(`the client met ${error.name} ${met.first.value} s after the fault, outside [${valueIn(error.low, "s")}, ${valueIn(error.high, "s")}] s`);
607
+ }
608
+ if (accepted === void 0) {
609
+ if (prediction.detects !== null) findings.push(lands === "new" ? "the client never wrote to a new primary in the window" : "the client never wrote again in the window");
610
+ return {
611
+ ...silent,
612
+ void: voided,
613
+ finding: findings[0] ?? null,
614
+ group
615
+ };
616
+ }
617
+ 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);
619
+ const at = segment?.mono ?? accepted.t;
620
+ return {
621
+ outcome: { kind: "answered" },
622
+ elapsed: between$1(reference, at),
623
+ socket: between$1(reference, accepted.t),
624
+ noticed: segment === void 0 ? null : between$1(segment.mono, accepted.t),
625
+ void: voided,
626
+ finding: findings[0] ?? null,
627
+ group
628
+ };
629
+ };
630
+ //#endregion
273
631
  //#region src/observe.ts
274
632
  /** These are the errno numbers that the kernel reports to a socket, by the names that predictions use. */
275
633
  const ERRNO = {
@@ -423,12 +781,82 @@ const crossingsOf = (run, hop, toClient, toTarget) => {
423
781
  * it, and before the first keepalive probe. A live peer answers that probe, and the answer
424
782
  * starts the idle time again. A run where the fault was late is void. A run whose records the
425
783
  * prober lost is also void.
784
+ * - For an HTTP client, it also reads what the second request met (`requestOf`). An outcome other
785
+ * than the one that the prediction gives is a finding.
426
786
  */
427
787
  const measure = (cell, run) => {
788
+ const read = measureSocket(cell, run);
789
+ const expected = cell.prediction.request;
790
+ if (cell.probe.http === void 0 && expected === void 0) return read;
791
+ const request = requestOf(run.events);
792
+ if (expected === void 0 || read.void !== null) return {
793
+ ...read,
794
+ request
795
+ };
796
+ const differs = requestFinding(expected, request);
797
+ const finding = read.finding === null ? differs : differs === null ? read.finding : `${read.finding}. ${differs}`;
798
+ return {
799
+ ...read,
800
+ request,
801
+ finding
802
+ };
803
+ };
804
+ /** This function tells how a request reading differs from its prediction, or gives null. */
805
+ const requestFinding = (expected, read) => {
806
+ if (read === null) return `the second request never went out, where it should meet: ${requestWords(expected)}`;
807
+ 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)}`;
808
+ };
809
+ /**
810
+ * This function reads the second request of an HTTP client from its lines. Its first try is its
811
+ * first `request` line, and each `retry` line is a new try. Its end is its `error` line, or its
812
+ * `replied` line. A request with neither is pending. It gives null when the client wrote no try.
813
+ *
814
+ * A new try that gets no connection writes no `retry` line. Its dial writes a `dial-error` line.
815
+ * Thus a `dial-error` line after the first try and before the end is also a new try.
816
+ */
817
+ const requestOf = (events) => {
818
+ const of = (event) => Number(event["i"]) === 2;
819
+ const tries = events.filter((event) => (event.ev === "request" || event.ev === "retry") && of(event));
820
+ const first = tries[0];
821
+ if (first === void 0) return null;
822
+ const failed = events.find((event) => event.ev === "error" && of(event));
823
+ const replied = events.some((event) => event.ev === "replied" && of(event));
824
+ const ready = events.find((event) => event.ev === "ready");
825
+ const sent = events.find((event) => event.ev === "sent" && of(event));
826
+ const end = failed ?? events.find((event) => event.ev === "done" && of(event));
827
+ const dialed = events.some((event) => event.ev === "dial-error" && event.t > first.t && (end === void 0 || event.t <= end.t));
828
+ return {
829
+ connection: first["reused"] === true ? "reused" : "new",
830
+ retried: dialed || tries.some((each) => each.ev === "retry"),
831
+ error: failed === void 0 ? null : String(failed["errno"]),
832
+ pending: failed === void 0 && !replied,
833
+ gap: ready === void 0 || sent === void 0 ? null : between(ready.t, sent.t)
834
+ };
835
+ };
836
+ /** This function reads the socket of one run against its cell, as `measure` does without the second request of an HTTP client. */
837
+ const measureSocket = (cell, run) => {
838
+ if (cell.group !== void 0) {
839
+ const read = measureGroup(cell, run);
840
+ return {
841
+ environment: run.environment,
842
+ index: run.index,
843
+ outcome: read.outcome,
844
+ elapsed: read.elapsed,
845
+ socket: read.socket,
846
+ noticed: read.noticed,
847
+ steps: [],
848
+ probes: 0,
849
+ retransmissions: run.kernel.filter((record) => record.netns === run.netns && record.ev === "retrans").length,
850
+ acks: read.group.writes.acked,
851
+ void: read.void,
852
+ finding: read.finding,
853
+ group: read.group
854
+ };
855
+ }
428
856
  const { events, kernel } = run;
429
857
  const ready = events.find((event) => event.ev === "ready");
430
858
  const local = events.find((event) => event.ev === "connected")?.["local"];
431
- const inflight = cell.probe.mode !== "idle";
859
+ const inflight = cell.probe.mode !== "idle" && cell.probe.mode !== "gap";
432
860
  const request = inflight ? events.filter((event) => event.ev === "sent")[1] : ready;
433
861
  const server = cell.observer === "server";
434
862
  const place = socketOf(cell, run);
@@ -517,14 +945,15 @@ const measure = (cell, run) => {
517
945
  const reported = socket.find((record) => record.ev === "error" && (record.err ?? 0) !== 0);
518
946
  const seen = remote ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
519
947
  const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8);
948
+ const own = cell.prediction.detects === true && cell.prediction.errno === "CLOSED" ? socket.find((record) => record.ev === "state" && oldState(record) === 1 && newState(record) === 4) : void 0;
520
949
  const outcome = reported !== void 0 ? {
521
950
  kind: "error",
522
951
  errno: ERRNO[reported.err] ?? `E${reported.err}`
523
- } : closeWait !== void 0 || seen?.ev === "eof" ? { kind: "eof" } : seen !== void 0 ? {
952
+ } : closeWait !== void 0 || seen?.ev === "eof" ? { kind: "eof" } : own !== void 0 ? { kind: "closed" } : seen !== void 0 ? {
524
953
  kind: "error",
525
954
  errno: String(seen["errno"])
526
955
  } : { kind: "silent" };
527
- const ended = reported?.mono ?? closeWait?.mono ?? seen?.t;
956
+ const ended = reported?.mono ?? closeWait?.mono ?? own?.mono ?? seen?.t;
528
957
  const noticed = reported !== void 0 && seen !== void 0 ? between(reported.mono, seen.t) : null;
529
958
  const { prediction } = cell;
530
959
  const before = run.fault?.t1 ?? ready.t;
@@ -651,13 +1080,25 @@ const measure = (cell, run) => {
651
1080
  if (!prediction.detects) {
652
1081
  const window = valueIn(prediction.window, "s") * 1e9;
653
1082
  if (ended === void 0 || ended - request.t > window) {
654
- const forgot = forgotten !== void 0 && forgotten.t - request.t <= window ? `a router forgot the flow ${between(lastRx, forgotten.t).value} s after the last segment, where it should keep it` : null;
1083
+ const forgetIn = forgotten !== void 0 && forgotten.t - request.t <= window ? forgotten : void 0;
1084
+ const expected = prediction.middlebox?.forgets;
1085
+ const after = forgetIn === void 0 ? null : between(lastRx, forgetIn.t);
1086
+ let finding = null;
1087
+ if (expected === void 0) {
1088
+ if (after !== null) finding = `a router forgot the flow ${after.value} s after the last segment, where it should keep it`;
1089
+ } else if (after === null) finding = "no router forgot the flow in the window, where it should forget it";
1090
+ else {
1091
+ const noise = valueIn(NOISE, "s");
1092
+ const low = valueIn(expected.low, "s") - noise;
1093
+ const high = valueIn(expected.high, "s") + noise;
1094
+ if (after.value < low || after.value > high) finding = `a router forgot the flow ${after.value} s after the last segment, outside [${Math.round(low * 1e6) / 1e6}, ${Math.round(high * 1e6) / 1e6}] s`;
1095
+ }
655
1096
  return {
656
1097
  ...base,
657
1098
  ...nothing,
658
1099
  ...chain,
659
1100
  void: voided,
660
- finding: forgot
1101
+ finding
661
1102
  };
662
1103
  }
663
1104
  return {
@@ -688,8 +1129,9 @@ const measure = (cell, run) => {
688
1129
  finding: `it ended without its reference event (${prediction.from})`
689
1130
  };
690
1131
  const steps = (prediction.from === "last-received" ? keepalives : prediction.from === "first-retransmission" ? retransmitted.filter((record) => record.why === "rto" && record.mono > reference) : []).map((record) => between(reference, record.mono));
691
- 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}`;
692
- if (prediction.from === "last-received" || prediction.from === "first-retransmission") voided ??= delayed(reference, ended, prediction.from === "last-received" ? "keepalive" : "retransmit");
1132
+ const finding = (prediction.errno === "EOF" ? outcome.kind === "eof" : prediction.errno === "CLOSED" ? outcome.kind === "closed" : outcome.kind === "error" && outcome.errno === prediction.errno) ? null : `it ended with ${outcome.kind === "error" ? outcome.errno : outcome.kind === "closed" ? "its own close" : "end of file"}, not ${prediction.errno}`;
1133
+ const programTimer = cell.probe.http !== void 0 && prediction.from === "last-received" && prediction.timer !== "keepalive";
1134
+ if (prediction.from === "last-received" && !programTimer || prediction.from === "first-retransmission") voided ??= delayed(reference, ended, prediction.from === "last-received" ? "keepalive" : "retransmit");
693
1135
  const lag = (valueIn(prediction.high, "s") - valueIn(prediction.low, "s")) * 1e9;
694
1136
  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;
695
1137
  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`;
@@ -746,6 +1188,18 @@ const NODE = {
746
1188
  dockerfile: "harness/image/node/Dockerfile",
747
1189
  from: ["harness/image/node"]
748
1190
  };
1191
+ /** This image holds the Go HTTP client: the http.Transport of Go 1.27.2. */
1192
+ const GO_HTTP = {
1193
+ name: "loom-harness-go-http",
1194
+ dockerfile: "harness/image/go-http/Dockerfile",
1195
+ from: ["harness/image/go-http"]
1196
+ };
1197
+ /** This image holds the Node HTTP clients: the http.Agent of Node 24.21.0, and undici 8.12.2. */
1198
+ const NODE_HTTP = {
1199
+ name: "loom-harness-node-http",
1200
+ dockerfile: "harness/image/node-http/Dockerfile",
1201
+ from: ["harness/image/node-http"]
1202
+ };
749
1203
  /** This image is the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow. */
750
1204
  const ROUTER = {
751
1205
  name: "loom-harness-router",
@@ -812,6 +1266,10 @@ const harnessImage = () => build(HARNESS);
812
1266
  const nodeClientImage = () => build(NODE);
813
1267
  /** This function gives the image of the router. */
814
1268
  const routerImage = () => build(ROUTER);
1269
+ /** This function gives the image of the Go HTTP client. */
1270
+ const goHttpImage = () => build(GO_HTTP);
1271
+ /** This function gives the image of the Node HTTP clients. */
1272
+ const nodeHttpImage = () => build(NODE_HTTP);
815
1273
  /**
816
1274
  * This time is the interval of the listing of a router, when its hop does not set one. A listing
817
1275
  * evicts each expired flow that it walks past, and the DESTROY event of that flow follows.
@@ -888,6 +1346,11 @@ const lines = (also) => {
888
1346
  }
889
1347
  };
890
1348
  };
1349
+ /** This function makes a container share the process namespace of another container, for example `container:<id>`. */
1350
+ const withPid = (container, mode) => {
1351
+ container.hostConfig.PidMode = mode;
1352
+ return container;
1353
+ };
891
1354
  /** This container class can share the process namespace of another container. */
892
1355
  const shared = (Base) => class extends Base {
893
1356
  withPidMode(mode) {
@@ -949,6 +1412,16 @@ const clientOf = async (kind, image) => {
949
1412
  command: ["node", "/opt/harness/node/client-ioredis.mjs"],
950
1413
  clientKind: kind
951
1414
  };
1415
+ if (kind === "go-http") return {
1416
+ clientImage: await goHttpImage(),
1417
+ command: ["/opt/harness/client-go"],
1418
+ clientKind: kind
1419
+ };
1420
+ if (kind === "http-agent" || kind === "undici") return {
1421
+ clientImage: await nodeHttpImage(),
1422
+ command: ["node", `/opt/harness/node-http/client-${kind === "undici" ? "undici" : "agent"}.mjs`],
1423
+ clientKind: kind
1424
+ };
952
1425
  return {
953
1426
  clientImage: image,
954
1427
  command: ["/opt/harness/client"],
@@ -989,6 +1462,7 @@ const openLab = async (target, kind = "tcp", topology) => {
989
1462
  const runtime = await getContainerRuntimeClient();
990
1463
  const opened = [];
991
1464
  const networks = [];
1465
+ const serverLines = lines();
992
1466
  const close = async () => {
993
1467
  for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
994
1468
  for (const network of networks.reverse()) await network.stop().catch(() => void 0);
@@ -1008,7 +1482,12 @@ const openLab = async (target, kind = "tcp", topology) => {
1008
1482
  try {
1009
1483
  for (let at = 0; at <= hops.length; at += 1) networks.push(await new Network().start());
1010
1484
  const last = networks[hops.length];
1011
- const stock = new GenericContainer(target.image).withNetwork(last).withWaitStrategy(Wait.forLogMessage(target.ready));
1485
+ let stock = new GenericContainer(target.image).withNetwork(last).withWaitStrategy(Wait.forLogMessage(target.ready));
1486
+ if (target.files !== void 0) stock = stock.withCopyContentToContainer(Object.entries(target.files).map(([path, content]) => ({
1487
+ content,
1488
+ target: path
1489
+ })));
1490
+ if (target.lines === true) stock = stock.withLogConsumer(serverLines.consume);
1012
1491
  const server = await (target.command === void 0 ? stock : stock.withCommand([...target.command])).start();
1013
1492
  opened.push(server);
1014
1493
  const serverIp = await ipOn(server, last);
@@ -1207,17 +1686,22 @@ const openLab = async (target, kind = "tcp", topology) => {
1207
1686
  * on its sockets. A router forwards past INPUT and OUTPUT, thus its rules are FORWARD rules on
1208
1687
  * its device toward the side (`--via`).
1209
1688
  */
1210
- const placeOf = (at) => {
1211
- if (at === "client") return [
1689
+ const hopPlace = (at) => {
1690
+ if (!isHopPlace(at)) throw new Error(`a lab of a topology has no part ${JSON.stringify(at)}: a group cell opens a group lab`);
1691
+ return at;
1692
+ };
1693
+ const placeOf = (place) => {
1694
+ if (place === "client") return [
1212
1695
  admin,
1213
1696
  targetIp,
1214
1697
  []
1215
1698
  ];
1216
- if (at === "target") return [
1699
+ if (place === "target") return [
1217
1700
  injector,
1218
1701
  targetPeer,
1219
1702
  []
1220
1703
  ];
1704
+ const at = hopPlace(place);
1221
1705
  const hop = hopNamed(at.hop);
1222
1706
  const peer = at.toward === "client" ? hop.peer.client : hop.peer.target.split(":")[0];
1223
1707
  const via = hop.hop.kind === "router" ? ["--via", hop.devices[at.toward]] : [];
@@ -1228,9 +1712,10 @@ const openLab = async (target, kind = "tcp", topology) => {
1228
1712
  ];
1229
1713
  };
1230
1714
  /** This function gives the namespace and the device of netem at a place: an end, or the device of a hop toward a side. */
1231
- const deviceAt = (at) => {
1232
- if (at === "client") return [admin, "eth0"];
1233
- if (at === "target") return [injector, "eth0"];
1715
+ const deviceAt = (place) => {
1716
+ if (place === "client") return [admin, "eth0"];
1717
+ if (place === "target") return [injector, "eth0"];
1718
+ const at = hopPlace(place);
1234
1719
  const hop = hopNamed(at.hop);
1235
1720
  return [hop.injector, hop.devices[at.toward]];
1236
1721
  };
@@ -1254,6 +1739,7 @@ const openLab = async (target, kind = "tcp", topology) => {
1254
1739
  netns,
1255
1740
  topology: placed,
1256
1741
  middlebox,
1742
+ server: serverLines.seen,
1257
1743
  kernel: records.seen,
1258
1744
  async sysctl(values) {
1259
1745
  const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
@@ -1340,7 +1826,7 @@ const openLab = async (target, kind = "tcp", topology) => {
1340
1826
  return stamped(where, args);
1341
1827
  },
1342
1828
  async offload(on, at = "target") {
1343
- const devices = typeof at === "object" ? Object.values(hopNamed(at.hop).devices) : [deviceAt(at)[1]];
1829
+ const devices = isHopPlace(at) ? Object.values(hopNamed(at.hop).devices) : [deviceAt(at)[1]];
1344
1830
  const [where] = deviceAt(at);
1345
1831
  for (const device of devices) await output(where, [
1346
1832
  "/opt/harness/inject",
@@ -1519,7 +2005,9 @@ const openLab = async (target, kind = "tcp", topology) => {
1519
2005
  image: target.image,
1520
2006
  id: (await inspect(server)).Image,
1521
2007
  settings,
1522
- sysctls: targetSysctls
2008
+ sysctls: targetSysctls,
2009
+ ...target.files === void 0 ? {} : { files: Object.fromEntries(Object.entries(target.files).map(([path, content]) => [path, createHash("sha256").update(content).digest("hex")])) },
2010
+ ...serverLines.seen[0] === void 0 ? {} : { first: serverLines.seen[0] }
1523
2011
  },
1524
2012
  harness: {
1525
2013
  image,
@@ -1547,6 +2035,673 @@ const openLab = async (target, kind = "tcp", topology) => {
1547
2035
  }
1548
2036
  };
1549
2037
  //#endregion
2038
+ //#region src/group.ts
2039
+ /** This function gives the configuration file of a member: its port, its settings, and the primary that a replica follows. */
2040
+ const memberConfig = (group, primary) => [
2041
+ `port ${group.port}`,
2042
+ ...Object.entries(group.memberSettings).map(([key, value]) => `${key} ${value}`),
2043
+ ...primary === null ? [] : [`replicaof ${primary} ${group.port}`],
2044
+ ""
2045
+ ].join("\n");
2046
+ /** This function gives the configuration file of a Sentinel: its port, the group that it monitors, and the settings of the group. */
2047
+ const sentinelConfig = (group, primary) => [
2048
+ `port ${group.sentinelPort}`,
2049
+ `sentinel monitor ${group.name} ${primary} ${group.port} ${group.quorum}`,
2050
+ ...Object.entries(group.sentinelSettings).map(([key, value]) => `sentinel ${key} ${group.name} ${value}`),
2051
+ ""
2052
+ ].join("\n");
2053
+ /**
2054
+ * This function gives the peers that a fault on a part cuts it from, by their addresses. `all` is
2055
+ * each other part and the client, `group` is the other parts, and `clients` is the client. The
2056
+ * observer is not a peer, thus the timeline of the group stays whole.
2057
+ */
2058
+ const peersOf = (parts, client, part, cuts) => {
2059
+ const others = [...Object.entries(parts.members), ...Object.entries(parts.sentinels)].filter(([name]) => name !== part).map(([, at]) => at.ip);
2060
+ if (cuts === "clients") return [client];
2061
+ if (cuts === "group") return others;
2062
+ return [...others, client];
2063
+ };
2064
+ /** This function reads the flat reply of a command line client into its fields: name, value, name, value. */
2065
+ const fieldsOf = (text) => {
2066
+ const out = [];
2067
+ const words = text.split("\n").map((line) => line.replace(/\r$/, ""));
2068
+ let current = null;
2069
+ for (let at = 0; at + 1 < words.length; at += 2) {
2070
+ const [key, value] = [words[at], words[at + 1]];
2071
+ if (key === "name" || current === null) {
2072
+ current = {};
2073
+ out.push(current);
2074
+ }
2075
+ current[key] = value;
2076
+ }
2077
+ return out;
2078
+ };
2079
+ /** This function reads the INFO of a server into its fields. */
2080
+ const infoOf = (text) => {
2081
+ const out = {};
2082
+ for (const line of text.split("\n")) {
2083
+ const at = line.indexOf(":");
2084
+ if (line.startsWith("#") || at === -1) continue;
2085
+ out[line.slice(0, at)] = line.slice(at + 1).trim();
2086
+ }
2087
+ return out;
2088
+ };
2089
+ /** This function judges the views of the Sentinels and of the members: the primary that they agree on, if the group is healthy. */
2090
+ const viewOf = (group, ips, sentinels, members) => {
2091
+ const others = String(group.sentinels.length - 1);
2092
+ const replicas = group.members.length - 1;
2093
+ const addresses = new Set(sentinels.map((each) => each.master["ip"]));
2094
+ if (addresses.size !== 1) return {
2095
+ primary: null,
2096
+ healthy: false,
2097
+ why: `the Sentinels name ${addresses.size} primaries`
2098
+ };
2099
+ const ip = [...addresses][0];
2100
+ const primary = Object.entries(ips).find(([, at]) => at === ip)?.[0] ?? null;
2101
+ if (primary === null) return {
2102
+ primary: null,
2103
+ healthy: false,
2104
+ why: `no member has the address ${ip}`
2105
+ };
2106
+ for (const [at, each] of sentinels.entries()) {
2107
+ if (each.master["flags"] !== "master") return {
2108
+ primary,
2109
+ healthy: false,
2110
+ why: `Sentinel ${at} flags the primary ${each.master["flags"]}`
2111
+ };
2112
+ if (each.master["num-other-sentinels"] !== others) return {
2113
+ primary,
2114
+ healthy: false,
2115
+ why: `Sentinel ${at} knows ${each.master["num-other-sentinels"]} other Sentinels`
2116
+ };
2117
+ const good = each.replicas.filter((replica) => replica["flags"] === "slave" && replica["master-link-status"] === "ok" && replica["master-host"] === ip);
2118
+ if (good.length !== replicas || each.replicas.length !== replicas) return {
2119
+ primary,
2120
+ healthy: false,
2121
+ why: `Sentinel ${at} has ${good.length} healthy replicas of ${each.replicas.length}`
2122
+ };
2123
+ }
2124
+ const info = members[primary] ?? {};
2125
+ if (info["role"] !== "master") return {
2126
+ primary,
2127
+ healthy: false,
2128
+ why: `${primary} reports the role ${info["role"]}`
2129
+ };
2130
+ if (info["master_failover_state"] !== void 0 && info["master_failover_state"] !== "no-failover") return {
2131
+ primary,
2132
+ healthy: false,
2133
+ why: `${primary} is in the failover state ${info["master_failover_state"]}`
2134
+ };
2135
+ const online = Object.entries(info).filter(([key, value]) => /^slave\d+$/.test(key) && value.includes("state=online")).length;
2136
+ if (online !== replicas) return {
2137
+ primary,
2138
+ healthy: false,
2139
+ why: `${primary} has ${online} replicas online`
2140
+ };
2141
+ for (const name of group.members) {
2142
+ if (name === primary) continue;
2143
+ const each = members[name] ?? {};
2144
+ if (each["role"] !== "slave" || each["master_host"] !== ip || each["master_link_status"] !== "up") return {
2145
+ primary,
2146
+ healthy: false,
2147
+ why: `${name} is not a replica of ${primary} with its link up`
2148
+ };
2149
+ }
2150
+ return {
2151
+ primary,
2152
+ healthy: true,
2153
+ why: "healthy"
2154
+ };
2155
+ };
2156
+ const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2157
+ /** This time is the longest that a group takes to converge before a run, or after it. */
2158
+ const CONVERGE_MS = 24e4;
2159
+ /**
2160
+ * This function opens one environment of a group cell:
2161
+ * - one new network, with each member, each Sentinel, the client and the observer on it.
2162
+ * - each member: the stock image, its server as the child of a shell, thus a fin can stop the
2163
+ * server and the container stays up. The first member is the primary, and the others follow it.
2164
+ * - each Sentinel: the stock image, its Sentinel as the child of a shell, which monitors the first member.
2165
+ * - an injector sidecar for each part, in its network and process namespaces.
2166
+ * - a client that holds its own network namespace, with a privileged sidecar for its sysctls.
2167
+ * - the observer of the Sentinels, in a namespace of its own.
2168
+ * - the prober, which watches the namespace of the client, and of each Sentinel for a calibration.
2169
+ */
2170
+ const openGroupLab = async (group, options = {}) => {
2171
+ const image = await harnessImage();
2172
+ const node = await nodeClientImage();
2173
+ const { GenericContainer, Network, Wait, getContainerRuntimeClient } = await containers();
2174
+ const runtime = await getContainerRuntimeClient();
2175
+ const opened = [];
2176
+ let network;
2177
+ const close = async () => {
2178
+ for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
2179
+ await network?.stop().catch(() => void 0);
2180
+ };
2181
+ const ipOf = async (container) => {
2182
+ const ip = (await runtime.container.inspect(runtime.container.getById(container.getId()))).NetworkSettings.Networks[network.getName()]?.IPAddress;
2183
+ if (ip === void 0 || ip === "") throw new Error(`${container.getName()} has no address on ${network.getName()}`);
2184
+ return ip;
2185
+ };
2186
+ try {
2187
+ network = await new Network().start();
2188
+ const boxes = /* @__PURE__ */ new Map();
2189
+ const injectors = /* @__PURE__ */ new Map();
2190
+ const parts = {
2191
+ members: {},
2192
+ sentinels: {}
2193
+ };
2194
+ /** This function starts one part: the stock image with its configuration, its program as the child of a shell, and its injector. */
2195
+ const start = async (name, program, file, content, ready) => {
2196
+ const box = await new GenericContainer(group.image).withNetwork(network).withNetworkAliases(name).withCopyContentToContainer([{
2197
+ content,
2198
+ target: file
2199
+ }]).withCommand([
2200
+ "sh",
2201
+ "-c",
2202
+ `${program} ${file} & while :; do sleep 3600; done`
2203
+ ]).withWaitStrategy(Wait.forLogMessage(ready)).start();
2204
+ opened.push(box);
2205
+ boxes.set(name, box);
2206
+ const injector = await withPid(new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${box.getId()}`).start();
2207
+ opened.push(injector);
2208
+ injectors.set(name, injector);
2209
+ return {
2210
+ ip: await ipOf(box),
2211
+ netns: await netnsOf(injector)
2212
+ };
2213
+ };
2214
+ const [first, ...rest] = group.members;
2215
+ if (first === void 0) throw new Error("a group has no member");
2216
+ parts.members[first] = await start(first, group.programs.server, "/data/server.conf", memberConfig(group, null), group.ready.member);
2217
+ const primaryIp = parts.members[first].ip;
2218
+ for (const name of rest) parts.members[name] = await start(name, group.programs.server, "/data/server.conf", memberConfig(group, primaryIp), group.ready.member);
2219
+ for (const name of group.sentinels) parts.sentinels[name] = await start(name, group.programs.sentinel, "/data/sentinel.conf", sentinelConfig(group, primaryIp), group.ready.sentinel);
2220
+ const holder = await new GenericContainer(image).withNetwork(network).start();
2221
+ opened.push(holder);
2222
+ const admin = await new GenericContainer(image).withNetworkMode(`container:${holder.getId()}`).withPrivilegedMode().start();
2223
+ opened.push(admin);
2224
+ const clientIp = await ipOf(holder);
2225
+ const sentinelAddresses = group.sentinels.map((name) => `${parts.sentinels[name].ip}:${group.sentinelPort}`);
2226
+ const timeline = lines();
2227
+ const observer = await new GenericContainer(node).withNetwork(network).withCommand([
2228
+ "node",
2229
+ "/opt/harness/node/observer.mjs",
2230
+ sentinelAddresses.join(",")
2231
+ ]).withLogConsumer(timeline.consume).start();
2232
+ opened.push(observer);
2233
+ const observerIp = await ipOf(observer);
2234
+ for (const address of sentinelAddresses) await new Promise((resolve, reject) => {
2235
+ const started = Date.now();
2236
+ const look = () => {
2237
+ if (timeline.seen.some((line) => line.ev === "subscribed" && line["sentinel"] === address)) resolve();
2238
+ else if (Date.now() - started > 3e4) reject(/* @__PURE__ */ new Error(`the observer never subscribed to ${address}`));
2239
+ else setTimeout(look, 50);
2240
+ };
2241
+ look();
2242
+ });
2243
+ const clientNetns = await netnsOf(admin);
2244
+ const watched = [clientNetns, ...options.watchSentinels === true ? group.sentinels.map((name) => parts.sentinels[name].netns) : []];
2245
+ const records = lines();
2246
+ const prober = await new GenericContainer(image).withPrivilegedMode().withNetworkMode("none").withCommand(["/opt/harness/prober", ...watched.map(String)]).withLogConsumer(records.consume).start();
2247
+ opened.push(prober);
2248
+ const begun = await records.until(["begin", "fail"], 3e4, "prober");
2249
+ if (begun.ev === "fail") throw new Error(`the prober failed: ${String(begun["why"])}`);
2250
+ const box = (name) => {
2251
+ const found = boxes.get(name);
2252
+ if (found === void 0) throw new Error(`the group has no part ${name}`);
2253
+ return found;
2254
+ };
2255
+ const injectorOf = (name) => {
2256
+ const found = injectors.get(name);
2257
+ if (found === void 0) throw new Error(`the group has no part ${name}`);
2258
+ return found;
2259
+ };
2260
+ const cli = (name, args) => {
2261
+ const port = group.sentinels.includes(name) ? group.sentinelPort : group.port;
2262
+ return output(box(name), [
2263
+ group.programs.cli,
2264
+ "-p",
2265
+ String(port),
2266
+ ...args
2267
+ ]);
2268
+ };
2269
+ const stamped = async (where, argv) => {
2270
+ const stamp = JSON.parse((await output(where, argv)).split("\n").at(-1));
2271
+ return {
2272
+ t0: stamp.t0,
2273
+ t1: stamp.t1
2274
+ };
2275
+ };
2276
+ const ips = Object.fromEntries(Object.entries(parts.members).map(([name, at]) => [name, at.ip]));
2277
+ const view = async () => {
2278
+ const sentinels = [];
2279
+ for (const name of group.sentinels) {
2280
+ const master = fieldsOf(await cli(name, [
2281
+ "SENTINEL",
2282
+ "MASTER",
2283
+ group.name
2284
+ ]))[0] ?? {};
2285
+ const replicas = fieldsOf(await cli(name, [
2286
+ "SENTINEL",
2287
+ "REPLICAS",
2288
+ group.name
2289
+ ]));
2290
+ sentinels.push({
2291
+ master,
2292
+ replicas
2293
+ });
2294
+ }
2295
+ const members = {};
2296
+ for (const name of group.members) members[name] = infoOf(await cli(name, ["INFO", "replication"]));
2297
+ return viewOf(group, ips, sentinels, members);
2298
+ };
2299
+ const after = (at) => at === void 0 ? [] : ["--at", String(Math.round(at))];
2300
+ const roles = (role, primary) => {
2301
+ if (role === "primary") return primary;
2302
+ if (role === "sentinel") return group.sentinels[0];
2303
+ const replica = group.members.find((name) => name !== primary);
2304
+ if (replica === void 0) throw new Error("the group has no replica");
2305
+ return replica;
2306
+ };
2307
+ return {
2308
+ group,
2309
+ clientIp,
2310
+ observerIp,
2311
+ members: parts.members,
2312
+ sentinels: parts.sentinels,
2313
+ netns: {
2314
+ client: clientNetns,
2315
+ watched
2316
+ },
2317
+ kernel: records.seen,
2318
+ timeline: timeline.seen,
2319
+ async sysctl(values) {
2320
+ const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
2321
+ if (settings.length > 0) await output(admin, [
2322
+ "sysctl",
2323
+ "-q",
2324
+ "-w",
2325
+ ...settings
2326
+ ]);
2327
+ const read = {};
2328
+ for (const key of Object.keys(values)) read[key] = await output(admin, [
2329
+ "sysctl",
2330
+ "-n",
2331
+ key
2332
+ ]);
2333
+ return read;
2334
+ },
2335
+ async converge(within = CONVERGE_MS) {
2336
+ const started = Date.now();
2337
+ let calm = 0;
2338
+ let last = {
2339
+ primary: null,
2340
+ healthy: false,
2341
+ why: "no view yet"
2342
+ };
2343
+ while (Date.now() - started < within) {
2344
+ last = await view().catch((error) => ({
2345
+ primary: null,
2346
+ healthy: false,
2347
+ why: error.message
2348
+ }));
2349
+ calm = last.healthy ? calm + 1 : 0;
2350
+ if (calm >= 2 && last.primary !== null) return last.primary;
2351
+ await sleep$1(1e3);
2352
+ }
2353
+ throw new Error(`the group did not converge in ${within} ms: ${last.why}`);
2354
+ },
2355
+ async flush(primary) {
2356
+ await cli(primary, ["FLUSHALL"]);
2357
+ const acked = Number(await cli(primary, [
2358
+ "WAIT",
2359
+ String(group.members.length - 1),
2360
+ "5000"
2361
+ ]));
2362
+ if (acked !== group.members.length - 1) throw new Error(`${acked} replicas acknowledged the flush`);
2363
+ },
2364
+ async reset() {
2365
+ for (const name of group.sentinels) await cli(name, [
2366
+ "SENTINEL",
2367
+ "RESET",
2368
+ group.name
2369
+ ]);
2370
+ },
2371
+ async holds(primary) {
2372
+ return (await cli(primary, [
2373
+ "--scan",
2374
+ "--pattern",
2375
+ "w:*"
2376
+ ])).split("\n").map((key) => Number(key.trim().slice(2))).filter((n) => Number.isInteger(n) && n > 0).sort((a, b) => a - b);
2377
+ },
2378
+ partOf: roles,
2379
+ async client(args) {
2380
+ const events = lines();
2381
+ const container = await new GenericContainer(node).withNetworkMode(`container:${holder.getId()}`).withCommand([
2382
+ "node",
2383
+ "/opt/harness/node/client-sentinel.mjs",
2384
+ ...args
2385
+ ]).withLogConsumer(events.consume).start();
2386
+ return {
2387
+ events: events.seen,
2388
+ until: (kinds, within) => events.until(kinds, within, "client"),
2389
+ stop: () => container.stop({ timeout: 2 }).then(() => void 0).catch(() => void 0)
2390
+ };
2391
+ },
2392
+ fault(kind, part, cuts, at) {
2393
+ const peers = peersOf(parts, clientIp, part, cuts).join(",");
2394
+ return stamped(injectorOf(part), [
2395
+ "/opt/harness/inject",
2396
+ kind,
2397
+ peers,
2398
+ ...after(at)
2399
+ ]);
2400
+ },
2401
+ lift(kind, part, cuts, at) {
2402
+ const peers = peersOf(parts, clientIp, part, cuts).join(",");
2403
+ return stamped(injectorOf(part), [
2404
+ "/opt/harness/inject",
2405
+ kind === "rst" ? "clear-rst" : "clear",
2406
+ peers,
2407
+ ...after(at)
2408
+ ]);
2409
+ },
2410
+ signal(part, signal, at) {
2411
+ const program = group.sentinels.includes(part) ? group.programs.sentinel : group.programs.server;
2412
+ return stamped(injectorOf(part), [
2413
+ "/opt/harness/inject",
2414
+ "signal",
2415
+ program,
2416
+ signal,
2417
+ ...after(at)
2418
+ ]);
2419
+ },
2420
+ async restart(member) {
2421
+ await output(box(member), [
2422
+ "sh",
2423
+ "-c",
2424
+ `${group.programs.server} /data/server.conf --daemonize yes`
2425
+ ]);
2426
+ const started = Date.now();
2427
+ while (await cli(member, ["PING"]).catch(() => "") !== "PONG") {
2428
+ if (Date.now() - started > 3e4) throw new Error(`${member} did not start again`);
2429
+ await sleep$1(200);
2430
+ }
2431
+ },
2432
+ failover(coordinated) {
2433
+ const sentinel = group.sentinels[0];
2434
+ return stamped(injectorOf(sentinel), [
2435
+ "/opt/harness/inject",
2436
+ "resp",
2437
+ `127.0.0.1:${group.sentinelPort}`,
2438
+ "SENTINEL",
2439
+ "FAILOVER",
2440
+ group.name,
2441
+ ...coordinated ? ["COORDINATED"] : []
2442
+ ]);
2443
+ },
2444
+ async forget() {
2445
+ await output(admin, [
2446
+ "ip",
2447
+ "tcp_metrics",
2448
+ "flush",
2449
+ "all"
2450
+ ]);
2451
+ },
2452
+ async settle() {
2453
+ const from = records.seen.length;
2454
+ const clock = await new Promise((resolve) => {
2455
+ const started = Date.now();
2456
+ const look = () => {
2457
+ const line = records.seen.slice(from).find((record) => record.ev === "clock");
2458
+ if (line !== void 0) resolve(line);
2459
+ else if (Date.now() - started > 5e3) resolve(null);
2460
+ else setTimeout(look, 50);
2461
+ };
2462
+ look();
2463
+ });
2464
+ return clock === null ? null : Number(clock["lost"] ?? 0);
2465
+ },
2466
+ async manifest(sysctls) {
2467
+ const config = await output(admin, ["zcat", "/proc/config.gz"]).catch(() => "");
2468
+ const inspect = async (container) => runtime.container.inspect(runtime.container.getById(container.getId()));
2469
+ const memberSettings = async (name) => {
2470
+ const read = {};
2471
+ for (const key of Object.keys(group.memberSettings)) read[key] = (await cli(name, [
2472
+ "CONFIG",
2473
+ "GET",
2474
+ key
2475
+ ])).split("\n")[1] ?? "";
2476
+ return read;
2477
+ };
2478
+ const sentinelSettings = async (name) => {
2479
+ const master = fieldsOf(await cli(name, [
2480
+ "SENTINEL",
2481
+ "MASTER",
2482
+ group.name
2483
+ ]))[0] ?? {};
2484
+ const read = { quorum: master["quorum"] ?? "" };
2485
+ for (const key of Object.keys(group.sentinelSettings)) read[key] = master[key] ?? "";
2486
+ return read;
2487
+ };
2488
+ const members = [];
2489
+ for (const name of group.members) members.push({
2490
+ name,
2491
+ ip: parts.members[name].ip,
2492
+ id: (await inspect(box(name))).Image,
2493
+ settings: await memberSettings(name)
2494
+ });
2495
+ const sentinels = [];
2496
+ for (const name of group.sentinels) sentinels.push({
2497
+ name,
2498
+ ip: parts.sentinels[name].ip,
2499
+ id: (await inspect(box(name))).Image,
2500
+ settings: await sentinelSettings(name)
2501
+ });
2502
+ return {
2503
+ kernel: await output(holder, ["uname", "-r"]),
2504
+ architecture: await output(holder, ["uname", "-m"]),
2505
+ hz: /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1] ?? null,
2506
+ boot_id: await output(holder, ["cat", "/proc/sys/kernel/random/boot_id"]),
2507
+ docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
2508
+ target: {
2509
+ image: group.image,
2510
+ id: members[0].id,
2511
+ settings: members[0].settings,
2512
+ sysctls: {}
2513
+ },
2514
+ harness: {
2515
+ image,
2516
+ id: (await inspect(holder)).Image,
2517
+ packages: (await output(holder, [
2518
+ "apk",
2519
+ "info",
2520
+ "-v"
2521
+ ])).split("\n").sort()
2522
+ },
2523
+ client: {
2524
+ kind: "ioredis",
2525
+ image: node
2526
+ },
2527
+ netns: {
2528
+ client: clientNetns,
2529
+ target: parts.members[first].netns
2530
+ },
2531
+ sysctls,
2532
+ group: {
2533
+ members,
2534
+ sentinels,
2535
+ observer: { image: node }
2536
+ }
2537
+ };
2538
+ },
2539
+ close
2540
+ };
2541
+ } catch (error) {
2542
+ await close();
2543
+ throw error;
2544
+ }
2545
+ };
2546
+ /** 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;
2548
+ /** This time is how long a run continues after its outcome, so that the record shows what came next. */
2549
+ const TAIL_MS = 2e3;
2550
+ /** This function gives the address of a member, with the port of the group. */
2551
+ const addressOf = (lab, member) => `${lab.members[member].ip}:${lab.group.port}`;
2552
+ /** This function gives the latest time that a group cell waits for its outcome, in ms from the fault. */
2553
+ const deadlineOf = (cell) => {
2554
+ const { prediction } = cell;
2555
+ return (prediction.detects === true ? valueIn(prediction.high, "s") : prediction.detects === false && prediction.recovers !== void 0 ? valueIn(prediction.recovers.high, "s") : valueIn(prediction.window, "s")) * 1e3 + 1e4;
2556
+ };
2557
+ /** This function gives the arguments of the client of a group cell. */
2558
+ const clientArgs = (cell, lab) => {
2559
+ return [
2560
+ "--sentinels",
2561
+ cell.group.sentinels.map((name) => `${lab.sentinels[name].ip}:${cell.group.sentinelPort}`).join(","),
2562
+ "--name",
2563
+ cell.group.name,
2564
+ "--interval-ms",
2565
+ String(cell.probe.interval === void 0 ? 50 : valueIn(cell.probe.interval, "ms")),
2566
+ "--max-s",
2567
+ String(Math.ceil(deadlineOf(cell) / 1e3 + 120)),
2568
+ ...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)]
2569
+ ];
2570
+ };
2571
+ /** 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) => {
2573
+ const started = Date.now();
2574
+ const look = () => {
2575
+ const found = client.events.find(match);
2576
+ if (found !== void 0 || Date.now() - started > within) resolve(found);
2577
+ else setTimeout(look, 20);
2578
+ };
2579
+ look();
2580
+ });
2581
+ /** This function gives the latest subscription of the observer to each Sentinel, in some lines of the observer. */
2582
+ const subscriptionsOf = (timeline) => {
2583
+ const latest = /* @__PURE__ */ new Map();
2584
+ for (const line of timeline) if (line.ev === "subscribed") latest.set(String(line.sentinel), line);
2585
+ return [...latest.values()];
2586
+ };
2587
+ /** This function gives the count of lost records in the latest clock line of the prober. */
2588
+ const droppedSoFar$1 = (records) => {
2589
+ for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
2590
+ return 0;
2591
+ };
2592
+ /**
2593
+ * This function does one run of a group cell:
2594
+ * - The group converges, and its keys go. The client connects through the Sentinels, and its
2595
+ * stream of writes gets steady.
2596
+ * - The fault occurs on its part, as the primary of this run makes the roles. A fault with a
2597
+ * hold lifts at its instant in the VM. A stall lifts at its length after the first write into it.
2598
+ * - The run waits for its outcome: the first write that a new primary accepts, or the first one
2599
+ * after the lift on the same primary. Then it continues for a moment.
2600
+ * - Each fault goes, the client stops, and the group converges again. The keys that it then holds
2601
+ * tell which writes it kept.
2602
+ */
2603
+ const groupOnce = async (lab, cell, environment, index) => {
2604
+ const group = cell.group;
2605
+ const primary = await lab.converge();
2606
+ await lab.flush(primary);
2607
+ await lab.forget();
2608
+ const from = lab.kernel.length;
2609
+ const timelineFrom = lab.timeline.length;
2610
+ const lostBefore = droppedSoFar$1(lab.kernel);
2611
+ const client = await lab.client(clientArgs(cell, lab));
2612
+ const lands = cell.prediction.group?.lands ?? "new";
2613
+ const place = cell.faultAt ?? { role: "primary" };
2614
+ let fault = null;
2615
+ let heal = null;
2616
+ let at = null;
2617
+ const undo = [];
2618
+ try {
2619
+ 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`);
2621
+ let healing = null;
2622
+ switch (cell.fault) {
2623
+ case "blackhole":
2624
+ case "partition":
2625
+ case "rst": {
2626
+ const kind = cell.fault === "rst" ? "rst" : "blackhole";
2627
+ const cuts = place.cuts ?? (cell.fault === "partition" ? "group" : "all");
2628
+ const part = lab.partOf(place.role, primary);
2629
+ at = part;
2630
+ fault = await lab.fault(kind, part, cuts);
2631
+ undo.push(() => lab.lift(kind, part, cuts));
2632
+ if (cell.stall !== void 0) {
2633
+ const t1 = fault.t1;
2634
+ const sent = await waitFor(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
2635
+ if (sent !== void 0) healing = lab.lift(kind, part, cuts, sent.t + valueIn(cell.stall, "s") * 1e9);
2636
+ } else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
2637
+ break;
2638
+ }
2639
+ case "freeze-process": {
2640
+ const part = lab.partOf(place.role, primary);
2641
+ at = part;
2642
+ fault = await lab.signal(part, "STOP");
2643
+ undo.push(() => lab.signal(part, "CONT"));
2644
+ if (cell.hold !== void 0) healing = lab.signal(part, "CONT", fault.t0 + valueIn(cell.hold, "s") * 1e9);
2645
+ break;
2646
+ }
2647
+ case "fin": {
2648
+ const part = lab.partOf(place.role, primary);
2649
+ at = part;
2650
+ fault = await lab.signal(part, "KILL");
2651
+ if (group.members.includes(part)) undo.push(() => lab.restart(part));
2652
+ break;
2653
+ }
2654
+ case "planned-failover":
2655
+ fault = await lab.failover(cell.coordinated === true);
2656
+ break;
2657
+ default: throw new Error(`a group cell has no fault ${cell.fault}`);
2658
+ }
2659
+ const healed = healing === null ? Promise.resolve(null) : healing.then((stamp) => {
2660
+ heal = stamp;
2661
+ undo.length = 0;
2662
+ return stamp;
2663
+ });
2664
+ healed.catch(() => void 0);
2665
+ const t0 = fault.t0;
2666
+ const old = addressOf(lab, primary);
2667
+ 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 () => {
2669
+ await healed;
2670
+ 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);
2673
+ await healed;
2674
+ } finally {
2675
+ for (const step of undo.reverse()) await step().catch(() => void 0);
2676
+ await client.stop();
2677
+ }
2678
+ const settled = await lab.settle();
2679
+ const lost = settled === null ? null : settled - lostBefore;
2680
+ const after = await lab.converge().catch(() => null);
2681
+ const holds = after === null ? null : await lab.holds(after);
2682
+ if (lab.timeline.slice(timelineFrom).some((line) => line.ev === "sentinel" && String(line.event).startsWith("-failover-abort"))) await lab.reset();
2683
+ return {
2684
+ environment,
2685
+ index,
2686
+ events: [...client.events],
2687
+ fault,
2688
+ ...heal === null ? {} : { heal },
2689
+ kernel: lab.kernel.slice(from),
2690
+ netns: lab.netns.client,
2691
+ targetNetns: lab.members[primary].netns,
2692
+ lost,
2693
+ group: {
2694
+ members: lab.members,
2695
+ sentinels: lab.sentinels,
2696
+ primary,
2697
+ at,
2698
+ timeline: [...subscriptionsOf(lab.timeline.slice(0, timelineFrom)), ...lab.timeline.slice(timelineFrom)],
2699
+ after,
2700
+ holds
2701
+ }
2702
+ };
2703
+ };
2704
+ //#endregion
1550
2705
  //#region src/run.ts
1551
2706
  /**
1552
2707
  * This function measures and judges the raw records of a cell, with the code of the harness at
@@ -1575,6 +2730,18 @@ const probeArgs = (cell, lab) => {
1575
2730
  "--mode",
1576
2731
  probe.mode
1577
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
+ }
1578
2745
  if (probe.request !== void 0) args.push("--request", probe.request);
1579
2746
  if (probe.count !== void 0) args.push("--count", String(probe.count));
1580
2747
  if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
@@ -1605,6 +2772,22 @@ const observerError = (cell, partial, within) => new Promise((resolve) => {
1605
2772
  });
1606
2773
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1607
2774
  /**
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.
2777
+ */
2778
+ const QUIET_MS = 250;
2779
+ /**
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.
2782
+ */
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
+ ];
2790
+ /**
1608
2791
  * This function waits for the request that an in-flight client sends on the signal: its second
1609
2792
  * send, after the PING that made it ready. Its stamp is when the request first met the fault.
1610
2793
  */
@@ -1640,6 +2823,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
1640
2823
  await lab.forget();
1641
2824
  const from = lab.kernel.length;
1642
2825
  const middleboxFrom = lab.middlebox.length;
2826
+ const serverFrom = lab.server.length;
1643
2827
  const lostBefore = droppedSoFar(lab.kernel);
1644
2828
  const probe = await lab.probe(probeArgs(cell, lab));
1645
2829
  const server = cell.observer === "server";
@@ -1681,7 +2865,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
1681
2865
  await sleep(200);
1682
2866
  } else await sleep(250);
1683
2867
  const faultAt = cell.faultAt ?? (server ? "client" : "target");
1684
- const towardServerComesIn = faultAt === "target" || typeof faultAt === "object" && faultAt.toward === "client";
2868
+ const towardServerComesIn = faultAt === "target" || isHopPlace(faultAt) && faultAt.toward === "client";
1685
2869
  const direction = cell.drops === void 0 ? void 0 : cell.drops === "to-server" === towardServerComesIn ? "in" : "out";
1686
2870
  const options = {
1687
2871
  ...direction === void 0 ? {} : { direction },
@@ -1702,11 +2886,12 @@ const once = async (lab, cell, environment, index, dry = false) => {
1702
2886
  return null;
1703
2887
  case "unreachable": return lab.fault("reject", faultAt, { code: cell.icmp ?? "host" });
1704
2888
  case "simplex": return lab.fault("blackhole", faultAt, options);
1705
- case "mtu-blackhole": return lab.fault("mtu", "client", { above: cell.mtu ?? 1200 });
2889
+ case "mtu-blackhole": return lab.fault("mtu", cell.faultAt ?? "client", { above: cell.mtu ?? 1200 });
1706
2890
  case "netem": return netemOn(cell.netem);
1707
2891
  case "rst": return lab.fault("rst", faultAt);
1708
2892
  case "middlebox-forget":
1709
2893
  case "proxy-idle":
2894
+ case "app-stall":
1710
2895
  case "none": return null;
1711
2896
  case "toxic":
1712
2897
  if (cell.toxic === void 0) throw new Error(`${cell.name} names no toxic`);
@@ -1724,10 +2909,11 @@ const once = async (lab, cell, environment, index, dry = false) => {
1724
2909
  case "middlebox-forget":
1725
2910
  case "proxy-idle":
1726
2911
  case "server-close":
2912
+ case "app-stall":
1727
2913
  case "none": return;
1728
2914
  case "unreachable": return lab.lift("reject", faultAt, { code: cell.icmp ?? "host" });
1729
2915
  case "simplex": return lab.lift("blackhole", faultAt, options);
1730
- case "mtu-blackhole": return lab.lift("mtu", "client", { above: cell.mtu ?? 1200 });
2916
+ case "mtu-blackhole": return lab.lift("mtu", cell.faultAt ?? "client", { above: cell.mtu ?? 1200 });
1731
2917
  case "netem":
1732
2918
  await netemOn(void 0);
1733
2919
  return;
@@ -1741,9 +2927,14 @@ const once = async (lab, cell, environment, index, dry = false) => {
1741
2927
  fault = await apply();
1742
2928
  try {
1743
2929
  if (cell.idleBefore !== void 0 && cell.probe.mode !== "idle") await sleep(valueIn(cell.idleBefore, "ms"));
1744
- if (cell.probe.mode !== "idle" && !replyInFlight) await go();
2930
+ if ((cell.probe.mode === "inflight" || cell.probe.mode === "stream") && !replyInFlight) await go();
1745
2931
  const { prediction } = cell;
1746
- if (cell.fault === "stall") {
2932
+ const http = cell.probe.http !== void 0;
2933
+ if (cell.probe.mode === "gap") {
2934
+ const gap = cell.probe.http?.gap === void 0 ? 0 : valueIn(cell.probe.http.gap, "s");
2935
+ const window = valueIn(prediction.detects ? prediction.high : prediction.window, "s");
2936
+ await probe.until(HTTP_DONE, (gap + window) * 2e3 + 1e4).catch(() => void 0);
2937
+ } else if (cell.fault === "stall") {
1747
2938
  if (cell.stall === void 0) throw new Error(`${cell.name} says no stall's length`);
1748
2939
  const sent = await requestSent(probe, 5e3);
1749
2940
  heal = await lab.heal(faultAt, sent.t + valueIn(cell.stall, "s") * 1e9);
@@ -1764,10 +2955,12 @@ const once = async (lab, cell, environment, index, dry = false) => {
1764
2955
  } else if (prediction.detects) {
1765
2956
  const within = valueIn(prediction.high, "s") * 2e3 + 1e4;
1766
2957
  if (cell.observer !== void 0 && cell.observer !== "client") await observerError(cell, partial, within);
2958
+ else if (http && cell.probe.mode !== "idle") await probe.until(HTTP_DONE, within).catch(() => void 0);
1767
2959
  else await probe.until([
1768
2960
  "error",
1769
2961
  "eof",
1770
- "deadline"
2962
+ "deadline",
2963
+ ...http ? ["closed"] : []
1771
2964
  ], within).catch(() => void 0);
1772
2965
  } else await sleep(valueIn(prediction.window, "s") * 1e3);
1773
2966
  } finally {
@@ -1792,7 +2985,8 @@ const once = async (lab, cell, environment, index, dry = false) => {
1792
2985
  targetNetns: lab.netns.target,
1793
2986
  lost,
1794
2987
  ...placed,
1795
- ...lab.topology === null ? {} : { middlebox: lab.middlebox.slice(middleboxFrom) }
2988
+ ...lab.topology === null ? {} : { middlebox: lab.middlebox.slice(middleboxFrom) },
2989
+ ...cell.target.lines === true ? { server: lab.server.slice(serverFrom) } : {}
1796
2990
  };
1797
2991
  };
1798
2992
  /** This function divides a count of runs across environments, as equally as possible. */
@@ -1808,6 +3002,7 @@ const line = (cell, run, read) => `${cell.name}: env ${run.environment} ${run.wa
1808
3002
  * of a cell whose fault ends the environment is a dry run, without the fault.
1809
3003
  */
1810
3004
  const inEnvironment = async (cell, environment, count, say) => {
3005
+ if (cell.group !== void 0) return inGroupEnvironment(cell, environment, count, say);
1811
3006
  const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
1812
3007
  try {
1813
3008
  const settings = await lab.configure(cell.targetSettings ?? {});
@@ -1816,7 +3011,7 @@ const inEnvironment = async (cell, environment, count, say) => {
1816
3011
  if (cell.fault === "mtu-blackhole" || cell.fault === "netem") {
1817
3012
  await lab.offload(false, "target");
1818
3013
  await lab.offload(false, "client");
1819
- if (typeof cell.faultAt === "object") await lab.offload(false, cell.faultAt);
3014
+ if (isHopPlace(cell.faultAt)) await lab.offload(false, cell.faultAt);
1820
3015
  }
1821
3016
  const latency = netemSpec(cell, void 0);
1822
3017
  if (latency !== null) await lab.netem(latency, "target");
@@ -1843,6 +3038,35 @@ const inEnvironment = async (cell, environment, count, say) => {
1843
3038
  }
1844
3039
  };
1845
3040
  /**
3041
+ * This function does the part of the runs of a group cell in one environment. The warm-up is a
3042
+ * whole run with its fault, because no fault of a group ends its environment. A server that a
3043
+ * fin stopped starts again, and the group converges before each run.
3044
+ */
3045
+ const inGroupEnvironment = async (cell, environment, count, say) => {
3046
+ const lab = await openGroupLab(cell.group, { watchSentinels: cell.calibrates === "sentinels" });
3047
+ try {
3048
+ const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
3049
+ const warmup = {
3050
+ ...await groupOnce(lab, cell, environment, -1),
3051
+ warmup: true
3052
+ };
3053
+ say(line(cell, warmup, measure(cell, warmup)));
3054
+ const runs = [];
3055
+ for (let index = 0; index < count; index += 1) {
3056
+ const run = await groupOnce(lab, cell, environment, index);
3057
+ runs.push(run);
3058
+ say(line(cell, run, measure(cell, run)));
3059
+ }
3060
+ return {
3061
+ manifest,
3062
+ runs,
3063
+ warmup
3064
+ };
3065
+ } finally {
3066
+ await lab.close();
3067
+ }
3068
+ };
3069
+ /**
1846
3070
  * This function does the runs of a cell. They go across new environments, and each environment records
1847
3071
  * its manifest. Then the verdict judges the runs. A median across the edge of the band extends
1848
3072
  * the cell from 11 runs to 22, and then to 44, each extension in new environments. With a
@@ -1891,6 +3115,6 @@ const runCell = async (cell, say = () => {}) => {
1891
3115
  }
1892
3116
  };
1893
3117
  //#endregion
1894
- export { LISTING, NOISE, PUMP, RELATIVE, STEPS, crossingsOf, deviceOf, endpointAfter, harnessImage, judge, measure, medianInterval, nodeClientImage, openLab, quantileOf, reorderingOf, routerImage, routesOf, runCell, socketOf, spread, spreadOf, templated, toxicBody, verdictOf };
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 };
1895
3119
 
1896
3120
  //# sourceMappingURL=index.js.map