@mark1russell7/harness 0.4.1 → 0.5.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,6 +5,10 @@ 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
9
13
  /**
10
14
  * This amount is the noise of the pipeline itself, δ_abs, as the calibration measured it. It is
@@ -111,6 +115,94 @@ const middle = (sorted) => {
111
115
  * which does not exist at this time.
112
116
  */
113
117
  const verdictOf = (prediction, runs) => {
118
+ const verdict = verdictOfBand(prediction, runs);
119
+ const counted = runs.filter((run) => run.void === null);
120
+ if (prediction.group === void 0 && !counted.some((run) => run.group !== void 0)) return verdict;
121
+ return {
122
+ ...verdict,
123
+ group: groupVerdictOf(prediction, counted)
124
+ };
125
+ };
126
+ /** This function gives the median of some counts, or null for none. */
127
+ const medianCount = (values) => values.length === 0 ? null : middle([...values].sort((a, b) => a - b));
128
+ /** This function sums what the group of a group cell did in its counted runs, and tests each stage that has a band. */
129
+ const groupVerdictOf = (prediction, counted) => {
130
+ const noise = valueIn(NOISE, "s");
131
+ const stamps = prediction.group?.stamps === void 0 ? 0 : valueIn(prediction.group.stamps, "s");
132
+ const stages = (prediction.group?.stages ?? []).map((band) => {
133
+ const interval = {
134
+ low: s(valueIn(band.low, "s") - noise),
135
+ high: s(valueIn(band.high, "s") + stamps + noise)
136
+ };
137
+ const outside = [];
138
+ const values = [];
139
+ for (const run of counted) {
140
+ const at = run.group?.stages[band.stage];
141
+ if (at === void 0) {
142
+ outside.push({
143
+ run: name(run),
144
+ why: `no Sentinel announced ${band.stage}`
145
+ });
146
+ continue;
147
+ }
148
+ const value = valueIn(at, "s");
149
+ values.push(value);
150
+ if (value < valueIn(interval.low, "s") || value > valueIn(interval.high, "s")) outside.push({
151
+ run: name(run),
152
+ why: `${band.stage} at ${value} s is outside [${interval.low.value}, ${interval.high.value}] s`
153
+ });
154
+ }
155
+ values.sort((a, b) => a - b);
156
+ return {
157
+ stage: band.stage,
158
+ band: interval,
159
+ median: values.length === 0 ? null : s(middle(values)),
160
+ outside,
161
+ decision: outside.length === 0 && values.length > 0 ? "holds" : "fails"
162
+ };
163
+ });
164
+ const medians = {};
165
+ const seen = new Set(counted.flatMap((run) => Object.keys(run.group?.stages ?? {})));
166
+ for (const stage of seen) {
167
+ const values = counted.flatMap((run) => {
168
+ const at = run.group?.stages[stage];
169
+ return at === void 0 ? [] : [valueIn(at, "s")];
170
+ });
171
+ medians[stage] = s(middle(values.sort((a, b) => a - b)));
172
+ }
173
+ const lostCounts = counted.flatMap((run) => run.group?.writes.lost === null || run.group === void 0 ? [] : [run.group.writes.lost.length]);
174
+ const oldCounts = counted.flatMap((run) => run.group === void 0 ? [] : [run.group.writes.old]);
175
+ const unknown = counted.flatMap((run) => run.group?.writes.unknown ?? []);
176
+ const errors = /* @__PURE__ */ new Map();
177
+ for (const run of counted) for (const error of run.group?.errors ?? []) errors.set(error.name, [...errors.get(error.name) ?? [], valueIn(error.first, "s")]);
178
+ const lags = counted.flatMap((run) => run.group?.lags ?? []);
179
+ return {
180
+ stages,
181
+ medians,
182
+ lost: {
183
+ runs: lostCounts.length,
184
+ total: lostCounts.reduce((sum, n) => sum + n, 0),
185
+ median: medianCount(lostCounts),
186
+ max: lostCounts.length === 0 ? null : Math.max(...lostCounts)
187
+ },
188
+ old: {
189
+ median: medianCount(oldCounts),
190
+ max: oldCounts.length === 0 ? null : Math.max(...oldCounts)
191
+ },
192
+ unknown: {
193
+ total: unknown.length,
194
+ held: unknown.filter((each) => each.held === true).length
195
+ },
196
+ errors: [...errors].map(([error, firsts]) => ({
197
+ name: error,
198
+ runs: firsts.length,
199
+ first: s(middle(firsts.sort((a, b) => a - b)))
200
+ })),
201
+ ...lags.length === 0 ? {} : { lags: spreadOf(lags) }
202
+ };
203
+ };
204
+ /** This function gives the verdict of the band of a cell: its containment and the equivalence of its median. */
205
+ const verdictOfBand = (prediction, runs) => {
114
206
  const counted = runs.filter((run) => run.void === null);
115
207
  const voided = runs.length - counted.length;
116
208
  const outside = counted.filter((run) => run.finding !== null).map((run) => ({
@@ -270,6 +362,252 @@ const verdictOf = (prediction, runs) => {
270
362
  };
271
363
  };
272
364
  //#endregion
365
+ //#region src/timeline.ts
366
+ /**
367
+ * This module measures a run of a group cell. It gives the first write that a primary accepted
368
+ * after the fault, and the timeline of the group as its Sentinels announced it. It also gives what
369
+ * became of each write.
370
+ */
371
+ /** These are the stages of a timeline, in their sequence after a fault. */
372
+ const STAGES = [
373
+ "+sdown",
374
+ "+odown",
375
+ "+try-failover",
376
+ "+elected-leader",
377
+ "+selected-slave",
378
+ "+promoted-slave",
379
+ "+switch-master",
380
+ "+convert-to-slave"
381
+ ];
382
+ const between$1 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
383
+ /** This function gives the member whose address and port is `at`, or null. */
384
+ const memberAt = (run, at) => {
385
+ if (typeof at !== "string") return null;
386
+ const ip = at.split(":")[0];
387
+ return Object.entries(run.group?.members ?? {}).find(([, part]) => part.ip === ip)?.[0] ?? null;
388
+ };
389
+ /** This function gives the time of each stage of a timeline from an instant: the first announcement of each, after the instant. */
390
+ const stagesOf = (timeline, from) => {
391
+ const out = {};
392
+ for (const line of timeline) {
393
+ if (line.ev !== "sentinel" || line.t < from) continue;
394
+ const stage = STAGES.find((each) => each === line.event);
395
+ if (stage === void 0 || out[stage] !== void 0) continue;
396
+ if ((stage === "+sdown" || stage === "+odown") && !String(line.detail ?? "").startsWith("master ")) continue;
397
+ out[stage] = between$1(from, line.t);
398
+ }
399
+ return out;
400
+ };
401
+ /** This function gives what became of each write of a run, from the lines of the client and the keys that the group kept. */
402
+ const writesOf = (run, after, primary) => {
403
+ const { events } = run;
404
+ const sent = events.filter((event) => event.ev === "sent");
405
+ const done = events.find((event) => event.ev === "done")?.t ?? sent.at(-1)?.t ?? Infinity;
406
+ const acks = /* @__PURE__ */ new Map();
407
+ for (const event of events) if (event.ev === "ack") acks.set(Number(event["n"]), event);
408
+ const holds = run.group?.holds ?? null;
409
+ const kept = holds === null ? null : new Set(holds);
410
+ const old = [...acks.values()].filter((event) => event.t > after && memberAt(run, event["at"]) === primary).length;
411
+ const lost = kept === null ? null : [...acks.keys()].filter((n) => !kept.has(n)).sort((a, b) => a - b);
412
+ const unknown = sent.filter((event) => event.t < done && !acks.has(Number(event["n"]))).map((event) => {
413
+ const n = Number(event["n"]);
414
+ return {
415
+ n,
416
+ held: kept === null ? null : kept.has(n)
417
+ };
418
+ });
419
+ return {
420
+ sent: sent.length,
421
+ acked: acks.size,
422
+ old,
423
+ lost,
424
+ unknown
425
+ };
426
+ };
427
+ /** This function gives the errors that the client met after an instant: each kind, its first time, and its count. */
428
+ const errorsOf = (events, after) => {
429
+ const done = events.find((event) => event.ev === "done")?.t ?? Infinity;
430
+ const kinds = /* @__PURE__ */ new Map();
431
+ for (const event of events) {
432
+ if (event.t <= after || event.t >= done) continue;
433
+ 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;
434
+ if (name === null) continue;
435
+ const seen = kinds.get(name);
436
+ if (seen === void 0) kinds.set(name, {
437
+ first: event.t,
438
+ count: 1
439
+ });
440
+ else seen.count += 1;
441
+ }
442
+ return [...kinds].map(([name, { first, count }]) => ({
443
+ name,
444
+ first: between$1(after, first),
445
+ count
446
+ }));
447
+ };
448
+ /**
449
+ * This function gives the delay of each stamp of the observer after the write of its Sentinel that
450
+ * the observer read. That write is the latest write of the Sentinel's socket toward the observer,
451
+ * at the stamp or before it. The prober records each write (`tx`) on the clock of the stamps.
452
+ */
453
+ const lagsOf = (run, kernel) => {
454
+ const timeline = run.group?.timeline ?? [];
455
+ const locals = /* @__PURE__ */ new Map();
456
+ for (const line of timeline) if (line.ev === "subscribed") locals.set(String(line.sentinel), String(line["local"]));
457
+ const out = [];
458
+ for (const line of timeline) {
459
+ if (line.ev !== "sentinel") continue;
460
+ const sentinel = String(line.sentinel);
461
+ const local = locals.get(sentinel);
462
+ if (local === void 0) continue;
463
+ const write = kernel.filter((record) => record.ev === "tx" && record.src === sentinel && record.dst === local && record.mono <= line.t).at(-1);
464
+ if (write !== void 0) out.push(between$1(write.mono, line.t));
465
+ }
466
+ return out;
467
+ };
468
+ /**
469
+ * This function reads one run of a group cell.
470
+ * - The reference is the start of the commit of the fault, as the injector stamped it. For a
471
+ * stall, it is the first write into the fault, as for the stall of a direct pair.
472
+ * - The outcome is the first write that a primary accepted after the fault. The prediction says
473
+ * which primary: a new one, or the same one after the lift.
474
+ * - Its time is the last segment with data that the client's socket to that primary received
475
+ * before the client saw the acceptance. The stamp of the client is the fallback.
476
+ * - A run is void when the prober lost records, or when the client did not become ready. It is
477
+ * also void when a Sentinel entered its TILT mode, or when the election split its votes. A band
478
+ * from the fault whose commit took longer than δ_abs is void too. So is a band whose hold lifted
479
+ * more than δ_abs late.
480
+ */
481
+ const measureGroup = (cell, run) => {
482
+ const { events, kernel } = run;
483
+ const { prediction } = cell;
484
+ const expected = prediction.group;
485
+ const primary = run.group?.primary ?? "";
486
+ const ready = events.find((event) => event.ev === "ready");
487
+ const t0 = run.fault?.t0;
488
+ const empty = {
489
+ from: primary,
490
+ to: null,
491
+ stages: {},
492
+ writes: {
493
+ sent: 0,
494
+ acked: 0,
495
+ old: 0,
496
+ lost: null,
497
+ unknown: []
498
+ },
499
+ errors: []
500
+ };
501
+ const silent = {
502
+ outcome: { kind: "silent" },
503
+ elapsed: null,
504
+ socket: null,
505
+ noticed: null,
506
+ finding: null
507
+ };
508
+ if (ready === void 0) return {
509
+ ...silent,
510
+ void: "the client never became ready",
511
+ group: empty
512
+ };
513
+ if (t0 === void 0 || run.fault === null) return {
514
+ ...silent,
515
+ void: "the fault never committed",
516
+ group: empty
517
+ };
518
+ if (run.lost === null) return {
519
+ ...silent,
520
+ void: "the prober wrote no clock line after the run, so nothing says what it dropped",
521
+ group: empty
522
+ };
523
+ if (run.lost > 0) return {
524
+ ...silent,
525
+ void: `the prober dropped ${run.lost} records`,
526
+ group: empty
527
+ };
528
+ const timeline = run.group?.timeline ?? [];
529
+ const stages = stagesOf(timeline, t0);
530
+ const heal = run.heal ?? null;
531
+ const lands = expected?.lands ?? "new";
532
+ const after = lands === "same" && heal !== null ? heal.t0 : t0;
533
+ const accepted = events.find((event) => {
534
+ if (event.ev !== "ack" || event.t <= after) return false;
535
+ const member = memberAt(run, event["at"]);
536
+ return member !== null && (lands === "new" ? member !== primary : true);
537
+ });
538
+ const to = accepted === void 0 ? null : memberAt(run, accepted["at"]);
539
+ const group = {
540
+ from: primary,
541
+ to,
542
+ stages,
543
+ writes: writesOf(run, t0, primary),
544
+ errors: errorsOf(events, t0),
545
+ ...cell.calibrates === "sentinels" ? { lags: lagsOf(run, kernel) } : {}
546
+ };
547
+ let voided = null;
548
+ const tilt = timeline.find((line) => line.ev === "sentinel" && line.event === "+tilt" && line.t >= t0);
549
+ 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`;
550
+ const split = timeline.find((line) => line.ev === "sentinel" && line.event === "-failover-abort-not-elected" && line.t >= t0);
551
+ const elected = timeline.find((line) => line.ev === "sentinel" && line.event === "+elected-leader" && line.t >= t0);
552
+ 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";
553
+ const odown = stages["+odown"];
554
+ 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";
555
+ if (voided === null && cell.hold !== void 0 && prediction.detects !== null) {
556
+ if (heal === null) voided = "the fault never lifted";
557
+ else {
558
+ const late = (heal.t0 - t0) / 1e9 - valueIn(cell.hold, "s");
559
+ 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`;
560
+ }
561
+ }
562
+ const assumed = prediction.detects === null ? void 0 : prediction.rtt;
563
+ const first = kernel.find((record) => record.netns === run.netns && record.dst === `${run.group?.members[primary]?.ip}:${cell.group?.port}` && record.tcp?.srtt !== void 0);
564
+ const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
565
+ if (voided === null && assumed !== void 0 && rtt !== null && rtt > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
566
+ 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`;
567
+ let reference = t0;
568
+ if (prediction.detects === false && prediction.recovers !== void 0) {
569
+ const into = events.find((event) => event.ev === "sent" && event.t > run.fault.t1);
570
+ if (into === void 0 || heal !== null && into.t > heal.t0) voided ??= "the write in flight went out before the fault held";
571
+ else {
572
+ reference = into.t;
573
+ const { heal: bounds } = prediction.recovers;
574
+ if (heal === null) voided ??= "the stall never ended";
575
+ 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`;
576
+ }
577
+ }
578
+ const findings = [];
579
+ const switched = stages["+switch-master"];
580
+ if (expected?.failover === false && switched !== void 0) findings.push(`the group failed over ${switched.value} s after the fault, where it should not`);
581
+ if (accepted !== void 0 && lands === "same" && to !== primary) findings.push(`the client wrote to ${to ?? String(accepted["at"])}, where it should stay on ${primary}`);
582
+ for (const error of expected?.errors ?? []) {
583
+ const met = group.errors.find((each) => each.name === error.name);
584
+ const noise = valueIn(NOISE, "s");
585
+ if (met === void 0) findings.push(`the client never met ${error.name}`);
586
+ 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`);
587
+ }
588
+ if (accepted === void 0) {
589
+ 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");
590
+ return {
591
+ ...silent,
592
+ void: voided,
593
+ finding: findings[0] ?? null,
594
+ group
595
+ };
596
+ }
597
+ const address = String(accepted["at"]);
598
+ const segment = kernel.filter((record) => record.netns === run.netns && record.ev === "rx" && record.dst === address && (record.seg?.len.value ?? 0) > 0 && record.mono <= accepted.t && record.mono > after).at(-1);
599
+ const at = segment?.mono ?? accepted.t;
600
+ return {
601
+ outcome: { kind: "answered" },
602
+ elapsed: between$1(reference, at),
603
+ socket: between$1(reference, accepted.t),
604
+ noticed: segment === void 0 ? null : between$1(segment.mono, accepted.t),
605
+ void: voided,
606
+ finding: findings[0] ?? null,
607
+ group
608
+ };
609
+ };
610
+ //#endregion
273
611
  //#region src/observe.ts
274
612
  /** These are the errno numbers that the kernel reports to a socket, by the names that predictions use. */
275
613
  const ERRNO = {
@@ -425,6 +763,24 @@ const crossingsOf = (run, hop, toClient, toTarget) => {
425
763
  * prober lost is also void.
426
764
  */
427
765
  const measure = (cell, run) => {
766
+ if (cell.group !== void 0) {
767
+ const read = measureGroup(cell, run);
768
+ return {
769
+ environment: run.environment,
770
+ index: run.index,
771
+ outcome: read.outcome,
772
+ elapsed: read.elapsed,
773
+ socket: read.socket,
774
+ noticed: read.noticed,
775
+ steps: [],
776
+ probes: 0,
777
+ retransmissions: run.kernel.filter((record) => record.netns === run.netns && record.ev === "retrans").length,
778
+ acks: read.group.writes.acked,
779
+ void: read.void,
780
+ finding: read.finding,
781
+ group: read.group
782
+ };
783
+ }
428
784
  const { events, kernel } = run;
429
785
  const ready = events.find((event) => event.ev === "ready");
430
786
  const local = events.find((event) => event.ev === "connected")?.["local"];
@@ -651,13 +1007,25 @@ const measure = (cell, run) => {
651
1007
  if (!prediction.detects) {
652
1008
  const window = valueIn(prediction.window, "s") * 1e9;
653
1009
  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;
1010
+ const forgetIn = forgotten !== void 0 && forgotten.t - request.t <= window ? forgotten : void 0;
1011
+ const expected = prediction.middlebox?.forgets;
1012
+ const after = forgetIn === void 0 ? null : between(lastRx, forgetIn.t);
1013
+ let finding = null;
1014
+ if (expected === void 0) {
1015
+ if (after !== null) finding = `a router forgot the flow ${after.value} s after the last segment, where it should keep it`;
1016
+ } else if (after === null) finding = "no router forgot the flow in the window, where it should forget it";
1017
+ else {
1018
+ const noise = valueIn(NOISE, "s");
1019
+ const low = valueIn(expected.low, "s") - noise;
1020
+ const high = valueIn(expected.high, "s") + noise;
1021
+ 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`;
1022
+ }
655
1023
  return {
656
1024
  ...base,
657
1025
  ...nothing,
658
1026
  ...chain,
659
1027
  void: voided,
660
- finding: forgot
1028
+ finding
661
1029
  };
662
1030
  }
663
1031
  return {
@@ -888,6 +1256,11 @@ const lines = (also) => {
888
1256
  }
889
1257
  };
890
1258
  };
1259
+ /** This function makes a container share the process namespace of another container, for example `container:<id>`. */
1260
+ const withPid = (container, mode) => {
1261
+ container.hostConfig.PidMode = mode;
1262
+ return container;
1263
+ };
891
1264
  /** This container class can share the process namespace of another container. */
892
1265
  const shared = (Base) => class extends Base {
893
1266
  withPidMode(mode) {
@@ -1207,17 +1580,22 @@ const openLab = async (target, kind = "tcp", topology) => {
1207
1580
  * on its sockets. A router forwards past INPUT and OUTPUT, thus its rules are FORWARD rules on
1208
1581
  * its device toward the side (`--via`).
1209
1582
  */
1210
- const placeOf = (at) => {
1211
- if (at === "client") return [
1583
+ const hopPlace = (at) => {
1584
+ if (!isHopPlace(at)) throw new Error(`a lab of a topology has no part ${JSON.stringify(at)}: a group cell opens a group lab`);
1585
+ return at;
1586
+ };
1587
+ const placeOf = (place) => {
1588
+ if (place === "client") return [
1212
1589
  admin,
1213
1590
  targetIp,
1214
1591
  []
1215
1592
  ];
1216
- if (at === "target") return [
1593
+ if (place === "target") return [
1217
1594
  injector,
1218
1595
  targetPeer,
1219
1596
  []
1220
1597
  ];
1598
+ const at = hopPlace(place);
1221
1599
  const hop = hopNamed(at.hop);
1222
1600
  const peer = at.toward === "client" ? hop.peer.client : hop.peer.target.split(":")[0];
1223
1601
  const via = hop.hop.kind === "router" ? ["--via", hop.devices[at.toward]] : [];
@@ -1228,9 +1606,10 @@ const openLab = async (target, kind = "tcp", topology) => {
1228
1606
  ];
1229
1607
  };
1230
1608
  /** 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"];
1609
+ const deviceAt = (place) => {
1610
+ if (place === "client") return [admin, "eth0"];
1611
+ if (place === "target") return [injector, "eth0"];
1612
+ const at = hopPlace(place);
1234
1613
  const hop = hopNamed(at.hop);
1235
1614
  return [hop.injector, hop.devices[at.toward]];
1236
1615
  };
@@ -1340,7 +1719,7 @@ const openLab = async (target, kind = "tcp", topology) => {
1340
1719
  return stamped(where, args);
1341
1720
  },
1342
1721
  async offload(on, at = "target") {
1343
- const devices = typeof at === "object" ? Object.values(hopNamed(at.hop).devices) : [deviceAt(at)[1]];
1722
+ const devices = isHopPlace(at) ? Object.values(hopNamed(at.hop).devices) : [deviceAt(at)[1]];
1344
1723
  const [where] = deviceAt(at);
1345
1724
  for (const device of devices) await output(where, [
1346
1725
  "/opt/harness/inject",
@@ -1547,6 +1926,673 @@ const openLab = async (target, kind = "tcp", topology) => {
1547
1926
  }
1548
1927
  };
1549
1928
  //#endregion
1929
+ //#region src/group.ts
1930
+ /** This function gives the configuration file of a member: its port, its settings, and the primary that a replica follows. */
1931
+ const memberConfig = (group, primary) => [
1932
+ `port ${group.port}`,
1933
+ ...Object.entries(group.memberSettings).map(([key, value]) => `${key} ${value}`),
1934
+ ...primary === null ? [] : [`replicaof ${primary} ${group.port}`],
1935
+ ""
1936
+ ].join("\n");
1937
+ /** This function gives the configuration file of a Sentinel: its port, the group that it monitors, and the settings of the group. */
1938
+ const sentinelConfig = (group, primary) => [
1939
+ `port ${group.sentinelPort}`,
1940
+ `sentinel monitor ${group.name} ${primary} ${group.port} ${group.quorum}`,
1941
+ ...Object.entries(group.sentinelSettings).map(([key, value]) => `sentinel ${key} ${group.name} ${value}`),
1942
+ ""
1943
+ ].join("\n");
1944
+ /**
1945
+ * This function gives the peers that a fault on a part cuts it from, by their addresses. `all` is
1946
+ * each other part and the client, `group` is the other parts, and `clients` is the client. The
1947
+ * observer is not a peer, thus the timeline of the group stays whole.
1948
+ */
1949
+ const peersOf = (parts, client, part, cuts) => {
1950
+ const others = [...Object.entries(parts.members), ...Object.entries(parts.sentinels)].filter(([name]) => name !== part).map(([, at]) => at.ip);
1951
+ if (cuts === "clients") return [client];
1952
+ if (cuts === "group") return others;
1953
+ return [...others, client];
1954
+ };
1955
+ /** This function reads the flat reply of a command line client into its fields: name, value, name, value. */
1956
+ const fieldsOf = (text) => {
1957
+ const out = [];
1958
+ const words = text.split("\n").map((line) => line.replace(/\r$/, ""));
1959
+ let current = null;
1960
+ for (let at = 0; at + 1 < words.length; at += 2) {
1961
+ const [key, value] = [words[at], words[at + 1]];
1962
+ if (key === "name" || current === null) {
1963
+ current = {};
1964
+ out.push(current);
1965
+ }
1966
+ current[key] = value;
1967
+ }
1968
+ return out;
1969
+ };
1970
+ /** This function reads the INFO of a server into its fields. */
1971
+ const infoOf = (text) => {
1972
+ const out = {};
1973
+ for (const line of text.split("\n")) {
1974
+ const at = line.indexOf(":");
1975
+ if (line.startsWith("#") || at === -1) continue;
1976
+ out[line.slice(0, at)] = line.slice(at + 1).trim();
1977
+ }
1978
+ return out;
1979
+ };
1980
+ /** This function judges the views of the Sentinels and of the members: the primary that they agree on, if the group is healthy. */
1981
+ const viewOf = (group, ips, sentinels, members) => {
1982
+ const others = String(group.sentinels.length - 1);
1983
+ const replicas = group.members.length - 1;
1984
+ const addresses = new Set(sentinels.map((each) => each.master["ip"]));
1985
+ if (addresses.size !== 1) return {
1986
+ primary: null,
1987
+ healthy: false,
1988
+ why: `the Sentinels name ${addresses.size} primaries`
1989
+ };
1990
+ const ip = [...addresses][0];
1991
+ const primary = Object.entries(ips).find(([, at]) => at === ip)?.[0] ?? null;
1992
+ if (primary === null) return {
1993
+ primary: null,
1994
+ healthy: false,
1995
+ why: `no member has the address ${ip}`
1996
+ };
1997
+ for (const [at, each] of sentinels.entries()) {
1998
+ if (each.master["flags"] !== "master") return {
1999
+ primary,
2000
+ healthy: false,
2001
+ why: `Sentinel ${at} flags the primary ${each.master["flags"]}`
2002
+ };
2003
+ if (each.master["num-other-sentinels"] !== others) return {
2004
+ primary,
2005
+ healthy: false,
2006
+ why: `Sentinel ${at} knows ${each.master["num-other-sentinels"]} other Sentinels`
2007
+ };
2008
+ const good = each.replicas.filter((replica) => replica["flags"] === "slave" && replica["master-link-status"] === "ok" && replica["master-host"] === ip);
2009
+ if (good.length !== replicas || each.replicas.length !== replicas) return {
2010
+ primary,
2011
+ healthy: false,
2012
+ why: `Sentinel ${at} has ${good.length} healthy replicas of ${each.replicas.length}`
2013
+ };
2014
+ }
2015
+ const info = members[primary] ?? {};
2016
+ if (info["role"] !== "master") return {
2017
+ primary,
2018
+ healthy: false,
2019
+ why: `${primary} reports the role ${info["role"]}`
2020
+ };
2021
+ if (info["master_failover_state"] !== void 0 && info["master_failover_state"] !== "no-failover") return {
2022
+ primary,
2023
+ healthy: false,
2024
+ why: `${primary} is in the failover state ${info["master_failover_state"]}`
2025
+ };
2026
+ const online = Object.entries(info).filter(([key, value]) => /^slave\d+$/.test(key) && value.includes("state=online")).length;
2027
+ if (online !== replicas) return {
2028
+ primary,
2029
+ healthy: false,
2030
+ why: `${primary} has ${online} replicas online`
2031
+ };
2032
+ for (const name of group.members) {
2033
+ if (name === primary) continue;
2034
+ const each = members[name] ?? {};
2035
+ if (each["role"] !== "slave" || each["master_host"] !== ip || each["master_link_status"] !== "up") return {
2036
+ primary,
2037
+ healthy: false,
2038
+ why: `${name} is not a replica of ${primary} with its link up`
2039
+ };
2040
+ }
2041
+ return {
2042
+ primary,
2043
+ healthy: true,
2044
+ why: "healthy"
2045
+ };
2046
+ };
2047
+ const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2048
+ /** This time is the longest that a group takes to converge before a run, or after it. */
2049
+ const CONVERGE_MS = 24e4;
2050
+ /**
2051
+ * This function opens one environment of a group cell:
2052
+ * - one new network, with each member, each Sentinel, the client and the observer on it.
2053
+ * - each member: the stock image, its server as the child of a shell, thus a fin can stop the
2054
+ * server and the container stays up. The first member is the primary, and the others follow it.
2055
+ * - each Sentinel: the stock image, its Sentinel as the child of a shell, which monitors the first member.
2056
+ * - an injector sidecar for each part, in its network and process namespaces.
2057
+ * - a client that holds its own network namespace, with a privileged sidecar for its sysctls.
2058
+ * - the observer of the Sentinels, in a namespace of its own.
2059
+ * - the prober, which watches the namespace of the client, and of each Sentinel for a calibration.
2060
+ */
2061
+ const openGroupLab = async (group, options = {}) => {
2062
+ const image = await harnessImage();
2063
+ const node = await nodeClientImage();
2064
+ const { GenericContainer, Network, Wait, getContainerRuntimeClient } = await containers();
2065
+ const runtime = await getContainerRuntimeClient();
2066
+ const opened = [];
2067
+ let network;
2068
+ const close = async () => {
2069
+ for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
2070
+ await network?.stop().catch(() => void 0);
2071
+ };
2072
+ const ipOf = async (container) => {
2073
+ const ip = (await runtime.container.inspect(runtime.container.getById(container.getId()))).NetworkSettings.Networks[network.getName()]?.IPAddress;
2074
+ if (ip === void 0 || ip === "") throw new Error(`${container.getName()} has no address on ${network.getName()}`);
2075
+ return ip;
2076
+ };
2077
+ try {
2078
+ network = await new Network().start();
2079
+ const boxes = /* @__PURE__ */ new Map();
2080
+ const injectors = /* @__PURE__ */ new Map();
2081
+ const parts = {
2082
+ members: {},
2083
+ sentinels: {}
2084
+ };
2085
+ /** This function starts one part: the stock image with its configuration, its program as the child of a shell, and its injector. */
2086
+ const start = async (name, program, file, content, ready) => {
2087
+ const box = await new GenericContainer(group.image).withNetwork(network).withNetworkAliases(name).withCopyContentToContainer([{
2088
+ content,
2089
+ target: file
2090
+ }]).withCommand([
2091
+ "sh",
2092
+ "-c",
2093
+ `${program} ${file} & while :; do sleep 3600; done`
2094
+ ]).withWaitStrategy(Wait.forLogMessage(ready)).start();
2095
+ opened.push(box);
2096
+ boxes.set(name, box);
2097
+ const injector = await withPid(new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${box.getId()}`).start();
2098
+ opened.push(injector);
2099
+ injectors.set(name, injector);
2100
+ return {
2101
+ ip: await ipOf(box),
2102
+ netns: await netnsOf(injector)
2103
+ };
2104
+ };
2105
+ const [first, ...rest] = group.members;
2106
+ if (first === void 0) throw new Error("a group has no member");
2107
+ parts.members[first] = await start(first, group.programs.server, "/data/server.conf", memberConfig(group, null), group.ready.member);
2108
+ const primaryIp = parts.members[first].ip;
2109
+ for (const name of rest) parts.members[name] = await start(name, group.programs.server, "/data/server.conf", memberConfig(group, primaryIp), group.ready.member);
2110
+ for (const name of group.sentinels) parts.sentinels[name] = await start(name, group.programs.sentinel, "/data/sentinel.conf", sentinelConfig(group, primaryIp), group.ready.sentinel);
2111
+ const holder = await new GenericContainer(image).withNetwork(network).start();
2112
+ opened.push(holder);
2113
+ const admin = await new GenericContainer(image).withNetworkMode(`container:${holder.getId()}`).withPrivilegedMode().start();
2114
+ opened.push(admin);
2115
+ const clientIp = await ipOf(holder);
2116
+ const sentinelAddresses = group.sentinels.map((name) => `${parts.sentinels[name].ip}:${group.sentinelPort}`);
2117
+ const timeline = lines();
2118
+ const observer = await new GenericContainer(node).withNetwork(network).withCommand([
2119
+ "node",
2120
+ "/opt/harness/node/observer.mjs",
2121
+ sentinelAddresses.join(",")
2122
+ ]).withLogConsumer(timeline.consume).start();
2123
+ opened.push(observer);
2124
+ const observerIp = await ipOf(observer);
2125
+ for (const address of sentinelAddresses) await new Promise((resolve, reject) => {
2126
+ const started = Date.now();
2127
+ const look = () => {
2128
+ if (timeline.seen.some((line) => line.ev === "subscribed" && line["sentinel"] === address)) resolve();
2129
+ else if (Date.now() - started > 3e4) reject(/* @__PURE__ */ new Error(`the observer never subscribed to ${address}`));
2130
+ else setTimeout(look, 50);
2131
+ };
2132
+ look();
2133
+ });
2134
+ const clientNetns = await netnsOf(admin);
2135
+ const watched = [clientNetns, ...options.watchSentinels === true ? group.sentinels.map((name) => parts.sentinels[name].netns) : []];
2136
+ const records = lines();
2137
+ const prober = await new GenericContainer(image).withPrivilegedMode().withNetworkMode("none").withCommand(["/opt/harness/prober", ...watched.map(String)]).withLogConsumer(records.consume).start();
2138
+ opened.push(prober);
2139
+ const begun = await records.until(["begin", "fail"], 3e4, "prober");
2140
+ if (begun.ev === "fail") throw new Error(`the prober failed: ${String(begun["why"])}`);
2141
+ const box = (name) => {
2142
+ const found = boxes.get(name);
2143
+ if (found === void 0) throw new Error(`the group has no part ${name}`);
2144
+ return found;
2145
+ };
2146
+ const injectorOf = (name) => {
2147
+ const found = injectors.get(name);
2148
+ if (found === void 0) throw new Error(`the group has no part ${name}`);
2149
+ return found;
2150
+ };
2151
+ const cli = (name, args) => {
2152
+ const port = group.sentinels.includes(name) ? group.sentinelPort : group.port;
2153
+ return output(box(name), [
2154
+ group.programs.cli,
2155
+ "-p",
2156
+ String(port),
2157
+ ...args
2158
+ ]);
2159
+ };
2160
+ const stamped = async (where, argv) => {
2161
+ const stamp = JSON.parse((await output(where, argv)).split("\n").at(-1));
2162
+ return {
2163
+ t0: stamp.t0,
2164
+ t1: stamp.t1
2165
+ };
2166
+ };
2167
+ const ips = Object.fromEntries(Object.entries(parts.members).map(([name, at]) => [name, at.ip]));
2168
+ const view = async () => {
2169
+ const sentinels = [];
2170
+ for (const name of group.sentinels) {
2171
+ const master = fieldsOf(await cli(name, [
2172
+ "SENTINEL",
2173
+ "MASTER",
2174
+ group.name
2175
+ ]))[0] ?? {};
2176
+ const replicas = fieldsOf(await cli(name, [
2177
+ "SENTINEL",
2178
+ "REPLICAS",
2179
+ group.name
2180
+ ]));
2181
+ sentinels.push({
2182
+ master,
2183
+ replicas
2184
+ });
2185
+ }
2186
+ const members = {};
2187
+ for (const name of group.members) members[name] = infoOf(await cli(name, ["INFO", "replication"]));
2188
+ return viewOf(group, ips, sentinels, members);
2189
+ };
2190
+ const after = (at) => at === void 0 ? [] : ["--at", String(Math.round(at))];
2191
+ const roles = (role, primary) => {
2192
+ if (role === "primary") return primary;
2193
+ if (role === "sentinel") return group.sentinels[0];
2194
+ const replica = group.members.find((name) => name !== primary);
2195
+ if (replica === void 0) throw new Error("the group has no replica");
2196
+ return replica;
2197
+ };
2198
+ return {
2199
+ group,
2200
+ clientIp,
2201
+ observerIp,
2202
+ members: parts.members,
2203
+ sentinels: parts.sentinels,
2204
+ netns: {
2205
+ client: clientNetns,
2206
+ watched
2207
+ },
2208
+ kernel: records.seen,
2209
+ timeline: timeline.seen,
2210
+ async sysctl(values) {
2211
+ const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
2212
+ if (settings.length > 0) await output(admin, [
2213
+ "sysctl",
2214
+ "-q",
2215
+ "-w",
2216
+ ...settings
2217
+ ]);
2218
+ const read = {};
2219
+ for (const key of Object.keys(values)) read[key] = await output(admin, [
2220
+ "sysctl",
2221
+ "-n",
2222
+ key
2223
+ ]);
2224
+ return read;
2225
+ },
2226
+ async converge(within = CONVERGE_MS) {
2227
+ const started = Date.now();
2228
+ let calm = 0;
2229
+ let last = {
2230
+ primary: null,
2231
+ healthy: false,
2232
+ why: "no view yet"
2233
+ };
2234
+ while (Date.now() - started < within) {
2235
+ last = await view().catch((error) => ({
2236
+ primary: null,
2237
+ healthy: false,
2238
+ why: error.message
2239
+ }));
2240
+ calm = last.healthy ? calm + 1 : 0;
2241
+ if (calm >= 2 && last.primary !== null) return last.primary;
2242
+ await sleep$1(1e3);
2243
+ }
2244
+ throw new Error(`the group did not converge in ${within} ms: ${last.why}`);
2245
+ },
2246
+ async flush(primary) {
2247
+ await cli(primary, ["FLUSHALL"]);
2248
+ const acked = Number(await cli(primary, [
2249
+ "WAIT",
2250
+ String(group.members.length - 1),
2251
+ "5000"
2252
+ ]));
2253
+ if (acked !== group.members.length - 1) throw new Error(`${acked} replicas acknowledged the flush`);
2254
+ },
2255
+ async reset() {
2256
+ for (const name of group.sentinels) await cli(name, [
2257
+ "SENTINEL",
2258
+ "RESET",
2259
+ group.name
2260
+ ]);
2261
+ },
2262
+ async holds(primary) {
2263
+ return (await cli(primary, [
2264
+ "--scan",
2265
+ "--pattern",
2266
+ "w:*"
2267
+ ])).split("\n").map((key) => Number(key.trim().slice(2))).filter((n) => Number.isInteger(n) && n > 0).sort((a, b) => a - b);
2268
+ },
2269
+ partOf: roles,
2270
+ async client(args) {
2271
+ const events = lines();
2272
+ const container = await new GenericContainer(node).withNetworkMode(`container:${holder.getId()}`).withCommand([
2273
+ "node",
2274
+ "/opt/harness/node/client-sentinel.mjs",
2275
+ ...args
2276
+ ]).withLogConsumer(events.consume).start();
2277
+ return {
2278
+ events: events.seen,
2279
+ until: (kinds, within) => events.until(kinds, within, "client"),
2280
+ stop: () => container.stop({ timeout: 2 }).then(() => void 0).catch(() => void 0)
2281
+ };
2282
+ },
2283
+ fault(kind, part, cuts, at) {
2284
+ const peers = peersOf(parts, clientIp, part, cuts).join(",");
2285
+ return stamped(injectorOf(part), [
2286
+ "/opt/harness/inject",
2287
+ kind,
2288
+ peers,
2289
+ ...after(at)
2290
+ ]);
2291
+ },
2292
+ lift(kind, part, cuts, at) {
2293
+ const peers = peersOf(parts, clientIp, part, cuts).join(",");
2294
+ return stamped(injectorOf(part), [
2295
+ "/opt/harness/inject",
2296
+ kind === "rst" ? "clear-rst" : "clear",
2297
+ peers,
2298
+ ...after(at)
2299
+ ]);
2300
+ },
2301
+ signal(part, signal, at) {
2302
+ const program = group.sentinels.includes(part) ? group.programs.sentinel : group.programs.server;
2303
+ return stamped(injectorOf(part), [
2304
+ "/opt/harness/inject",
2305
+ "signal",
2306
+ program,
2307
+ signal,
2308
+ ...after(at)
2309
+ ]);
2310
+ },
2311
+ async restart(member) {
2312
+ await output(box(member), [
2313
+ "sh",
2314
+ "-c",
2315
+ `${group.programs.server} /data/server.conf --daemonize yes`
2316
+ ]);
2317
+ const started = Date.now();
2318
+ while (await cli(member, ["PING"]).catch(() => "") !== "PONG") {
2319
+ if (Date.now() - started > 3e4) throw new Error(`${member} did not start again`);
2320
+ await sleep$1(200);
2321
+ }
2322
+ },
2323
+ failover(coordinated) {
2324
+ const sentinel = group.sentinels[0];
2325
+ return stamped(injectorOf(sentinel), [
2326
+ "/opt/harness/inject",
2327
+ "resp",
2328
+ `127.0.0.1:${group.sentinelPort}`,
2329
+ "SENTINEL",
2330
+ "FAILOVER",
2331
+ group.name,
2332
+ ...coordinated ? ["COORDINATED"] : []
2333
+ ]);
2334
+ },
2335
+ async forget() {
2336
+ await output(admin, [
2337
+ "ip",
2338
+ "tcp_metrics",
2339
+ "flush",
2340
+ "all"
2341
+ ]);
2342
+ },
2343
+ async settle() {
2344
+ const from = records.seen.length;
2345
+ const clock = await new Promise((resolve) => {
2346
+ const started = Date.now();
2347
+ const look = () => {
2348
+ const line = records.seen.slice(from).find((record) => record.ev === "clock");
2349
+ if (line !== void 0) resolve(line);
2350
+ else if (Date.now() - started > 5e3) resolve(null);
2351
+ else setTimeout(look, 50);
2352
+ };
2353
+ look();
2354
+ });
2355
+ return clock === null ? null : Number(clock["lost"] ?? 0);
2356
+ },
2357
+ async manifest(sysctls) {
2358
+ const config = await output(admin, ["zcat", "/proc/config.gz"]).catch(() => "");
2359
+ const inspect = async (container) => runtime.container.inspect(runtime.container.getById(container.getId()));
2360
+ const memberSettings = async (name) => {
2361
+ const read = {};
2362
+ for (const key of Object.keys(group.memberSettings)) read[key] = (await cli(name, [
2363
+ "CONFIG",
2364
+ "GET",
2365
+ key
2366
+ ])).split("\n")[1] ?? "";
2367
+ return read;
2368
+ };
2369
+ const sentinelSettings = async (name) => {
2370
+ const master = fieldsOf(await cli(name, [
2371
+ "SENTINEL",
2372
+ "MASTER",
2373
+ group.name
2374
+ ]))[0] ?? {};
2375
+ const read = { quorum: master["quorum"] ?? "" };
2376
+ for (const key of Object.keys(group.sentinelSettings)) read[key] = master[key] ?? "";
2377
+ return read;
2378
+ };
2379
+ const members = [];
2380
+ for (const name of group.members) members.push({
2381
+ name,
2382
+ ip: parts.members[name].ip,
2383
+ id: (await inspect(box(name))).Image,
2384
+ settings: await memberSettings(name)
2385
+ });
2386
+ const sentinels = [];
2387
+ for (const name of group.sentinels) sentinels.push({
2388
+ name,
2389
+ ip: parts.sentinels[name].ip,
2390
+ id: (await inspect(box(name))).Image,
2391
+ settings: await sentinelSettings(name)
2392
+ });
2393
+ return {
2394
+ kernel: await output(holder, ["uname", "-r"]),
2395
+ architecture: await output(holder, ["uname", "-m"]),
2396
+ hz: /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1] ?? null,
2397
+ boot_id: await output(holder, ["cat", "/proc/sys/kernel/random/boot_id"]),
2398
+ docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
2399
+ target: {
2400
+ image: group.image,
2401
+ id: members[0].id,
2402
+ settings: members[0].settings,
2403
+ sysctls: {}
2404
+ },
2405
+ harness: {
2406
+ image,
2407
+ id: (await inspect(holder)).Image,
2408
+ packages: (await output(holder, [
2409
+ "apk",
2410
+ "info",
2411
+ "-v"
2412
+ ])).split("\n").sort()
2413
+ },
2414
+ client: {
2415
+ kind: "ioredis",
2416
+ image: node
2417
+ },
2418
+ netns: {
2419
+ client: clientNetns,
2420
+ target: parts.members[first].netns
2421
+ },
2422
+ sysctls,
2423
+ group: {
2424
+ members,
2425
+ sentinels,
2426
+ observer: { image: node }
2427
+ }
2428
+ };
2429
+ },
2430
+ close
2431
+ };
2432
+ } catch (error) {
2433
+ await close();
2434
+ throw error;
2435
+ }
2436
+ };
2437
+ /** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
2438
+ const STEADY = 20;
2439
+ /** This time is how long a run continues after its outcome, so that the record shows what came next. */
2440
+ const TAIL_MS = 2e3;
2441
+ /** This function gives the address of a member, with the port of the group. */
2442
+ const addressOf = (lab, member) => `${lab.members[member].ip}:${lab.group.port}`;
2443
+ /** This function gives the latest time that a group cell waits for its outcome, in ms from the fault. */
2444
+ const deadlineOf = (cell) => {
2445
+ const { prediction } = cell;
2446
+ 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;
2447
+ };
2448
+ /** This function gives the arguments of the client of a group cell. */
2449
+ const clientArgs = (cell, lab) => {
2450
+ return [
2451
+ "--sentinels",
2452
+ cell.group.sentinels.map((name) => `${lab.sentinels[name].ip}:${cell.group.sentinelPort}`).join(","),
2453
+ "--name",
2454
+ cell.group.name,
2455
+ "--interval-ms",
2456
+ String(cell.probe.interval === void 0 ? 50 : valueIn(cell.probe.interval, "ms")),
2457
+ "--max-s",
2458
+ String(Math.ceil(deadlineOf(cell) / 1e3 + 120)),
2459
+ ...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)]
2460
+ ];
2461
+ };
2462
+ /** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
2463
+ const waitFor = (client, match, within) => new Promise((resolve) => {
2464
+ const started = Date.now();
2465
+ const look = () => {
2466
+ const found = client.events.find(match);
2467
+ if (found !== void 0 || Date.now() - started > within) resolve(found);
2468
+ else setTimeout(look, 20);
2469
+ };
2470
+ look();
2471
+ });
2472
+ /** This function gives the latest subscription of the observer to each Sentinel, in some lines of the observer. */
2473
+ const subscriptionsOf = (timeline) => {
2474
+ const latest = /* @__PURE__ */ new Map();
2475
+ for (const line of timeline) if (line.ev === "subscribed") latest.set(String(line.sentinel), line);
2476
+ return [...latest.values()];
2477
+ };
2478
+ /** This function gives the count of lost records in the latest clock line of the prober. */
2479
+ const droppedSoFar$1 = (records) => {
2480
+ for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
2481
+ return 0;
2482
+ };
2483
+ /**
2484
+ * This function does one run of a group cell:
2485
+ * - The group converges, and its keys go. The client connects through the Sentinels, and its
2486
+ * stream of writes gets steady.
2487
+ * - The fault occurs on its part, as the primary of this run makes the roles. A fault with a
2488
+ * hold lifts at its instant in the VM. A stall lifts at its length after the first write into it.
2489
+ * - The run waits for its outcome: the first write that a new primary accepts, or the first one
2490
+ * after the lift on the same primary. Then it continues for a moment.
2491
+ * - Each fault goes, the client stops, and the group converges again. The keys that it then holds
2492
+ * tell which writes it kept.
2493
+ */
2494
+ const groupOnce = async (lab, cell, environment, index) => {
2495
+ const group = cell.group;
2496
+ const primary = await lab.converge();
2497
+ await lab.flush(primary);
2498
+ await lab.forget();
2499
+ const from = lab.kernel.length;
2500
+ const timelineFrom = lab.timeline.length;
2501
+ const lostBefore = droppedSoFar$1(lab.kernel);
2502
+ const client = await lab.client(clientArgs(cell, lab));
2503
+ const lands = cell.prediction.group?.lands ?? "new";
2504
+ const place = cell.faultAt ?? { role: "primary" };
2505
+ let fault = null;
2506
+ let heal = null;
2507
+ let at = null;
2508
+ const undo = [];
2509
+ try {
2510
+ await client.until(["ready"], 6e4);
2511
+ if (await waitFor(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY, 3e4) === void 0) throw new Error(`the client did not write ${STEADY} times in 30 s`);
2512
+ let healing = null;
2513
+ switch (cell.fault) {
2514
+ case "blackhole":
2515
+ case "partition":
2516
+ case "rst": {
2517
+ const kind = cell.fault === "rst" ? "rst" : "blackhole";
2518
+ const cuts = place.cuts ?? (cell.fault === "partition" ? "group" : "all");
2519
+ const part = lab.partOf(place.role, primary);
2520
+ at = part;
2521
+ fault = await lab.fault(kind, part, cuts);
2522
+ undo.push(() => lab.lift(kind, part, cuts));
2523
+ if (cell.stall !== void 0) {
2524
+ const t1 = fault.t1;
2525
+ const sent = await waitFor(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
2526
+ if (sent !== void 0) healing = lab.lift(kind, part, cuts, sent.t + valueIn(cell.stall, "s") * 1e9);
2527
+ } else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
2528
+ break;
2529
+ }
2530
+ case "freeze-process": {
2531
+ const part = lab.partOf(place.role, primary);
2532
+ at = part;
2533
+ fault = await lab.signal(part, "STOP");
2534
+ undo.push(() => lab.signal(part, "CONT"));
2535
+ if (cell.hold !== void 0) healing = lab.signal(part, "CONT", fault.t0 + valueIn(cell.hold, "s") * 1e9);
2536
+ break;
2537
+ }
2538
+ case "fin": {
2539
+ const part = lab.partOf(place.role, primary);
2540
+ at = part;
2541
+ fault = await lab.signal(part, "KILL");
2542
+ if (group.members.includes(part)) undo.push(() => lab.restart(part));
2543
+ break;
2544
+ }
2545
+ case "planned-failover":
2546
+ fault = await lab.failover(cell.coordinated === true);
2547
+ break;
2548
+ default: throw new Error(`a group cell has no fault ${cell.fault}`);
2549
+ }
2550
+ const healed = healing === null ? Promise.resolve(null) : healing.then((stamp) => {
2551
+ heal = stamp;
2552
+ undo.length = 0;
2553
+ return stamp;
2554
+ });
2555
+ healed.catch(() => void 0);
2556
+ const t0 = fault.t0;
2557
+ const old = addressOf(lab, primary);
2558
+ const deadline = deadlineOf(cell);
2559
+ if ((lands === "new" ? await waitFor(client, (event) => event.ev === "ack" && event.t > t0 && typeof event["at"] === "string" && event["at"] !== old, deadline) : await (async () => {
2560
+ await healed;
2561
+ const after = heal?.t0 ?? t0;
2562
+ return waitFor(client, (event) => event.ev === "ack" && event.t > after, deadline);
2563
+ })()) !== void 0) await sleep$1(TAIL_MS);
2564
+ await healed;
2565
+ } finally {
2566
+ for (const step of undo.reverse()) await step().catch(() => void 0);
2567
+ await client.stop();
2568
+ }
2569
+ const settled = await lab.settle();
2570
+ const lost = settled === null ? null : settled - lostBefore;
2571
+ const after = await lab.converge().catch(() => null);
2572
+ const holds = after === null ? null : await lab.holds(after);
2573
+ if (lab.timeline.slice(timelineFrom).some((line) => line.ev === "sentinel" && String(line.event).startsWith("-failover-abort"))) await lab.reset();
2574
+ return {
2575
+ environment,
2576
+ index,
2577
+ events: [...client.events],
2578
+ fault,
2579
+ ...heal === null ? {} : { heal },
2580
+ kernel: lab.kernel.slice(from),
2581
+ netns: lab.netns.client,
2582
+ targetNetns: lab.members[primary].netns,
2583
+ lost,
2584
+ group: {
2585
+ members: lab.members,
2586
+ sentinels: lab.sentinels,
2587
+ primary,
2588
+ at,
2589
+ timeline: [...subscriptionsOf(lab.timeline.slice(0, timelineFrom)), ...lab.timeline.slice(timelineFrom)],
2590
+ after,
2591
+ holds
2592
+ }
2593
+ };
2594
+ };
2595
+ //#endregion
1550
2596
  //#region src/run.ts
1551
2597
  /**
1552
2598
  * This function measures and judges the raw records of a cell, with the code of the harness at
@@ -1681,7 +2727,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
1681
2727
  await sleep(200);
1682
2728
  } else await sleep(250);
1683
2729
  const faultAt = cell.faultAt ?? (server ? "client" : "target");
1684
- const towardServerComesIn = faultAt === "target" || typeof faultAt === "object" && faultAt.toward === "client";
2730
+ const towardServerComesIn = faultAt === "target" || isHopPlace(faultAt) && faultAt.toward === "client";
1685
2731
  const direction = cell.drops === void 0 ? void 0 : cell.drops === "to-server" === towardServerComesIn ? "in" : "out";
1686
2732
  const options = {
1687
2733
  ...direction === void 0 ? {} : { direction },
@@ -1808,6 +2854,7 @@ const line = (cell, run, read) => `${cell.name}: env ${run.environment} ${run.wa
1808
2854
  * of a cell whose fault ends the environment is a dry run, without the fault.
1809
2855
  */
1810
2856
  const inEnvironment = async (cell, environment, count, say) => {
2857
+ if (cell.group !== void 0) return inGroupEnvironment(cell, environment, count, say);
1811
2858
  const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
1812
2859
  try {
1813
2860
  const settings = await lab.configure(cell.targetSettings ?? {});
@@ -1816,7 +2863,7 @@ const inEnvironment = async (cell, environment, count, say) => {
1816
2863
  if (cell.fault === "mtu-blackhole" || cell.fault === "netem") {
1817
2864
  await lab.offload(false, "target");
1818
2865
  await lab.offload(false, "client");
1819
- if (typeof cell.faultAt === "object") await lab.offload(false, cell.faultAt);
2866
+ if (isHopPlace(cell.faultAt)) await lab.offload(false, cell.faultAt);
1820
2867
  }
1821
2868
  const latency = netemSpec(cell, void 0);
1822
2869
  if (latency !== null) await lab.netem(latency, "target");
@@ -1843,6 +2890,35 @@ const inEnvironment = async (cell, environment, count, say) => {
1843
2890
  }
1844
2891
  };
1845
2892
  /**
2893
+ * This function does the part of the runs of a group cell in one environment. The warm-up is a
2894
+ * whole run with its fault, because no fault of a group ends its environment. A server that a
2895
+ * fin stopped starts again, and the group converges before each run.
2896
+ */
2897
+ const inGroupEnvironment = async (cell, environment, count, say) => {
2898
+ const lab = await openGroupLab(cell.group, { watchSentinels: cell.calibrates === "sentinels" });
2899
+ try {
2900
+ const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
2901
+ const warmup = {
2902
+ ...await groupOnce(lab, cell, environment, -1),
2903
+ warmup: true
2904
+ };
2905
+ say(line(cell, warmup, measure(cell, warmup)));
2906
+ const runs = [];
2907
+ for (let index = 0; index < count; index += 1) {
2908
+ const run = await groupOnce(lab, cell, environment, index);
2909
+ runs.push(run);
2910
+ say(line(cell, run, measure(cell, run)));
2911
+ }
2912
+ return {
2913
+ manifest,
2914
+ runs,
2915
+ warmup
2916
+ };
2917
+ } finally {
2918
+ await lab.close();
2919
+ }
2920
+ };
2921
+ /**
1846
2922
  * This function does the runs of a cell. They go across new environments, and each environment records
1847
2923
  * its manifest. Then the verdict judges the runs. A median across the edge of the band extends
1848
2924
  * the cell from 11 runs to 22, and then to 44, each extension in new environments. With a
@@ -1891,6 +2967,6 @@ const runCell = async (cell, say = () => {}) => {
1891
2967
  }
1892
2968
  };
1893
2969
  //#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 };
2970
+ export { CONVERGE_MS, LISTING, NOISE, PUMP, RELATIVE, STAGES, STEPS, crossingsOf, deadlineOf, deviceOf, endpointAfter, errorsOf, fieldsOf, groupOnce, groupVerdictOf, harnessImage, infoOf, isHopPlace, judge, lagsOf, measure, measureGroup, medianInterval, memberConfig, nodeClientImage, openGroupLab, openLab, peersOf, quantileOf, reorderingOf, routerImage, routesOf, runCell, sentinelConfig, socketOf, spread, spreadOf, stagesOf, subscriptionsOf, templated, toxicBody, verdictOf, viewOf, writesOf };
1895
2971
 
1896
2972
  //# sourceMappingURL=index.js.map