@mark1russell7/harness 0.14.3 → 0.14.4

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
@@ -1,4 +1,4 @@
1
- import { A as unpinnedBases, C as routerImage, D as targetImageOf, E as subjectProblems, F as locksOf, I as npmVersions, L as versionsOf, M as cargoVersions, N as goVersions, O as targetSourceOf, P as jarVersions, S as programOf, T as subjectClient, _ as mongoClientImage, a as SHIPPED_NAMES, b as pgClientImage, c as buildRecordOf, d as harnessImage, f as harnessVersion, g as inContext, h as imageProblems, i as SHIPPED_CLIENTS, j as LOCK_NAMES, k as unlockedManifests, l as goHttpImage, m as imageOf, n as PINNED, o as amqpClientImage, p as hikariClientImage, r as REPOSITORY, s as basesOf, t as CLIENT_NAMES, u as grpcClientImage, v as nodeClientImage, w as shippedSource, x as planOf, y as nodeHttpImage } from "./image-BMdV7yWD.js";
1
+ import { A as unlockedManifests, C as programOf, D as subjectProblems, E as subjectClient, F as jarVersions, I as locksOf, L as npmVersions, M as LOCK_NAMES, N as cargoVersions, O as targetImageOf, P as goVersions, R as versionsOf, S as planOf, T as shippedSource, _ as kafkaClientImage, a as SHIPPED_NAMES, b as nodeHttpImage, c as buildRecordOf, d as harnessImage, f as harnessVersion, g as inContext, h as imageProblems, i as SHIPPED_CLIENTS, j as unpinnedBases, k as targetSourceOf, l as goHttpImage, m as imageOf, n as PINNED, o as amqpClientImage, p as hikariClientImage, r as REPOSITORY, s as basesOf, t as CLIENT_NAMES, u as grpcClientImage, v as mongoClientImage, w as routerImage, x as pgClientImage, y as nodeClientImage } from "./image-EZ8ioZ3L.js";
2
2
  import { quantity, valueIn } from "@mark1russell7/quantity";
3
3
  import { median, medianInterval as medianInterval$1, quantileSorted } from "@mark1russell7/rigor-statistics";
4
4
  import { createHash, randomBytes } from "node:crypto";
@@ -288,6 +288,10 @@ const groupVerdictOf = (prediction, counted, delta = NOISE) => {
288
288
  const errors = /* @__PURE__ */ new Map();
289
289
  for (const run of counted) for (const error of run.group?.errors ?? []) errors.set(error.name, [...errors.get(error.name) ?? [], valueIn(error.first, "s")]);
290
290
  const lags = counted.flatMap((run) => run.group?.lags ?? []);
291
+ const copyCounts = counted.flatMap((run) => {
292
+ const copies = run.group?.writes.duplicates;
293
+ return copies === null || copies === void 0 ? [] : [copies.length];
294
+ });
291
295
  return {
292
296
  stages,
293
297
  medians,
@@ -310,7 +314,13 @@ const groupVerdictOf = (prediction, counted, delta = NOISE) => {
310
314
  runs: firsts.length,
311
315
  first: s(median(firsts.sort((a, b) => a - b)))
312
316
  })),
313
- ...lags.length === 0 ? {} : { lags: spreadOf(lags) }
317
+ ...lags.length === 0 ? {} : { lags: spreadOf(lags) },
318
+ ...prediction.group?.duplicates === void 0 ? {} : { duplicates: {
319
+ runs: copyCounts.length,
320
+ total: copyCounts.reduce((sum, n) => sum + n, 0),
321
+ median: medianCount(copyCounts),
322
+ max: copyCounts.length === 0 ? null : Math.max(...copyCounts)
323
+ } }
314
324
  };
315
325
  };
316
326
  /** This function gives the verdict of the band of a cell: its containment and the equivalence of its median. */
@@ -335,6 +345,16 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
335
345
  why: "no run counted"
336
346
  };
337
347
  if (prediction.detects === null) {
348
+ if (prediction.queue !== void 0 || prediction.ports !== void 0) {
349
+ const off = new Set(outside.map((each) => each.run)).size;
350
+ return {
351
+ ...empty,
352
+ decision: off === 0 ? "holds" : "fails",
353
+ band: null,
354
+ outside,
355
+ why: `${counted.length - off} of ${counted.length} runs hold the law of the ${prediction.queue === void 0 ? "port range" : "queue"}; ${prediction.why}`
356
+ };
357
+ }
338
358
  const crossed = counted.flatMap((run) => run.crossings === void 0 ? [] : [run.crossings]);
339
359
  if (crossed.length > 0) {
340
360
  const hop = crossed[0].hop;
@@ -363,16 +383,30 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
363
383
  }
364
384
  const outcomes = [...kinds].map(([kind, n]) => `${n} ${kind}`).join(", ");
365
385
  const timed = times.length === 0 ? "no time" : `median ${s(median(times)).value} s from ${prediction.from}, range [${s(times[0]).value}, ${s(times.at(-1)).value}] s`;
366
- const counts = prediction.group?.lost;
386
+ const { counts } = prediction;
367
387
  if (counts !== void 0) {
388
+ const exact = counts.map((each) => `${each.what} ${each.count}`).join(", ");
389
+ return {
390
+ ...empty,
391
+ decision: outside.length === 0 ? "holds" : "fails",
392
+ band: null,
393
+ outside,
394
+ median: times.length === 0 ? null : s(median(times)),
395
+ why: `${counted.length - outside.length} of ${counted.length} runs gave the exact counts (${exact}); ${outcomes}; ${timed}. ${prediction.why}`
396
+ };
397
+ }
398
+ const lost = prediction.group?.lost;
399
+ const copies = prediction.group?.duplicates;
400
+ if (lost !== void 0 || copies !== void 0) {
368
401
  const off = new Set(outside.map((each) => each.run)).size;
402
+ const bands = [...lost === void 0 ? [] : [`lost writes [${lost.low}, ${lost.high}]`], ...copies === void 0 ? [] : [`extra copies [${copies.low}, ${copies.high}]`]];
369
403
  return {
370
404
  ...empty,
371
405
  decision: off === 0 ? "holds" : "fails",
372
406
  band: null,
373
407
  outside,
374
408
  median: times.length === 0 ? null : s(median(times)),
375
- why: `${counted.length - off} of ${counted.length} runs inside the band of lost writes [${counts.low}, ${counts.high}]; ${outcomes}; ${timed}. ${prediction.why}`
409
+ why: `${counted.length - off} of ${counted.length} runs inside the band of ${bands.join(" and the band of ")}; ${outcomes}; ${timed}. ${prediction.why}`
376
410
  };
377
411
  }
378
412
  const missed = outside.filter((each) => counted.some((run) => name(run) === each.run && run.reconnect !== void 0));
@@ -529,7 +563,7 @@ const DROP_HANDLING = quantity(1.5, "ms");
529
563
  /** This function gives the window of the act of a drop, in seconds: `DROP_HANDLING`, or the pairing window (`delta`) when it is longer. */
530
564
  const dropWindow = (delta) => Math.max(valueIn(DROP_HANDLING, "s"), valueIn(delta, "s"));
531
565
  /** This function gives the interval between two instants on the one clock, in seconds to the microsecond. */
532
- const between$3 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
566
+ const between$4 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
533
567
  /** This function gives the state of a state record after the change, and before it. TCP_ESTABLISHED is 1, TCP_SYN_SENT 2. */
534
568
  const newState$2 = (record) => record.new;
535
569
  const oldState$2 = (record) => record.old;
@@ -571,7 +605,7 @@ const eventOf = (socket, line, matches, delta = NOISE) => {
571
605
  */
572
606
  const leftAfter = (socket, line) => {
573
607
  const left = socket.find((record) => record.ev === "state" && oldState$2(record) === 1 && (newState$2(record) === 4 || newState$2(record) === 7) && record.mono >= line.t);
574
- return left === void 0 ? null : between$3(line.t, left.mono);
608
+ return left === void 0 ? null : between$4(line.t, left.mono);
575
609
  };
576
610
  /**
577
611
  * This function gives the end of the client process in a run, as the kernel stamps it. The process
@@ -663,19 +697,19 @@ const reconnectOf = (run, detected, cookie, from = detected) => {
663
697
  const waits = starts.flatMap((start, at) => {
664
698
  const next = starts[at + 1]?.t ?? Number.POSITIVE_INFINITY;
665
699
  const dial = dials.find((each) => each.t >= start.t && each.t < next);
666
- return dial === void 0 ? [] : [between$3(start.t, dial.t)];
700
+ return dial === void 0 ? [] : [between$4(start.t, dial.t)];
667
701
  });
668
702
  const fresh = run.kernel.filter((record) => record.netns === run.netns && record.cookie !== cookie && record.ev === "state" && record.mono > detected);
669
703
  const syns = fresh.filter((record) => newState$2(record) === 2);
670
704
  const established = fresh.find((record) => newState$2(record) === 1);
671
705
  const connects = dials.flatMap((dial) => {
672
706
  const syn = syns.find((record) => record.mono >= dial.t);
673
- return syn === void 0 ? [] : [between$3(dial.t, syn.mono)];
707
+ return syn === void 0 ? [] : [between$4(dial.t, syn.mono)];
674
708
  });
675
709
  return {
676
710
  waits,
677
- syn: syns[0] === void 0 ? null : between$3(detected, syns[0].mono),
678
- established: established === void 0 ? null : between$3(detected, established.mono),
711
+ syn: syns[0] === void 0 ? null : between$4(detected, syns[0].mono),
712
+ established: established === void 0 ? null : between$4(detected, established.mono),
679
713
  connects
680
714
  };
681
715
  };
@@ -702,7 +736,7 @@ const callOf = (events, request, detected) => {
702
736
  };
703
737
  return {
704
738
  outcome: end.ev === "data" ? "a reply" : String(end["message"] ?? end["code"] ?? "an error"),
705
- after: detected === void 0 ? null : between$3(detected, end.t)
739
+ after: detected === void 0 ? null : between$4(detected, end.t)
706
740
  };
707
741
  };
708
742
  //#endregion
@@ -717,12 +751,22 @@ const ERRNO$1 = {
717
751
  111: "ECONNREFUSED",
718
752
  113: "EHOSTUNREACH"
719
753
  };
720
- const between$2 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
754
+ const between$3 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
721
755
  const round = (seconds) => Math.round(seconds * 1e6) / 1e6;
722
756
  /** This function gives the value of a field of a line by its name. A dotted name is a field in a field. */
723
757
  const fieldOf = (line, path) => path.split(".").reduce((value, key) => value !== null && typeof value === "object" ? value[key] : void 0, line);
724
758
  /** This function tells if a line of the client matches a line match: its `ev`, and each field that the match names. */
725
759
  const matches = (line, match) => line.ev === match.ev && Object.entries(match.where ?? {}).every(([path, value]) => fieldOf(line, path) === value);
760
+ /** This function gives the count of the lines that match. */
761
+ const countOf = (lines, match) => lines.filter((line) => matches(line, match)).length;
762
+ /** This function gives the finding of the line counts of a run, or null when each count is the exact one. */
763
+ const countFinding = (counts, lines) => {
764
+ const off = counts.flatMap((each) => {
765
+ const seen = countOf(lines, each.match);
766
+ return seen === each.count ? [] : [`${each.what}: ${seen} lines, and the law gives ${each.count}`];
767
+ });
768
+ return off.length === 0 ? null : off.join("; ");
769
+ };
726
770
  /** This function says a line match in words, for a finding. */
727
771
  const wordsOf = (match) => {
728
772
  const where = Object.entries(match.where ?? {}).map(([path, value]) => `${path} ${String(value)}`);
@@ -1025,7 +1069,7 @@ const measureDriver = (cell, run) => {
1025
1069
  const until = prediction.detects ? Infinity : after + valueIn(prediction.window, "s") * 1e9;
1026
1070
  for (const match of driver.quiet ?? []) {
1027
1071
  const line = events.find((event) => event.t > after && event.t <= until && matches(event, match));
1028
- if (line !== void 0) findings.push(`${wordsOf(match)} came ${between$2(after, line.t).value} s after the fault, where none should`);
1072
+ if (line !== void 0) findings.push(`${wordsOf(match)} came ${between$3(after, line.t).value} s after the fault, where none should`);
1029
1073
  }
1030
1074
  const expected = prediction.detects ? void 0 : prediction.middlebox?.forgets;
1031
1075
  if (!prediction.detects && (run.middlebox !== void 0 || expected !== void 0)) {
@@ -1035,14 +1079,14 @@ const measureDriver = (cell, run) => {
1035
1079
  for (const socket of socketsOf(events)) {
1036
1080
  const forgot = middlebox.find((line) => line.ev === "ct" && line["type"] === "DESTROY" && String(line["orig"] ?? "").startsWith(`${socket.local}>`) && line.t > socket.opened && (socket.ended === null || line.t < socket.ended) && line.t <= until);
1037
1081
  if (expected === void 0 || socket.local !== own) {
1038
- if (forgot !== void 0) findings.push(`a router forgot the flow of the ${socket.role} socket ${between$2(after, forgot.t).value} s after the fault, where it should keep it`);
1082
+ if (forgot !== void 0) findings.push(`a router forgot the flow of the ${socket.role} socket ${between$3(after, forgot.t).value} s after the fault, where it should keep it`);
1039
1083
  continue;
1040
1084
  }
1041
1085
  const last = referenceOf(run, "last-received", driver.socket, false);
1042
1086
  if (forgot === void 0) findings.push(`no router forgot the flow of the ${socket.role} socket in the window, where it should forget it`);
1043
1087
  else if (last === void 0) findings.push(`the ${socket.role} socket got no segment before the fault`);
1044
1088
  else {
1045
- const at = between$2(last, forgot.t).value;
1089
+ const at = between$3(last, forgot.t).value;
1046
1090
  const [low, high] = [round(valueIn(expected.low, "s") - noise), round(valueIn(expected.high, "s") + noise)];
1047
1091
  if (at < low || at > high) findings.push(`a router forgot the flow of the ${socket.role} socket ${at} s after its last segment, outside [${low}, ${high}] s`);
1048
1092
  }
@@ -1059,7 +1103,7 @@ const measureDriver = (cell, run) => {
1059
1103
  elapsed: null
1060
1104
  };
1061
1105
  }
1062
- const elapsed = between$2(from, line.t);
1106
+ const elapsed = between$3(from, line.t);
1063
1107
  const name = nameOf$1(line, step.ends);
1064
1108
  if (step.name !== "OK" && name !== step.name) findings.push(`${step.what}: it ended with ${name}, not ${step.name}`);
1065
1109
  const [low, high] = [round(valueIn(step.low, "s") - noise), round(valueIn(step.high, "s") + noise)];
@@ -1093,7 +1137,7 @@ const measureDriver = (cell, run) => {
1093
1137
  ...nothing,
1094
1138
  ...stepped,
1095
1139
  outcome: { kind: "answered" },
1096
- elapsed: between$2(from, line.t),
1140
+ elapsed: between$3(from, line.t),
1097
1141
  void: voided,
1098
1142
  finding: findings[0] ?? null
1099
1143
  };
@@ -1136,15 +1180,266 @@ const measureDriver = (cell, run) => {
1136
1180
  ...base,
1137
1181
  ...stepped,
1138
1182
  outcome,
1139
- elapsed: between$2(reference, ended),
1140
- socket: seen === void 0 ? null : between$2(reference, seen.t),
1141
- noticed: report === void 0 || seen === void 0 ? null : between$2(report.at, seen.t),
1142
- steps: (prediction.from === "last-received" ? records.filter((record) => record.ev === "probe" && record.what === "keepalive" && record.mono > reference) : []).map((record) => between$2(reference, record.mono)),
1183
+ elapsed: between$3(reference, ended),
1184
+ socket: seen === void 0 ? null : between$3(reference, seen.t),
1185
+ noticed: report === void 0 || seen === void 0 ? null : between$3(report.at, seen.t),
1186
+ steps: (prediction.from === "last-received" ? records.filter((record) => record.ev === "probe" && record.what === "keepalive" && record.mono > reference) : []).map((record) => between$3(reference, record.mono)),
1143
1187
  void: voided,
1144
1188
  finding: findings[0] ?? null
1145
1189
  };
1146
1190
  };
1147
1191
  //#endregion
1192
+ //#region src/queue.ts
1193
+ /** This bit is the SYN of the flags of an `rx` record of the prober. */
1194
+ const SYN = 2;
1195
+ /** This function gives the time between two stamps of CLOCK_MONOTONIC. */
1196
+ const between$2 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
1197
+ const numberOf$1 = (line, field) => {
1198
+ const value = line[field];
1199
+ return typeof value === "number" ? value : 0;
1200
+ };
1201
+ /** This function says if a time is inside a band. */
1202
+ const inside = (time, band) => {
1203
+ const seconds = valueIn(time, "s");
1204
+ return seconds >= valueIn(band.low, "s") && seconds <= valueIn(band.high, "s");
1205
+ };
1206
+ /** This function gives the words of a band. */
1207
+ const bandWords = (band) => `[${valueIn(band.low, "s")}, ${valueIn(band.high, "s")}] s`;
1208
+ const waitsOf = (from, stamps) => stamps.map((stamp, at) => between$2(at === 0 ? from : stamps[at - 1], stamp));
1209
+ /** This function finds the records of the sockets of the client that had this local address. */
1210
+ const socketsAt = (run, local) => {
1211
+ const cookies = new Set(run.kernel.filter((record) => record.netns === run.netns && record.src === local).map((record) => record.cookie));
1212
+ return run.kernel.filter((record) => record.netns === run.netns && cookies.has(record.cookie));
1213
+ };
1214
+ const readConnection = (name, run, expected) => {
1215
+ const lineOf = (ev) => run.events.find((event) => event.ev === ev && event["name"] === name);
1216
+ const dial = lineOf("dial");
1217
+ const connected = lineOf("connected");
1218
+ const failed = lineOf("failed");
1219
+ const state = lineOf("state");
1220
+ const socket = dial === void 0 ? [] : socketsAt(run, dial["local"]);
1221
+ const sent = socket.filter((record) => record.ev === "retrans").map((record) => record.mono);
1222
+ const rx = socket.filter((record) => record.ev === "rx" && ((record.seg?.flags ?? 0) & SYN) !== 0);
1223
+ const from = dial?.t ?? 0;
1224
+ const start = expected?.ends === null || expected?.ends === void 0 ? void 0 : lineOf(expected.ends.from);
1225
+ const last = sent.at(-1);
1226
+ return {
1227
+ name,
1228
+ connected: connected !== void 0,
1229
+ syns: waitsOf(from, connected === void 0 ? sent : sent.filter((stamp) => stamp < connected.t)),
1230
+ synAcks: waitsOf(from, rx.map((record) => record.mono)),
1231
+ retransmissions: sent.length,
1232
+ errno: failed === void 0 ? null : String(failed["errno"]),
1233
+ ended: failed === void 0 || start === void 0 ? null : between$2(start.t, failed.t),
1234
+ afterLast: failed === void 0 || last === void 0 ? null : between$2(last, failed.t),
1235
+ unacked: state === void 0 ? null : numberOf$1(state, "unacked")
1236
+ };
1237
+ };
1238
+ /** This function reads one run against the band of each wait in a list, and gives the words of each miss. */
1239
+ const waitFindings = (what, seen, bands) => {
1240
+ if (seen.length !== bands.length) return [`${what}: ${seen.length} waits, not ${bands.length}`];
1241
+ return seen.flatMap((wait, at) => inside(wait, bands[at]) ? [] : [`${what}: wait ${at} of ${valueIn(wait, "s")} s, outside ${bandWords(bands[at])}`]);
1242
+ };
1243
+ const connectionFindings = (expected, read, expiredAt, stamps) => {
1244
+ const { name, ends } = expected;
1245
+ const found = [];
1246
+ if (read.connected !== expected.connects) found.push(`${name}: ${read.connected ? "connected" : "did not connect"}, against the prediction`);
1247
+ if (expected.syns !== void 0) found.push(...waitFindings(`${name} SYN`, read.syns, expected.syns));
1248
+ if (expected.synAcks !== void 0) found.push(...waitFindings(`${name} SYN-ACK`, read.synAcks, expected.synAcks));
1249
+ if (expected.unacked !== void 0 && read.unacked !== expected.unacked) found.push(`${name}: ${read.unacked ?? "no"} segments without an ACK, not ${expected.unacked}`);
1250
+ if (ends === null) {
1251
+ if (read.errno !== null) found.push(`${name}: ended with ${read.errno}, and the prediction has no end`);
1252
+ return found;
1253
+ }
1254
+ if (read.errno !== ends.errno) found.push(`${name}: ended with ${read.errno ?? "nothing"}, not ${ends.errno}`);
1255
+ if (read.ended === null) return [...found, `${name}: no time for its end`];
1256
+ if (!inside(read.ended, ends.within)) found.push(`${name}: its end came at ${valueIn(read.ended, "s")} s, outside ${bandWords(ends.within)}`);
1257
+ if (ends.retransmitted === void 0) return found;
1258
+ if (read.afterLast === null || !inside(read.afterLast, ends.retransmitted)) found.push(`${name}: its end came ${read.afterLast === null ? "with no retransmission" : `${valueIn(read.afterLast, "s")} s after its last retransmission`}, outside ${bandWords(ends.retransmitted)}`);
1259
+ if (expiredAt === null) return found;
1260
+ const before = stamps.at(-2);
1261
+ const last = stamps.at(-1);
1262
+ if (last === void 0 || last < expiredAt || before !== void 0 && before > expiredAt) found.push(`${name}: its last two retransmissions do not span the expiry`);
1263
+ return found;
1264
+ };
1265
+ /**
1266
+ * This function reads one run of a queue cell. The client lines give the connections and the
1267
+ * counters at the two ends of the run. The target lines give the count of the queue. The records of
1268
+ * the prober give the waits of each socket.
1269
+ */
1270
+ const measureQueue = (run, expected) => {
1271
+ const { events, kernel } = run;
1272
+ const lineOf = (ev) => events.find((event) => event.ev === ev);
1273
+ const base = {
1274
+ environment: run.environment,
1275
+ index: run.index,
1276
+ outcome: { kind: "answered" },
1277
+ elapsed: null,
1278
+ socket: null,
1279
+ noticed: null,
1280
+ steps: [],
1281
+ probes: 0,
1282
+ retransmissions: kernel.filter((record) => record.netns === run.netns && record.ev === "retrans").length,
1283
+ acks: 0
1284
+ };
1285
+ const voided = (why) => ({
1286
+ ...base,
1287
+ void: why,
1288
+ finding: null
1289
+ });
1290
+ const opened = lineOf("opened");
1291
+ const stats = lineOf("stats");
1292
+ if (opened === void 0) return voided("the target did not open its listener");
1293
+ if (stats === void 0 || lineOf("done") === void 0) return voided("the client did not reach its end");
1294
+ if (run.lost === null) return voided("the prober wrote no clock line after the run, so nothing says what it dropped");
1295
+ if (run.lost > 0) return voided(`the prober dropped ${run.lost} records`);
1296
+ const lines = (run.server ?? []).filter((line) => line.ev === "queue" && line.t >= opened.t);
1297
+ if (lines.length === 0) return voided("the target wrote no queue line in the run");
1298
+ const named = [...new Set(events.filter((event) => event.ev === "dial").map((event) => String(event["name"])))];
1299
+ const predicted = new Map(expected.connections.map((each) => [each.name, each]));
1300
+ const connections = named.map((name) => readConnection(name, run, predicted.get(name)));
1301
+ const admitted = expected.admitted === void 0 ? void 0 : connections.find((each) => each.name === expected.admitted);
1302
+ if (expected.admitted !== void 0 && (admitted === void 0 || !admitted.connected || admitted.unacked !== 0)) return voided(`${expected.admitted} did not take the last place in the queue: the burst did not land in its sequence`);
1303
+ const change = (field) => numberOf$1(stats, field) - numberOf$1(opened, field);
1304
+ const burst = events.find((event) => event.ev === "dial" && event["name"] === "in")?.t ?? opened.t;
1305
+ const counted = expected.expiry?.timeouts;
1306
+ const expired = counted === void 0 ? void 0 : lines.find((line) => numberOf$1(line, "timeouts") - numberOf$1(opened, "timeouts") >= counted);
1307
+ const accepted = events.find((event) => event.ev === "accepted");
1308
+ const rise = expected.rise?.count ?? 0;
1309
+ const low = accepted === void 0 ? -1 : lines.findIndex((line) => line.t >= accepted.t && numberOf$1(line, "count") < rise);
1310
+ const risen = low < 0 ? void 0 : lines.slice(low).find((line) => numberOf$1(line, "count") >= rise);
1311
+ const drained = events.filter((event) => event.ev === "accepted").reduce((sum, event) => sum + (Array.isArray(event["peers"]) ? event["peers"].length : 0), 0);
1312
+ const queue = {
1313
+ max: numberOf$1(opened, "max"),
1314
+ held: Math.max(...lines.map((line) => numberOf$1(line, "count"))),
1315
+ connected: connections.filter((each) => each.connected).length,
1316
+ overflows: change("overflows"),
1317
+ drops: change("drops"),
1318
+ cookies: change("cookies"),
1319
+ timeouts: change("timeouts"),
1320
+ resets: change("resets"),
1321
+ drained,
1322
+ expiry: expired === void 0 ? null : between$2(burst, expired.t),
1323
+ rise: risen === void 0 ? null : between$2(burst, risen.t),
1324
+ connections
1325
+ };
1326
+ const found = [];
1327
+ if (queue.max !== expected.max) found.push(`the limit is ${queue.max}, not ${expected.max}`);
1328
+ if (queue.held !== expected.held) found.push(`the queue held ${queue.held}, not ${expected.held}`);
1329
+ if (queue.connected !== expected.connected) found.push(`${queue.connected} connected, not ${expected.connected}`);
1330
+ if (queue.overflows < expected.overflows.low || queue.overflows > expected.overflows.high) found.push(`${queue.overflows} overflows, outside [${expected.overflows.low}, ${expected.overflows.high}]`);
1331
+ if (queue.resets !== expected.resets) found.push(`${queue.resets} resets, not ${expected.resets}`);
1332
+ if (queue.cookies !== 0) found.push(`${queue.cookies} SYN cookies, and the law has none`);
1333
+ if (expected.drained !== void 0 && queue.drained !== expected.drained) found.push(`${queue.drained} accepted, not ${expected.drained}`);
1334
+ if (expected.expiry !== void 0 && (queue.expiry === null || !inside(queue.expiry, expected.expiry.within))) found.push(`the expiry came at ${queue.expiry === null ? "no time" : `${valueIn(queue.expiry, "s")} s`}, outside ${bandWords(expected.expiry.within)}`);
1335
+ if (expected.rise !== void 0 && (queue.rise === null || !inside(queue.rise, expected.rise.within))) found.push(`the count got back to ${expected.rise.count} at ${queue.rise === null ? "no time" : `${valueIn(queue.rise, "s")} s`}, outside ${bandWords(expected.rise.within)}`);
1336
+ for (const each of expected.connections) {
1337
+ const read = connections.find((connection) => connection.name === each.name);
1338
+ if (read === void 0) {
1339
+ found.push(`${each.name}: the client did not dial it`);
1340
+ continue;
1341
+ }
1342
+ const dial = events.find((event) => event.ev === "dial" && event["name"] === each.name);
1343
+ const stamps = socketsAt(run, dial?.["local"]).filter((record) => record.ev === "retrans").map((record) => record.mono);
1344
+ found.push(...connectionFindings(each, read, expired?.t ?? null, stamps));
1345
+ }
1346
+ return {
1347
+ ...base,
1348
+ void: null,
1349
+ finding: found.length === 0 ? null : found.join(". "),
1350
+ queue
1351
+ };
1352
+ };
1353
+ //#endregion
1354
+ //#region src/ports.ts
1355
+ /** This errno ends a search that found no port. */
1356
+ const NO_PORT = "EADDRNOTAVAIL";
1357
+ const portsOf = (line) => Array.isArray(line["ports"]) ? line["ports"].filter((port) => typeof port === "number") : [];
1358
+ /** This function says if the ports of the parity of the low port come first in one search. */
1359
+ const lowFirst = (ports, low) => {
1360
+ const odd = ports.findIndex((port) => (port - low) % 2 !== 0);
1361
+ return odd < 0 || ports.slice(odd).every((port) => (port - low) % 2 !== 0);
1362
+ };
1363
+ /**
1364
+ * This function reads one run of a port cell. The `round` lines give each search. The `closing` and
1365
+ * `freed` lines give when each held port comes back. The records of the prober give the
1366
+ * retransmissions.
1367
+ */
1368
+ const measurePorts = (run, expected) => {
1369
+ const { events, kernel } = run;
1370
+ const retransmissions = kernel.filter((record) => record.netns === run.netns && record.ev === "retrans").length;
1371
+ const base = {
1372
+ environment: run.environment,
1373
+ index: run.index,
1374
+ outcome: { kind: "answered" },
1375
+ elapsed: null,
1376
+ socket: null,
1377
+ noticed: null,
1378
+ steps: [],
1379
+ probes: 0,
1380
+ retransmissions,
1381
+ acks: 0
1382
+ };
1383
+ const voided = (why) => ({
1384
+ ...base,
1385
+ void: why,
1386
+ finding: null
1387
+ });
1388
+ if (events.find((event) => event.ev === "done") === void 0) return voided("the client did not reach its end");
1389
+ if (run.lost === null) return voided("the prober wrote no clock line after the run, so nothing says what it dropped");
1390
+ if (run.lost > 0) return voided(`the prober dropped ${run.lost} records`);
1391
+ const rounds = events.filter((event) => event.ev === "round");
1392
+ const closing = events.find((event) => event.ev === "closing");
1393
+ const freedLines = events.filter((event) => event.ev === "freed");
1394
+ const counts = {};
1395
+ for (const round of rounds) {
1396
+ const count = String(portsOf(round).length);
1397
+ counts[count] = (counts[count] ?? 0) + 1;
1398
+ }
1399
+ const seen = [...new Set(rounds.flatMap((round) => portsOf(round).map((port) => port - expected.low)))].sort((a, b) => a - b);
1400
+ const refusal = Math.max(0, ...rounds.map((round) => typeof round["refusal"] === "number" ? round["refusal"] : 0));
1401
+ const first = freedLines[0];
1402
+ const last = freedLines.at(-1);
1403
+ const ports = {
1404
+ rounds: rounds.length,
1405
+ counts,
1406
+ seen,
1407
+ errnos: [...new Set(rounds.map((round) => String(round["errno"])))],
1408
+ refusal: quantity(Math.round(refusal / 1e3) / 1e6, "s"),
1409
+ firstFreed: closing === void 0 || first === void 0 ? null : between$2(closing.t, first.t),
1410
+ lastFreed: closing === void 0 || last === void 0 ? null : between$2(closing.t, last.t),
1411
+ freed: freedLines.length,
1412
+ tries: last === void 0 || typeof last["tries"] !== "number" ? 0 : last["tries"]
1413
+ };
1414
+ const found = [];
1415
+ const searches = expected.rounds * expected.destinations;
1416
+ if (ports.rounds !== searches) found.push(`${ports.rounds} searches, not ${searches}`);
1417
+ const stray = Object.keys(counts).filter((count) => !expected.counts.includes(Number(count)));
1418
+ if (stray.length > 0) found.push(`a search gave ${stray.join(" or ")} connects, outside ${expected.counts.join(" or ")}`);
1419
+ if (seen.join() !== expected.seen.join()) found.push(`the ports at offsets ${seen.join(" ")} were used, not ${expected.seen.join(" ")}`);
1420
+ if (ports.errnos.join() !== NO_PORT) found.push(`the searches ended with ${ports.errnos.join(" and ")}, not ${NO_PORT}`);
1421
+ if (valueIn(ports.refusal, "s") > valueIn(expected.refusedWithin, "s")) found.push(`a refused connect took ${valueIn(ports.refusal, "s")} s, above ${valueIn(expected.refusedWithin, "s")} s`);
1422
+ if (expected.share !== void 0) {
1423
+ const { count, low, high } = expected.share;
1424
+ const share = (counts[String(count)] ?? 0) / Math.max(1, ports.rounds);
1425
+ if (share < low || share > high) found.push(`the share of ${count} connects is ${share}, outside [${low}, ${high}]`);
1426
+ }
1427
+ if (expected.lowFirst === true && !rounds.every((round) => lowFirst(portsOf(round), expected.low))) found.push("a search got a port of the other parity before each port of the parity of the low port");
1428
+ if (expected.retransmissions !== void 0 && retransmissions !== expected.retransmissions) found.push(`${retransmissions} retransmissions, not ${expected.retransmissions}`);
1429
+ if (expected.freed !== void 0) {
1430
+ const held = Math.max(...expected.counts);
1431
+ if (ports.freed !== held) found.push(`${ports.freed} ports came back, not ${held}`);
1432
+ for (const [name, time] of [["first", ports.firstFreed], ["last", ports.lastFreed]]) if (time === null || !inside(time, expected.freed)) found.push(`the ${name} port came back at ${time === null ? "no time" : `${valueIn(time, "s")} s`}, outside ${bandWords(expected.freed)}`);
1433
+ if (ports.tries === 0) found.push("no connect was refused before the ports came back");
1434
+ }
1435
+ return {
1436
+ ...base,
1437
+ void: null,
1438
+ finding: found.length === 0 ? null : found.join(". "),
1439
+ ports
1440
+ };
1441
+ };
1442
+ //#endregion
1148
1443
  //#region src/recovery.ts
1149
1444
  /**
1150
1445
  * This module holds the pure parts of a recovery drill. These are the scripts of the shell, the reader
@@ -1516,12 +1811,32 @@ const writesOf = (run, after, primary) => {
1516
1811
  held: kept === null ? null : kept.has(n)
1517
1812
  };
1518
1813
  });
1814
+ const duplicates = holds === null ? null : [...holds].sort((a, b) => a - b).filter((n, at, all) => all[at - 1] === n);
1815
+ const oldHolds = run.group?.oldHolds;
1519
1816
  return {
1520
1817
  sent: sent.length,
1521
1818
  acked: acks.size,
1522
1819
  old,
1523
1820
  lost,
1524
- unknown
1821
+ unknown,
1822
+ duplicates,
1823
+ ...oldHolds === void 0 ? {} : { sets: setsOf(run, acks.keys(), unknown, new Set(oldHolds)) }
1824
+ };
1825
+ };
1826
+ /**
1827
+ * This function splits the writes of a pair by its two final reads. A write with no reply is `kept`
1828
+ * on the new primary. It is `stranded` on the old primary alone, or `absent` from each server.
1829
+ */
1830
+ const setsOf = (run, acked, unknown, old) => {
1831
+ const kept = unknown.filter((each) => each.held === true).length;
1832
+ const stranded = unknown.filter((each) => each.held !== true && old.has(each.n)).length;
1833
+ return {
1834
+ unknown: unknown.length,
1835
+ kept,
1836
+ stranded,
1837
+ absent: unknown.length - kept - stranded,
1838
+ missing: [...acked].filter((n) => !old.has(n)).length,
1839
+ warnings: run.events.filter((event) => event.ev === "notice" && event["severity"] === "WARNING").length
1525
1840
  };
1526
1841
  };
1527
1842
  /** This function gives the errors that the client met after an instant: each kind, its first time, and its count. */
@@ -1632,9 +1947,10 @@ const measureGroup = (cell, run) => {
1632
1947
  const heal = run.heal ?? null;
1633
1948
  const lands = expected?.lands ?? "new";
1634
1949
  const cluster = cell.cluster !== void 0;
1635
- const replica = cell.replicaSet !== void 0 || cluster;
1950
+ const kafka = cell.kafka !== void 0;
1951
+ const replica = cell.replicaSet !== void 0 || cluster || kafka;
1636
1952
  const stall = prediction.detects === false && prediction.recovers !== void 0;
1637
- const stages = cluster ? clusterStagesOf(run, t0, run.fault.t1) : replica ? replicaSetStagesOf(run, t0, run.fault.t1, lands) : stagesOf(timeline, t0);
1953
+ const stages = kafka ? {} : cluster ? clusterStagesOf(run, t0, run.fault.t1) : replica ? replicaSetStagesOf(run, t0, run.fault.t1, lands) : stagesOf(timeline, t0);
1638
1954
  const after = lands === "same" && heal !== null && (!replica || stall) ? heal.t0 : t0;
1639
1955
  const sentAt = /* @__PURE__ */ new Map();
1640
1956
  for (const event of events) if (event.ev === "sent") sentAt.set(Number(event["n"]), event.t);
@@ -1666,14 +1982,14 @@ const measureGroup = (cell, run) => {
1666
1982
  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";
1667
1983
  const odown = stages["+odown"];
1668
1984
  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";
1669
- if (voided === null && cell.hold !== void 0 && prediction.detects !== null) {
1985
+ if (voided === null && cell.hold !== void 0 && (prediction.detects !== null || kafka)) {
1670
1986
  if (heal === null) voided = "the fault never lifted";
1671
1987
  else {
1672
1988
  const late = (heal.t0 - t0) / 1e9 - valueIn(cell.hold, "s");
1673
1989
  if (late > valueIn(INJECT, "s")) voided = `the lift came ${Math.round(late * 1e4) / 10} ms after its hold: the injector was late, not the group`;
1674
1990
  }
1675
1991
  }
1676
- const firstAddress = cluster ? String(events.find((event) => event.ev === "connected")?.["at"] ?? "") : `${run.group?.members[primary]?.ip}:${(cell.group ?? cell.replicaSet)?.port}`;
1992
+ const firstAddress = cluster ? String(events.find((event) => event.ev === "connected")?.["at"] ?? "") : `${run.group?.members[primary]?.ip}:${(cell.group ?? cell.replicaSet ?? cell.kafka)?.port}`;
1677
1993
  const assumed = prediction.detects === null ? void 0 : prediction.rtt;
1678
1994
  const first = kernel.find((record) => record.netns === run.netns && record.dst === firstAddress && record.tcp?.srtt !== void 0);
1679
1995
  const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
@@ -1693,11 +2009,13 @@ const measureGroup = (cell, run) => {
1693
2009
  const findings = [];
1694
2010
  const switched = replica ? stages["elected"] : stages["+switch-master"];
1695
2011
  if (expected?.failover === false && switched !== void 0) findings.push(`the group failed over ${switched.value} s after the fault, where it should not`);
2012
+ const leaderAfter = run.group?.after ?? null;
2013
+ if (kafka && expected !== void 0 && leaderAfter !== null && expected.failover !== (leaderAfter !== primary)) findings.push(expected.failover ? `the leader stayed on ${primary}, where it should change` : `the leader moved to ${leaderAfter}, where it should stay on ${primary}`);
1696
2014
  for (const stage of expected?.quiet ?? []) {
1697
2015
  const at = stages[stage];
1698
2016
  if (at !== void 0) findings.push(`${stage} came ${at.value} s after the fault, where it should not`);
1699
2017
  }
1700
- if (accepted !== void 0 && lands === "same" && !cluster && to !== primary) findings.push(`the client wrote to ${to ?? String(accepted["at"])}, where it should stay on ${primary}`);
2018
+ if (accepted !== void 0 && lands === "same" && !cluster && !kafka && to !== primary) findings.push(`the client wrote to ${to ?? String(accepted["at"])}, where it should stay on ${primary}`);
1701
2019
  for (const error of expected?.errors ?? []) {
1702
2020
  const met = group.errors.find((each) => each.name === error.name);
1703
2021
  const noise = valueIn(noiseOf(cell), "s");
@@ -1710,9 +2028,22 @@ const measureGroup = (cell, run) => {
1710
2028
  if (lost === null) findings.push("the group did not converge, thus the count of its lost writes is not known");
1711
2029
  else if (lost.length < low || lost.length > high) findings.push(`the group lost ${lost.length} accepted writes, outside [${low}, ${high}]`);
1712
2030
  }
2031
+ if (expected?.duplicates !== void 0) {
2032
+ const { low, high } = expected.duplicates;
2033
+ const copies = group.writes.duplicates ?? null;
2034
+ if (copies === null) findings.push("the group did not converge, thus the count of its extra copies is not known");
2035
+ else if (run.group?.holds?.length === 0) findings.push("the group holds no write, thus the count of its extra copies says nothing");
2036
+ else if (copies.length < low || copies.length > high) findings.push(`the group holds ${copies.length} extra copies, outside [${low}, ${high}]`);
2037
+ }
2038
+ const { sets } = group.writes;
2039
+ for (const [name, band] of Object.entries(expected?.outcomes ?? {})) {
2040
+ const count = sets?.[name];
2041
+ if (count === void 0) findings.push(`the run has no count of ${name}`);
2042
+ else if (count < band.low || count > band.high) findings.push(`the count of ${name} is ${count}, outside [${band.low}, ${band.high}]`);
2043
+ }
1713
2044
  if (group.recovery !== void 0) findings.push(...recoveryFindings(group.recovery, group.writes));
1714
2045
  if (accepted === void 0) {
1715
- 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");
2046
+ if (prediction.detects !== null || kafka) findings.push(lands === "new" ? "the client never wrote to a new primary in the window" : "the client never wrote again in the window");
1716
2047
  return {
1717
2048
  ...silent,
1718
2049
  void: voided,
@@ -1981,7 +2312,7 @@ const requestOf = (events) => {
1981
2312
  };
1982
2313
  /** This function reads the socket of one run against its cell, as `measure` does without the second request of an HTTP client. */
1983
2314
  const measureSocket = (cell, run) => {
1984
- if (cell.group !== void 0 || cell.replicaSet !== void 0 || cell.cluster !== void 0) {
2315
+ if (cell.group !== void 0 || cell.replicaSet !== void 0 || cell.cluster !== void 0 || cell.pair !== void 0 || cell.kafka !== void 0) {
1985
2316
  const read = measureGroup(cell, run);
1986
2317
  return {
1987
2318
  environment: run.environment,
@@ -1999,6 +2330,8 @@ const measureSocket = (cell, run) => {
1999
2330
  group: read.group
2000
2331
  };
2001
2332
  }
2333
+ if (cell.prediction.detects === null && cell.prediction.queue !== void 0) return measureQueue(run, cell.prediction.queue);
2334
+ if (cell.prediction.detects === null && cell.prediction.ports !== void 0) return measurePorts(run, cell.prediction.ports);
2002
2335
  if (cell.prediction.detects !== null && cell.prediction.driver !== void 0) return measureDriver(cell, run);
2003
2336
  const { events, kernel } = run;
2004
2337
  const ready = events.find((event) => event.ev === "ready");
@@ -2155,12 +2488,13 @@ const measureSocket = (cell, run) => {
2155
2488
  }, run)) };
2156
2489
  const reconnect = prediction.reconnects === void 0 || ended === void 0 ? void 0 : reconnectOf(run, ended, cookie, drop?.line.t ?? ended);
2157
2490
  const late = reconnect === void 0 || prediction.reconnects === void 0 ? null : reconnectFinding(prediction.reconnects, reconnect, noiseOf(cell));
2491
+ const counted = prediction.counts === void 0 ? null : countFinding(prediction.counts, run.server ?? []);
2158
2492
  return {
2159
2493
  ...base,
2160
2494
  ...nothing,
2161
2495
  ...chain,
2162
2496
  ...reconnect === void 0 ? {} : { reconnect },
2163
- finding: late,
2497
+ finding: late ?? counted,
2164
2498
  outcome: ended !== void 0 ? outcome : { kind: "answered" },
2165
2499
  ...from === void 0 || until === void 0 ? {} : { elapsed: between(from, until) },
2166
2500
  noticed: prediction.from === void 0 ? null : noticed,
@@ -2918,7 +3252,7 @@ const TAIL_LINES = 60;
2918
3252
  const OUTPUT_LINES = 2e3;
2919
3253
  /** This time is how long a write of a hop waits for the line that confirms it, in milliseconds. */
2920
3254
  const CONFIRM_MS = 1e4;
2921
- const sleep$4 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3255
+ const sleep$6 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2922
3256
  /** This function gives the arguments of the injector for the options of a fault. */
2923
3257
  const optionArgs = (options) => [
2924
3258
  ...options.direction === void 0 ? [] : ["--direction", options.direction],
@@ -3460,6 +3794,7 @@ const openLab = async (target, kind = "tcp", topology) => {
3460
3794
  };
3461
3795
  const proxies = inChain.filter((hop) => hop.hop.kind === "proxy");
3462
3796
  return {
3797
+ serverIp,
3463
3798
  targetIp,
3464
3799
  targetPort,
3465
3800
  clientIp,
@@ -3524,7 +3859,7 @@ const openLab = async (target, kind = "tcp", topology) => {
3524
3859
  return stamped(where, [
3525
3860
  "/opt/harness/inject",
3526
3861
  kind,
3527
- peer,
3862
+ options.peers?.join(",") ?? peer,
3528
3863
  ...optionArgs(options),
3529
3864
  ...via
3530
3865
  ]);
@@ -3534,7 +3869,7 @@ const openLab = async (target, kind = "tcp", topology) => {
3534
3869
  await output(where, [
3535
3870
  "/opt/harness/inject",
3536
3871
  LIFT[kind],
3537
- peer,
3872
+ options.peers?.join(",") ?? peer,
3538
3873
  ...optionArgs(options),
3539
3874
  ...via
3540
3875
  ]);
@@ -3586,6 +3921,18 @@ const openLab = async (target, kind = "tcp", topology) => {
3586
3921
  if (target.close === void 0) throw new Error(`${target.image} names no way to close its clients`);
3587
3922
  await output(server, [...target.close]);
3588
3923
  },
3924
+ async exec(argv) {
3925
+ return output(server, [...argv]);
3926
+ },
3927
+ async sibling(siblingImage, argv, ready) {
3928
+ const started = await new GenericContainer(siblingImage).withNetwork(last).withWaitStrategy(Wait.forLogMessage(ready)).withCommand([...argv]).start();
3929
+ opened.push(started);
3930
+ return {
3931
+ ip: await ipOn(started, last),
3932
+ netns: await netnsOf(started),
3933
+ exec: (command) => output(started, [...command])
3934
+ };
3935
+ },
3589
3936
  async agent(agentImage, argv, options = {}) {
3590
3937
  const Shared = shared(GenericContainer);
3591
3938
  const seen = lines();
@@ -3726,7 +4073,7 @@ const openLab = async (target, kind = "tcp", topology) => {
3726
4073
  };
3727
4074
  while (!confirmed()) {
3728
4075
  if (Date.now() - started > CONFIRM_MS) throw new Error(`the hop ${event.hop} did not confirm the write of ${event.write.path}: no line "${confirm}" in ${CONFIRM_MS / 1e3} s`);
3729
- await sleep$4(50);
4076
+ await sleep$6(50);
3730
4077
  }
3731
4078
  }
3732
4079
  return stamp;
@@ -3864,6 +4211,213 @@ const openLab = async (target, kind = "tcp", topology) => {
3864
4211
  }
3865
4212
  };
3866
4213
  //#endregion
4214
+ //#region src/standby.ts
4215
+ /**
4216
+ * This module is the second server of a pair (`Cell.pair`). The lab of the cell has the primary, the
4217
+ * topology in front of it, the injector and the prober. This module adds one standby on the network
4218
+ * of the primary, and it does one run:
4219
+ *
4220
+ * 1. The client writes a stream of sequence numbers through the topology.
4221
+ * 2. The fault cuts the primary from the standby, or from each peer. The cut stays to the end.
4222
+ * 3. At the end of the hold, the injector stops each process of the primary with SIGKILL.
4223
+ * 4. The harness promotes the standby, and reads what it holds.
4224
+ * 5. The harness starts the old primary, and reads what its crash recovery gives.
4225
+ *
4226
+ * The standby has no way to the WAL of the old primary, because the primary stops under the cut. A
4227
+ * promotion cannot be undone, thus each run has its own environment (`Cell.fresh`).
4228
+ */
4229
+ /** This table holds one row for each write of the client. */
4230
+ const TABLE = "writes";
4231
+ /** This time bounds each wait of the setup of the pair, in ms. */
4232
+ const SETUP_MS = 6e4;
4233
+ /** This count is the acknowledged writes before the fault: the stream is steady. */
4234
+ const STEADY$4 = 20;
4235
+ /** This time is the wait after the stop of the primary, in ms: each writer sends its last write in it. */
4236
+ const LAST_WRITE_MS = 2e3;
4237
+ /** This time bounds the wait for the last line of the client, in ms. */
4238
+ const STOP_MS = 5e3;
4239
+ /** This text is the line of the log of a standby that serves reads (src/backend/postmaster/postmaster.c:3740). */
4240
+ const STANDBY_READY = "database system is ready to accept read-only connections";
4241
+ /** This text is the write of the client, with its sequence number. */
4242
+ const INSERT = `INSERT INTO ${TABLE} VALUES ($n)`;
4243
+ const sleep$5 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4244
+ /** This function gives the argv of one statement in psql, on the server in the same container. */
4245
+ const psql = (statement) => [
4246
+ "psql",
4247
+ "-h",
4248
+ "127.0.0.1",
4249
+ "-U",
4250
+ "postgres",
4251
+ "-At",
4252
+ "-c",
4253
+ statement
4254
+ ];
4255
+ /** This function gives the command of a standby: a base backup of the primary, then the server in recovery. */
4256
+ const standbyCommand = (primary) => [
4257
+ "sh",
4258
+ "-c",
4259
+ `chown postgres:postgres "$PGDATA"; chmod 0700 "$PGDATA"; until gosu postgres pg_basebackup -h ${primary} -U postgres -D "$PGDATA" -X stream -c fast -R; do gosu postgres sh -c 'rm -rf "$PGDATA"/*'; sleep 0.5; done; exec gosu postgres postgres`
4260
+ ];
4261
+ /** This command starts the old primary again, after the stop of each of its processes. Its crash recovery runs before the wait ends. */
4262
+ const RESTART = [
4263
+ "sh",
4264
+ "-c",
4265
+ "exec gosu postgres pg_ctl -D \"$PGDATA\" -l /tmp/restart.log -w -t 60 start"
4266
+ ];
4267
+ /** This function reads the output of a query for sequence numbers into a sorted list. */
4268
+ const numbersOf = (text) => text.split("\n").map((line) => line.trim()).filter((line) => line !== "").map(Number).sort((a, b) => a - b);
4269
+ /** This function gives the arguments of the client of a run. */
4270
+ const pairArgs = (cell, lab) => {
4271
+ const pair = cell.pair;
4272
+ const hold = cell.hold === void 0 ? 0 : valueIn(cell.hold, "s");
4273
+ return [
4274
+ lab.targetIp,
4275
+ String(lab.targetPort),
4276
+ "--mode",
4277
+ "stream",
4278
+ "--interval-ms",
4279
+ String(valueIn(pair.interval, "ms")),
4280
+ "--writers",
4281
+ String(pair.writers),
4282
+ "--query",
4283
+ INSERT,
4284
+ "--max-s",
4285
+ String(Math.ceil(hold) + 60),
4286
+ ...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)]
4287
+ ];
4288
+ };
4289
+ /** This function gives the peers of the cut of a pair: the standby, or the standby and the peer of the primary in the topology. */
4290
+ const peersOf$1 = (pair, standby, lab) => pair.cuts === "standby" ? [standby] : [standby, lab.topology?.targetPeer ?? lab.clientIp];
4291
+ /** This function gives the instants of the cancels of a hold, in ns from the fault: equal parts of the hold. */
4292
+ const cancelsOf = (pair, holdNs) => {
4293
+ const count = pair.cancels ?? 0;
4294
+ return Array.from({ length: count }, (_, at) => (at + 1) * holdNs / (count + 1));
4295
+ };
4296
+ /** This function asks until the primary reports the standby in a state of replication. */
4297
+ const untilStreaming = async (lab, state) => {
4298
+ const started = Date.now();
4299
+ for (;;) {
4300
+ const seen = await lab.exec(psql("SELECT sync_state FROM pg_stat_replication WHERE state = 'streaming'"));
4301
+ if (seen === state) return;
4302
+ if (Date.now() - started > SETUP_MS) throw new Error(`the standby did not stream as ${state} in ${SETUP_MS} ms: it reports "${seen}"`);
4303
+ await sleep$5(100);
4304
+ }
4305
+ };
4306
+ /**
4307
+ * This function opens the pair of an environment. The table comes first and the standby next: the
4308
+ * base backup has it. A synchronous standby is named last, thus no setup step waits for it.
4309
+ */
4310
+ const openStandby = async (lab, cell) => {
4311
+ const pair = cell.pair;
4312
+ await lab.exec([
4313
+ "sh",
4314
+ "-c",
4315
+ "echo \"host replication all all trust\" >> \"$PGDATA/pg_hba.conf\""
4316
+ ]);
4317
+ await lab.exec(psql("SELECT pg_reload_conf()"));
4318
+ await lab.exec(psql(`CREATE TABLE ${TABLE} (n integer PRIMARY KEY)`));
4319
+ const standby = await lab.sibling(await targetImageOf(cell.target), standbyCommand(lab.serverIp), STANDBY_READY);
4320
+ await untilStreaming(lab, "async");
4321
+ if (pair.synchronous) {
4322
+ await lab.exec(psql("ALTER SYSTEM SET synchronous_standby_names = '*'"));
4323
+ await lab.exec(psql("SELECT pg_reload_conf()"));
4324
+ await untilStreaming(lab, "sync");
4325
+ }
4326
+ return standby;
4327
+ };
4328
+ /** This function waits for a count of acknowledged writes. */
4329
+ const untilSteady = async (probe) => {
4330
+ const started = Date.now();
4331
+ while (probe.events.filter((event) => event.ev === "ack").length < STEADY$4) {
4332
+ if (Date.now() - started > SETUP_MS) throw new Error(`the client did not write ${STEADY$4} times in ${SETUP_MS} ms`);
4333
+ await sleep$5(10);
4334
+ }
4335
+ };
4336
+ /** This function gives the count of lost records in the latest clock line of the prober. */
4337
+ const droppedSoFar$5 = (records) => {
4338
+ for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
4339
+ return 0;
4340
+ };
4341
+ /**
4342
+ * This function does one run of a pair. A dry run writes and stops, with no fault: the warm-up of the
4343
+ * environment. It removes its rows, thus the numbers of the run start on an empty table.
4344
+ */
4345
+ const pairOnce = async (lab, standby, cell, environment, index, dry = false) => {
4346
+ const pair = cell.pair;
4347
+ await lab.forget();
4348
+ const from = lab.kernel.length;
4349
+ const lostBefore = droppedSoFar$5(lab.kernel);
4350
+ const probe = await lab.probe(pairArgs(cell, lab));
4351
+ const holdNs = cell.hold === void 0 ? 0 : valueIn(cell.hold, "s") * 1e9;
4352
+ let fault = null;
4353
+ let heal = null;
4354
+ try {
4355
+ await probe.until(["ready"], 15e3);
4356
+ await untilSteady(probe);
4357
+ if (!dry) {
4358
+ fault = await lab.fault("blackhole", "target", { peers: peersOf$1(pair, standby.ip, lab) });
4359
+ const started = Date.now();
4360
+ for (const at of cancelsOf(pair, holdNs)) {
4361
+ await sleep$5(Math.max(0, at / 1e6 - (Date.now() - started)));
4362
+ await lab.exec(psql("SELECT count(pg_cancel_backend(pid)) FROM pg_stat_activity WHERE wait_event = 'SyncRep'"));
4363
+ }
4364
+ heal = await lab.signal("KILL", fault.t0 + holdNs);
4365
+ await sleep$5(LAST_WRITE_MS);
4366
+ if (cell.target.kill !== void 0) await lab.exec(cell.target.kill);
4367
+ }
4368
+ await probe.go().catch(() => void 0);
4369
+ await probe.until(["done"], STOP_MS);
4370
+ } finally {
4371
+ await probe.stop();
4372
+ }
4373
+ if (dry) await lab.exec(psql(`TRUNCATE ${TABLE}`));
4374
+ const settled = await lab.settle();
4375
+ const lost = settled === null ? null : settled - lostBefore;
4376
+ const read = `SELECT n FROM ${TABLE} ORDER BY n`;
4377
+ let holds = null;
4378
+ let oldHolds = [];
4379
+ if (!dry) {
4380
+ await standby.exec(psql("SELECT pg_promote(true, 60)"));
4381
+ holds = numbersOf(await standby.exec(psql(read)));
4382
+ await lab.exec([...RESTART]);
4383
+ oldHolds = numbersOf(await lab.exec(psql(read)));
4384
+ }
4385
+ return {
4386
+ environment,
4387
+ index,
4388
+ events: [...probe.events],
4389
+ fault,
4390
+ ...heal === null ? {} : { heal },
4391
+ kernel: lab.kernel.slice(from),
4392
+ netns: lab.netns.client,
4393
+ targetNetns: lab.netns.target,
4394
+ lost,
4395
+ ...lab.topology === null ? {} : {
4396
+ topology: lab.topology,
4397
+ middlebox: []
4398
+ },
4399
+ group: {
4400
+ members: {
4401
+ primary: {
4402
+ ip: lab.serverIp,
4403
+ netns: lab.netns.target
4404
+ },
4405
+ standby: {
4406
+ ip: standby.ip,
4407
+ netns: standby.netns
4408
+ }
4409
+ },
4410
+ sentinels: {},
4411
+ primary: "primary",
4412
+ at: "primary",
4413
+ timeline: [],
4414
+ after: dry ? "primary" : "standby",
4415
+ holds,
4416
+ oldHolds
4417
+ }
4418
+ };
4419
+ };
4420
+ //#endregion
3867
4421
  //#region src/pins.ts
3868
4422
  /**
3869
4423
  * This module tells if a manifest pins each program that a run operated. A record that does not pin
@@ -3911,6 +4465,21 @@ const manifestProblems = (manifest) => [
3911
4465
  ];
3912
4466
  /** This function gives the problem of the commit of a record: no commit, or a text that is not a commit of git. */
3913
4467
  const commitProblems = (commit) => commit === void 0 ? ["the record has no commit of the code that operated"] : COMMIT.test(commit) ? [] : [`the record has the commit ${commit}, which is not 40 hex digits`];
4468
+ /** This problem is the problem of a checkout with changes: its commit does not hold the code that operated. */
4469
+ const DIRTY = "the checkout has changes to tracked files that no commit holds";
4470
+ /** This problem is the problem of a record of a local debug run. */
4471
+ const DEBUG = "the record is of a local debug run, and no page reads it";
4472
+ /**
4473
+ * This function gives each problem that stops a record from pinning the programs of its runs. A
4474
+ * record with a problem is not a record (SYS-101). The writer of the cells refuses it, and the read
4475
+ * of a schedule refuses it. A commit with changes to tracked files is not a pin.
4476
+ */
4477
+ const recordProblems = (record) => [.../* @__PURE__ */ new Set([
4478
+ ...commitProblems(record.code?.commit),
4479
+ ...record.code?.dirty === true ? [DIRTY] : [],
4480
+ ...record.manifests.flatMap((manifest) => manifestProblems(manifest)),
4481
+ ...record.debug === true ? [DEBUG] : []
4482
+ ])];
3914
4483
  //#endregion
3915
4484
  //#region src/group.ts
3916
4485
  /** This function gives the configuration file of a member: its port, its settings, and the primary that a replica follows. */
@@ -3929,19 +4498,19 @@ const sentinelConfig = (group, primary) => [
3929
4498
  ].join("\n");
3930
4499
  /**
3931
4500
  * This function gives the peers that a fault on a part cuts it from, by their addresses. `all` is
3932
- * each other part and the client, `group` is the other parts, and `clients` is the client. `one`
3933
- * and `one-way` are the first other member. The observer is not a peer, thus the timeline of the
4501
+ * each other part and the client, `group` is the other parts, and `clients` and `to-clients` are
4502
+ * the client. `one` and `one-way` are the first other member. The observer is not a peer, thus the timeline of the
3934
4503
  * group stays whole.
3935
4504
  */
3936
4505
  const peersOf = (parts, client, part, cuts) => {
3937
4506
  const others = [...Object.entries(parts.members), ...Object.entries(parts.sentinels)].filter(([name]) => name !== part).map(([, at]) => at.ip);
3938
- if (cuts === "clients") return [client];
4507
+ if (cuts === "clients" || cuts === "to-clients") return [client];
3939
4508
  if (cuts === "group") return others;
3940
4509
  if (cuts === "one" || cuts === "one-way") return Object.entries(parts.members).filter(([name]) => name !== part).slice(0, 1).map(([, at]) => at.ip);
3941
4510
  return [...others, client];
3942
4511
  };
3943
- /** This function gives the direction of the rules of a cut: `one-way` drops only the segments from the part. */
3944
- const directionOf = (cuts) => cuts === "one-way" ? ["--direction", "out"] : [];
4512
+ /** This function gives the direction of the rules of a cut: `one-way` and `to-clients` drop only the segments from the part. */
4513
+ const directionOf = (cuts) => cuts === "one-way" || cuts === "to-clients" ? ["--direction", "out"] : [];
3945
4514
  /** This function reads the flat reply of a command line client into its fields: name, value, name, value. */
3946
4515
  const fieldsOf = (text) => {
3947
4516
  const out = [];
@@ -4034,7 +4603,7 @@ const viewOf = (group, ips, sentinels, members) => {
4034
4603
  why: "healthy"
4035
4604
  };
4036
4605
  };
4037
- const sleep$3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4606
+ const sleep$4 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4038
4607
  /** This time is the longest that a group takes to converge before a run, or after it. */
4039
4608
  const CONVERGE_MS = 24e4;
4040
4609
  /**
@@ -4229,7 +4798,7 @@ const openGroupLab = async (group, options = {}) => {
4229
4798
  }));
4230
4799
  calm = last.healthy ? calm + 1 : 0;
4231
4800
  if (calm >= 2 && last.primary !== null) return last.primary;
4232
- await sleep$3(1e3);
4801
+ await sleep$4(1e3);
4233
4802
  }
4234
4803
  throw new Error(`the group did not converge in ${within} ms: ${last.why}`);
4235
4804
  },
@@ -4250,11 +4819,16 @@ const openGroupLab = async (group, options = {}) => {
4250
4819
  ]);
4251
4820
  },
4252
4821
  async holds(primary) {
4253
- return (await cli(primary, [
4822
+ return holdsOf(await cli(primary, [
4254
4823
  "--scan",
4255
4824
  "--pattern",
4256
4825
  "w:*"
4257
- ])).split("\n").map((key) => Number(key.trim().slice(2))).filter((n) => Number.isInteger(n) && n > 0).sort((a, b) => a - b);
4826
+ ]), await cli(primary, [
4827
+ "LRANGE",
4828
+ "w",
4829
+ "0",
4830
+ "-1"
4831
+ ]));
4258
4832
  },
4259
4833
  partOf: roles,
4260
4834
  async client(args) {
@@ -4309,7 +4883,7 @@ const openGroupLab = async (group, options = {}) => {
4309
4883
  const started = Date.now();
4310
4884
  while (await cli(member, ["PING"]).catch(() => "") !== "PONG") {
4311
4885
  if (Date.now() - started > 3e4) throw new Error(`${member} did not start again`);
4312
- await sleep$3(200);
4886
+ await sleep$4(200);
4313
4887
  }
4314
4888
  },
4315
4889
  failover(coordinated) {
@@ -4425,7 +4999,7 @@ const openGroupLab = async (group, options = {}) => {
4425
4999
  }
4426
5000
  };
4427
5001
  /** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
4428
- const STEADY$2 = 20;
5002
+ const STEADY$3 = 20;
4429
5003
  /** This time is how long a run continues after its outcome, so that the record shows what came next. */
4430
5004
  const TAIL_MS$3 = 2e3;
4431
5005
  /** This function gives the address of a member, with the port of the group. */
@@ -4435,22 +5009,32 @@ const deadlineOf = (cell) => {
4435
5009
  const { prediction } = cell;
4436
5010
  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;
4437
5011
  };
5012
+ /**
5013
+ * This function gives the sequence numbers that a group holds, in order, from the output of its
5014
+ * command line. They are the keys `w:<n>` of a stream of SET, and the entries of the list `w` of a
5015
+ * stream of RPUSH. A list keeps each copy, thus a number can repeat.
5016
+ */
5017
+ const holdsOf = (keys, list) => [...keys.split("\n").map((key) => Number(key.trim().slice(2))), ...list.split("\n").map((entry) => Number(entry.trim()))].filter((n) => Number.isInteger(n) && n > 0).sort((a, b) => a - b);
4438
5018
  /** This function gives the arguments of the client of a group cell. */
4439
5019
  const clientArgs = (cell, lab) => {
5020
+ const sentinels = cell.group.sentinels.map((name) => `${lab.sentinels[name].ip}:${cell.group.sentinelPort}`);
5021
+ const driver = cell.probe.driver?.name === "ioredis" ? cell.probe.driver : void 0;
4440
5022
  return [
4441
5023
  "--sentinels",
4442
- cell.group.sentinels.map((name) => `${lab.sentinels[name].ip}:${cell.group.sentinelPort}`).join(","),
5024
+ sentinels.join(","),
4443
5025
  "--name",
4444
5026
  cell.group.name,
4445
5027
  "--interval-ms",
4446
5028
  String(cell.probe.interval === void 0 ? 50 : valueIn(cell.probe.interval, "ms")),
4447
5029
  "--max-s",
4448
5030
  String(Math.ceil(deadlineOf(cell) / 1e3 + 120)),
4449
- ...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)]
5031
+ ...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)],
5032
+ ...driver?.op === void 0 ? [] : ["--op", driver.op],
5033
+ ...driver?.window === void 0 ? [] : ["--window", String(driver.window)]
4450
5034
  ];
4451
5035
  };
4452
5036
  /** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
4453
- const waitFor$2 = (client, match, within) => new Promise((resolve) => {
5037
+ const waitFor$3 = (client, match, within) => new Promise((resolve) => {
4454
5038
  const started = Date.now();
4455
5039
  const look = () => {
4456
5040
  const found = client.events.find(match);
@@ -4466,7 +5050,7 @@ const subscriptionsOf = (timeline) => {
4466
5050
  return [...latest.values()];
4467
5051
  };
4468
5052
  /** This function gives the count of lost records in the latest clock line of the prober. */
4469
- const droppedSoFar$3 = (records) => {
5053
+ const droppedSoFar$4 = (records) => {
4470
5054
  for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
4471
5055
  return 0;
4472
5056
  };
@@ -4488,7 +5072,7 @@ const groupOnce = async (lab, cell, environment, index) => {
4488
5072
  await lab.forget();
4489
5073
  const from = lab.kernel.length;
4490
5074
  const timelineFrom = lab.timeline.length;
4491
- const lostBefore = droppedSoFar$3(lab.kernel);
5075
+ const lostBefore = droppedSoFar$4(lab.kernel);
4492
5076
  const client = await lab.client(clientArgs(cell, lab));
4493
5077
  const lands = cell.prediction.group?.lands ?? "new";
4494
5078
  const place = cell.faultAt ?? { role: "primary" };
@@ -4498,7 +5082,7 @@ const groupOnce = async (lab, cell, environment, index) => {
4498
5082
  const undo = [];
4499
5083
  try {
4500
5084
  await client.until(["ready"], 6e4);
4501
- if (await waitFor$2(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY$2, 3e4) === void 0) throw new Error(`the client did not write ${STEADY$2} times in 30 s`);
5085
+ if (await waitFor$3(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY$3, 3e4) === void 0) throw new Error(`the client did not write ${STEADY$3} times in 30 s`);
4502
5086
  let healing = null;
4503
5087
  switch (cell.fault) {
4504
5088
  case "blackhole":
@@ -4512,7 +5096,7 @@ const groupOnce = async (lab, cell, environment, index) => {
4512
5096
  undo.push(() => lab.lift(kind, part, cuts));
4513
5097
  if (cell.stall !== void 0) {
4514
5098
  const t1 = fault.t1;
4515
- const sent = await waitFor$2(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
5099
+ const sent = await waitFor$3(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
4516
5100
  if (sent !== void 0) healing = lab.lift(kind, part, cuts, sent.t + valueIn(cell.stall, "s") * 1e9);
4517
5101
  } else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
4518
5102
  break;
@@ -4546,11 +5130,11 @@ const groupOnce = async (lab, cell, environment, index) => {
4546
5130
  const t0 = fault.t0;
4547
5131
  const old = addressOf(lab, primary);
4548
5132
  const deadline = deadlineOf(cell);
4549
- if ((lands === "new" ? await waitFor$2(client, (event) => event.ev === "ack" && event.t > t0 && typeof event["at"] === "string" && event["at"] !== old, deadline) : await (async () => {
5133
+ if ((lands === "new" ? await waitFor$3(client, (event) => event.ev === "ack" && event.t > t0 && typeof event["at"] === "string" && event["at"] !== old, deadline) : await (async () => {
4550
5134
  await healed;
4551
5135
  const after = heal?.t0 ?? t0;
4552
- return waitFor$2(client, (event) => event.ev === "ack" && event.t > after, deadline);
4553
- })()) !== void 0) await sleep$3(TAIL_MS$3);
5136
+ return waitFor$3(client, (event) => event.ev === "ack" && event.t > after, deadline);
5137
+ })()) !== void 0) await sleep$4(TAIL_MS$3);
4554
5138
  await healed;
4555
5139
  } finally {
4556
5140
  for (const step of undo.reverse()) await step().catch(() => void 0);
@@ -4643,6 +5227,10 @@ const cutOf = (set, members, client, part, cuts) => {
4643
5227
  peers: others.slice(0, 1),
4644
5228
  direction: "out"
4645
5229
  };
5230
+ if (cuts === "to-clients") return {
5231
+ peers: [client],
5232
+ direction: "out"
5233
+ };
4646
5234
  return { peers: [...others, client] };
4647
5235
  };
4648
5236
  /** This function gives the command of the server of each member. The server is the child of a shell, thus a fin can stop it and the container stays up. */
@@ -4714,7 +5302,7 @@ const statusViewOf = (set, ips, status) => {
4714
5302
  };
4715
5303
  };
4716
5304
  /** This function waits for a time, in ms. */
4717
- const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5305
+ const sleep$3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4718
5306
  /** These are the arguments of replSetStepDown: the time that the old primary cannot stand again, and the wait for a secondary to catch up. */
4719
5307
  const STEP_DOWN = {
4720
5308
  seconds: 60,
@@ -4862,7 +5450,7 @@ const openReplicaSetLab = async (set, backups = false) => {
4862
5450
  calm = last.healthy ? calm + 1 : 0;
4863
5451
  if (last.primary !== null) known = last.primary;
4864
5452
  if (calm >= 2 && last.primary !== null) return last.primary;
4865
- await sleep$2(1e3);
5453
+ await sleep$3(1e3);
4866
5454
  }
4867
5455
  throw new Error(`the set did not converge in ${within} ms: ${last.why}`);
4868
5456
  };
@@ -4968,7 +5556,7 @@ const openReplicaSetLab = async (set, backups = false) => {
4968
5556
  const started = Date.now();
4969
5557
  while (!(await shell(member, "print(db.hello().ok)").catch(() => "")).includes("1")) {
4970
5558
  if (Date.now() - started > 6e4) throw new Error(`${member} did not start again`);
4971
- await sleep$2(500);
5559
+ await sleep$3(500);
4972
5560
  }
4973
5561
  },
4974
5562
  exec(member, argv) {
@@ -5071,6 +5659,8 @@ const openReplicaSetLab = async (set, backups = false) => {
5071
5659
  };
5072
5660
  /** This time is how long a run continues after its outcome, so that the record shows what came next. */
5073
5661
  const TAIL_MS$2 = 2e3;
5662
+ /** This function gives how long a run of a cell continues after its lift, in ms: the tail, and the watch of the cell. */
5663
+ const afterLiftOf = (cell) => TAIL_MS$2 + (cell.watch === void 0 ? 0 : valueIn(cell.watch, "s") * 1e3);
5074
5664
  /** This function gives the arguments of the client of a replica set cell: each member, and the stream of writes. */
5075
5665
  const replicaSetArgs = (cell, members) => {
5076
5666
  const set = cell.replicaSet;
@@ -5094,7 +5684,7 @@ const replicaSetArgs = (cell, members) => {
5094
5684
  ];
5095
5685
  };
5096
5686
  /** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
5097
- const waitFor$1 = (client, match, within) => new Promise((resolve) => {
5687
+ const waitFor$2 = (client, match, within) => new Promise((resolve) => {
5098
5688
  const started = Date.now();
5099
5689
  const look = () => {
5100
5690
  const found = client.events.find(match);
@@ -5104,7 +5694,7 @@ const waitFor$1 = (client, match, within) => new Promise((resolve) => {
5104
5694
  look();
5105
5695
  });
5106
5696
  /** This function gives the count of lost records in the latest clock line of the prober. */
5107
- const droppedSoFar$2 = (records) => {
5697
+ const droppedSoFar$3 = (records) => {
5108
5698
  for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
5109
5699
  return 0;
5110
5700
  };
@@ -5129,7 +5719,7 @@ const replicaSetOnce = async (lab, cell, environment, index) => {
5129
5719
  await lab.forget();
5130
5720
  const from = lab.kernel.length;
5131
5721
  const logFrom = lab.log.length;
5132
- const lostBefore = droppedSoFar$2(lab.kernel);
5722
+ const lostBefore = droppedSoFar$3(lab.kernel);
5133
5723
  const client = await lab.client(replicaSetArgs(cell, lab.members));
5134
5724
  const lands = cell.prediction.group?.lands ?? "new";
5135
5725
  const place = cell.faultAt ?? { role: "primary" };
@@ -5139,7 +5729,7 @@ const replicaSetOnce = async (lab, cell, environment, index) => {
5139
5729
  const undo = [];
5140
5730
  try {
5141
5731
  await client.until(["ready"], 6e4);
5142
- if (await waitFor$1(client, (event) => event.ev === "ack" && Number(event["n"]) >= 20, 3e4) === void 0) throw new Error(`the client did not write 20 times in 30 s`);
5732
+ if (await waitFor$2(client, (event) => event.ev === "ack" && Number(event["n"]) >= 20, 3e4) === void 0) throw new Error(`the client did not write 20 times in 30 s`);
5143
5733
  let healing = null;
5144
5734
  switch (cell.fault) {
5145
5735
  case "blackhole":
@@ -5153,7 +5743,7 @@ const replicaSetOnce = async (lab, cell, environment, index) => {
5153
5743
  undo.push(() => lab.lift(kind, part, cuts));
5154
5744
  if (cell.stall !== void 0) {
5155
5745
  const t1 = fault.t1;
5156
- const sent = await waitFor$1(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
5746
+ const sent = await waitFor$2(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
5157
5747
  healing = lab.lift(kind, part, cuts, (sent?.t ?? t1) + valueIn(cell.stall, "s") * 1e9);
5158
5748
  } else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
5159
5749
  break;
@@ -5188,10 +5778,10 @@ const replicaSetOnce = async (lab, cell, environment, index) => {
5188
5778
  const t1 = fault.t1;
5189
5779
  const old = `${lab.members[primary].ip}:${set.port}`;
5190
5780
  const sentAt = (n) => client.events.find((event) => event.ev === "sent" && Number(event["n"]) === n)?.t ?? -Infinity;
5191
- if (await waitFor$1(client, (event) => event.ev === "ack" && sentAt(Number(event["n"])) > t1 && (lands === "same" || typeof event["at"] === "string" && event["at"] !== old), deadlineOf(cell)) !== void 0) await sleep$2(TAIL_MS$2);
5781
+ if (await waitFor$2(client, (event) => event.ev === "ack" && sentAt(Number(event["n"])) > t1 && (lands === "same" || typeof event["at"] === "string" && event["at"] !== old), deadlineOf(cell)) !== void 0) await sleep$3(TAIL_MS$2);
5192
5782
  if (healing !== null) {
5193
5783
  await healed.catch(() => void 0);
5194
- await sleep$2(TAIL_MS$2);
5784
+ await sleep$3(afterLiftOf(cell));
5195
5785
  }
5196
5786
  } finally {
5197
5787
  for (const step of undo.reverse()) await step().catch(() => void 0);
@@ -5249,7 +5839,7 @@ const until = async (what, check, within) => {
5249
5839
  const started = Date.now();
5250
5840
  while (!await check().catch(() => false)) {
5251
5841
  if (Date.now() - started > within) throw new Error(`${what} did not come in ${within} ms`);
5252
- await sleep$2(500);
5842
+ await sleep$3(500);
5253
5843
  }
5254
5844
  };
5255
5845
  /** This function gives the time that the client sent a write. Zero is no write. */
@@ -5307,7 +5897,7 @@ const backupsOf = async (lab, drill, primary, writer) => {
5307
5897
  }];
5308
5898
  const every = valueIn(drill.sliceEvery, "s") * 1e3;
5309
5899
  for (let slice = 1; slice <= drill.slices; slice += 1) {
5310
- await sleep$2(Math.max(0, ended + slice * every - Date.now()));
5900
+ await sleep$3(Math.max(0, ended + slice * every - Date.now()));
5311
5901
  const out = `${VAULT}/slice-${slice}`;
5312
5902
  const begun = Date.now();
5313
5903
  await lab.vault([
@@ -5351,7 +5941,7 @@ const lose = async (lab, drill, primary, last) => {
5351
5941
  const parts = drill.recovery === "restore" ? [primary, ...lab.set.members.filter((name) => name !== primary)] : [primary];
5352
5942
  const age = valueIn(drill.age, "s");
5353
5943
  const at = last === null || drill.quiet === true ? void 0 : last.t + age * 1e9;
5354
- if (at === void 0) await sleep$2(age * 1e3);
5944
+ if (at === void 0) await sleep$3(age * 1e3);
5355
5945
  const stamps = await Promise.all(parts.map((part) => lab.signal(part, "KILL", at)));
5356
5946
  for (const part of parts) {
5357
5947
  await lab.exec(part, [
@@ -5442,7 +6032,7 @@ const drillOnce = async (lab, cell, environment, index) => {
5442
6032
  const tool = (await lab.vault(["mongodump", "--version"])).split("\n")[0].trim();
5443
6033
  const from = lab.kernel.length;
5444
6034
  const logFrom = lab.log.length;
5445
- const lostBefore = droppedSoFar$2(lab.kernel);
6035
+ const lostBefore = droppedSoFar$3(lab.kernel);
5446
6036
  const writer = await lab.client(replicaSetArgs(cell, lab.members));
5447
6037
  const restores = drill.recovery === "restore";
5448
6038
  let successor = null;
@@ -5452,7 +6042,7 @@ const drillOnce = async (lab, cell, environment, index) => {
5452
6042
  const backups = [];
5453
6043
  try {
5454
6044
  await writer.until(["ready"], 6e4);
5455
- if (await waitFor$1(writer, (event) => event.ev === "ack" && Number(event["n"]) >= 20, 3e4) === void 0) throw new Error(`the client did not write 20 times in 30 s`);
6045
+ if (await waitFor$2(writer, (event) => event.ev === "ack" && Number(event["n"]) >= 20, 3e4) === void 0) throw new Error(`the client did not write 20 times in 30 s`);
5456
6046
  if (drill.quiet === true) await writer.stop();
5457
6047
  if (restores) backups.push(...await backupsOf(lab, drill, primary, writer));
5458
6048
  const stamp = await lose(lab, drill, primary, backups.at(-1)?.last ?? null);
@@ -5466,11 +6056,11 @@ const drillOnce = async (lab, cell, environment, index) => {
5466
6056
  "--from",
5467
6057
  String(second)
5468
6058
  ]);
5469
- if (await waitFor$1(successor, (event) => event.ev === "ack", deadlineOf(cell)) !== void 0) await sleep$2(TAIL_MS$2);
6059
+ if (await waitFor$2(successor, (event) => event.ev === "ack", deadlineOf(cell)) !== void 0) await sleep$3(TAIL_MS$2);
5470
6060
  } else {
5471
6061
  const old = `${lab.members[primary].ip}:${set.port}`;
5472
6062
  const sentAt = (n) => writer.events.find((event) => event.ev === "sent" && Number(event["n"]) === n)?.t ?? -Infinity;
5473
- if (await waitFor$1(writer, (event) => event.ev === "ack" && sentAt(Number(event["n"])) > stamp.t1 && typeof event["at"] === "string" && event["at"] !== old, deadlineOf(cell)) !== void 0) await sleep$2(TAIL_MS$2);
6063
+ if (await waitFor$2(writer, (event) => event.ev === "ack" && sentAt(Number(event["n"])) > stamp.t1 && typeof event["at"] === "string" && event["at"] !== old, deadlineOf(cell)) !== void 0) await sleep$3(TAIL_MS$2);
5474
6064
  await lab.restart(primary);
5475
6065
  }
5476
6066
  } finally {
@@ -5632,6 +6222,7 @@ const clusterViewOf = (cluster, rows) => {
5632
6222
  * This function gives the peers that a fault on a node cuts it from. `all` is each other node and the
5633
6223
  * client, `group` is the other nodes, and `clients` is the client. `one` is the first other node in
5634
6224
  * the sequence of the cluster, and `one-way` is the same node for the segments from the part only.
6225
+ * `to-clients` is the client, for the segments from the part only.
5635
6226
  */
5636
6227
  const clusterCutOf = (cluster, members, client, part, cuts) => {
5637
6228
  const others = cluster.members.filter((name) => name !== part).map((name) => members[name].ip);
@@ -5642,9 +6233,13 @@ const clusterCutOf = (cluster, members, client, part, cuts) => {
5642
6233
  peers: others.slice(0, 1),
5643
6234
  direction: "out"
5644
6235
  };
6236
+ if (cuts === "to-clients") return {
6237
+ peers: [client],
6238
+ direction: "out"
6239
+ };
5645
6240
  return { peers: [...others, client] };
5646
6241
  };
5647
- const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6242
+ const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5648
6243
  /**
5649
6244
  * This function opens one environment of a cluster cell:
5650
6245
  * - one new network, with each node, the client and the admin on it.
@@ -5806,7 +6401,7 @@ const openClusterLab = async (cluster) => {
5806
6401
  calm = last.healthy ? calm + 1 : 0;
5807
6402
  if (last.primary !== null) known = last.primary;
5808
6403
  if (calm >= 2 && last.primary !== null) return last.primary;
5809
- await sleep$1(1e3);
6404
+ await sleep$2(1e3);
5810
6405
  }
5811
6406
  throw new Error(`the queue did not converge in ${within} ms: ${last.why}`);
5812
6407
  };
@@ -5822,7 +6417,7 @@ const openClusterLab = async (cluster) => {
5822
6417
  const running = (text.trim() === "" ? {} : toolJsonOf(text)).running_nodes ?? [];
5823
6418
  if (cluster.members.every((name) => running.includes(nodeNameOf(name)))) break;
5824
6419
  if (Date.now() - started > 12e4) throw new Error(`the cluster did not form in 120 s: ${running.join(", ")} run`);
5825
- await sleep$1(1e3);
6420
+ await sleep$2(1e3);
5826
6421
  }
5827
6422
  const declared = lastLine(await output(commander, [
5828
6423
  "node",
@@ -5942,7 +6537,7 @@ const openClusterLab = async (cluster) => {
5942
6537
  "check_running"
5943
6538
  ]).then(() => true).catch(() => false)) {
5944
6539
  if (Date.now() - begun > 12e4) throw new Error(`${member} did not start again`);
5945
- await sleep$1(1e3);
6540
+ await sleep$2(1e3);
5946
6541
  }
5947
6542
  },
5948
6543
  stepDown(leader, to) {
@@ -6044,7 +6639,7 @@ const openClusterLab = async (cluster) => {
6044
6639
  }
6045
6640
  };
6046
6641
  /** This count is the number of confirmed publishes that a run waits for before its fault: the stream is steady then. */
6047
- const STEADY = 20;
6642
+ const STEADY$1 = 20;
6048
6643
  /** This time is how long a run continues after its outcome, so that the record shows what came next. */
6049
6644
  const TAIL_MS$1 = 2e3;
6050
6645
  /**
@@ -6073,7 +6668,7 @@ const clusterArgs = (cell, members, leader) => [
6073
6668
  ...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)]
6074
6669
  ];
6075
6670
  /** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
6076
- const waitFor = (client, match, within) => new Promise((resolve) => {
6671
+ const waitFor$1 = (client, match, within) => new Promise((resolve) => {
6077
6672
  const started = Date.now();
6078
6673
  const look = () => {
6079
6674
  const found = client.events.find(match);
@@ -6083,7 +6678,7 @@ const waitFor = (client, match, within) => new Promise((resolve) => {
6083
6678
  look();
6084
6679
  });
6085
6680
  /** This function gives the count of lost records in the latest clock line of the prober. */
6086
- const droppedSoFar$1 = (records) => {
6681
+ const droppedSoFar$2 = (records) => {
6087
6682
  for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
6088
6683
  return 0;
6089
6684
  };
@@ -6113,7 +6708,7 @@ const stampOf = async (lab, client, logFrom, marks, wall) => {
6113
6708
  },
6114
6709
  line
6115
6710
  };
6116
- await sleep$1(100);
6711
+ await sleep$2(100);
6117
6712
  }
6118
6713
  return {
6119
6714
  stamp: monotonicOf(wall, client.events),
@@ -6150,7 +6745,7 @@ const clusterOnce = async (lab, cell, environment, index) => {
6150
6745
  await lab.forget();
6151
6746
  const from = lab.kernel.length;
6152
6747
  const logFrom = lab.log.length;
6153
- const lostBefore = droppedSoFar$1(lab.kernel);
6748
+ const lostBefore = droppedSoFar$2(lab.kernel);
6154
6749
  const client = await lab.client(clusterArgs(cell, lab.members, leader));
6155
6750
  const place = cell.faultAt ?? { role: "primary" };
6156
6751
  let fault = null;
@@ -6161,7 +6756,7 @@ const clusterOnce = async (lab, cell, environment, index) => {
6161
6756
  const undo = [];
6162
6757
  try {
6163
6758
  await client.until(["ready"], 6e4);
6164
- if (await waitFor(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY, 3e4) === void 0) throw new Error(`the client did not publish ${STEADY} times in 30 s`);
6759
+ if (await waitFor$1(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY$1, 3e4) === void 0) throw new Error(`the client did not publish ${STEADY$1} times in 30 s`);
6165
6760
  let healing = null;
6166
6761
  switch (cell.fault) {
6167
6762
  case "blackhole":
@@ -6215,10 +6810,10 @@ const clusterOnce = async (lab, cell, environment, index) => {
6215
6810
  healed.catch(() => void 0);
6216
6811
  const t1 = fault.t1;
6217
6812
  const sentAt = (n) => client.events.find((event) => event.ev === "sent" && Number(event["n"]) === n)?.t ?? -Infinity;
6218
- if (await waitFor(client, (event) => event.ev === "ack" && sentAt(Number(event["n"])) > t1, deadlineOf(cell)) !== void 0) await sleep$1(TAIL_MS$1);
6813
+ if (await waitFor$1(client, (event) => event.ev === "ack" && sentAt(Number(event["n"])) > t1, deadlineOf(cell)) !== void 0) await sleep$2(TAIL_MS$1);
6219
6814
  if (healing !== null) {
6220
6815
  await healed.catch(() => void 0);
6221
- await sleep$1(TAIL_MS$1);
6816
+ await sleep$2(TAIL_MS$1);
6222
6817
  }
6223
6818
  } finally {
6224
6819
  for (const step of undo.reverse()) await step().catch(() => void 0);
@@ -6250,42 +6845,540 @@ const clusterOnce = async (lab, cell, environment, index) => {
6250
6845
  };
6251
6846
  };
6252
6847
  //#endregion
6253
- //#region src/run.ts
6848
+ //#region src/kafka.ts
6254
6849
  /**
6255
- * This function measures and judges the raw records of a cell, with the code of the harness at
6256
- * this time. `runCell` uses it during a cell. A holder of the raw records of a cell can use it
6257
- * again later. Thus the records get a new analysis after a change to observe.ts or to the verdict.
6850
+ * This module is the lab of a group of Kafka brokers in KRaft mode with one controller. It opens one
6851
+ * environment for a Kafka cell, for the runs of the cell. Each node is a container from the stock
6852
+ * image, in its own network namespace, and an injector sidecar shares its network and process
6853
+ * namespaces. The image starts by its own command, and each setting comes from the environment.
6854
+ *
6855
+ * Each run makes one new topic, thus each run starts with an empty log and its client counts from 1.
6856
+ * The client is the producer of kafka-clients. The commander holds the admin, on the network but
6857
+ * not in the namespace that the prober watches. The record of a run is the lines of the client and
6858
+ * the whole partition, read to its high watermark after the group converged.
6258
6859
  */
6259
- const judge = (cell, runs) => {
6260
- const measured = runs.filter((run) => run.warmup !== true).map((run) => measure(cell, run));
6860
+ /** This path is the class path of the Kafka image: the JVM expands the star itself. */
6861
+ const CLASS_PATH = "/opt/harness/kafka/classes:/opt/harness/kafka/lib/*";
6862
+ /** This option is the heap of the client and of the admin. It has no source: the two hold a producer or a read of one partition. */
6863
+ const CLIENT_HEAP = "-Xmx256m";
6864
+ /** This function gives the node id of a name. A broker has its place in the list from 1, and the controller has the id after them. */
6865
+ const nodeIdOf = (kafka, name) => {
6866
+ const at = kafka.brokers.indexOf(name);
6867
+ if (at !== -1) return at + 1;
6868
+ if (name === kafka.controller) return kafka.brokers.length + 1;
6869
+ throw new Error(`the Kafka group has no node ${name}`);
6870
+ };
6871
+ /**
6872
+ * This function gives the variable of a server property for the image. The wrapper of the image
6873
+ * reverses this map (KafkaDockerWrapper.scala:188-203). After the prefix `KAFKA_`, `___` is a
6874
+ * hyphen, `__` is an underscore and `_` is a dot.
6875
+ */
6876
+ const envOf = (property) => `KAFKA_${property.replace(/_/g, "__").replace(/-/g, "___").replace(/\./g, "_").toUpperCase()}`;
6877
+ /** This function gives the variables of each node: the listener names, the voters, the log and the settings of the group. */
6878
+ const commonEnv = (kafka) => ({
6879
+ CLUSTER_ID: kafka.clusterId,
6880
+ KAFKA_HEAP_OPTS: kafka.heap,
6881
+ KAFKA_CONTROLLER_QUORUM_VOTERS: `${nodeIdOf(kafka, kafka.controller)}@${kafka.controller}:${kafka.controllerPort}`,
6882
+ KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER",
6883
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT",
6884
+ KAFKA_LOG_DIRS: "/var/lib/kafka/data",
6885
+ ...Object.fromEntries(Object.entries(kafka.settings).map(([property, value]) => [envOf(property), value]))
6886
+ });
6887
+ /** This function gives the environment of a broker. */
6888
+ const brokerEnv = (kafka, name) => ({
6889
+ ...commonEnv(kafka),
6890
+ KAFKA_PROCESS_ROLES: "broker",
6891
+ KAFKA_NODE_ID: String(nodeIdOf(kafka, name)),
6892
+ KAFKA_LISTENERS: `PLAINTEXT://0.0.0.0:${kafka.port}`,
6893
+ KAFKA_ADVERTISED_LISTENERS: `PLAINTEXT://${name}:${kafka.port}`,
6894
+ KAFKA_INTER_BROKER_LISTENER_NAME: "PLAINTEXT"
6895
+ });
6896
+ /** This function gives the environment of the controller. The start script of the image stops on an advertised listener here (configure:43-48). */
6897
+ const controllerEnv = (kafka) => ({
6898
+ ...commonEnv(kafka),
6899
+ KAFKA_PROCESS_ROLES: "controller",
6900
+ KAFKA_NODE_ID: String(nodeIdOf(kafka, kafka.controller)),
6901
+ KAFKA_LISTENERS: `CONTROLLER://0.0.0.0:${kafka.controllerPort}`
6902
+ });
6903
+ /**
6904
+ * This function judges a view of a partition: its leader, and if the group is healthy. A healthy
6905
+ * group has each broker not fenced, one leader, and each broker in the in-sync replicas.
6906
+ */
6907
+ const kafkaViewOf = (kafka, view) => {
6908
+ const primary = view.leader === void 0 || view.leader === null ? null : kafka.brokers[view.leader - 1] ?? null;
6909
+ const all = kafka.brokers.length;
6910
+ if (view.brokers.length !== all) return {
6911
+ primary,
6912
+ healthy: false,
6913
+ why: `${view.brokers.length} brokers are not fenced`
6914
+ };
6915
+ if (primary === null) return {
6916
+ primary,
6917
+ healthy: false,
6918
+ why: "the partition has no leader"
6919
+ };
6920
+ if (view.replicas?.length !== all) return {
6921
+ primary,
6922
+ healthy: false,
6923
+ why: `the partition has ${view.replicas?.length ?? 0} replicas`
6924
+ };
6925
+ if (view.isr?.length !== all) return {
6926
+ primary,
6927
+ healthy: false,
6928
+ why: `the partition has ${view.isr?.length ?? 0} in-sync replicas`
6929
+ };
6261
6930
  return {
6262
- measured,
6263
- verdict: verdictOf(cell.prediction, measured, noiseOf(cell))
6931
+ primary,
6932
+ healthy: true,
6933
+ why: "healthy"
6264
6934
  };
6265
6935
  };
6266
- /** This function gives the count of lost records in the latest clock line of the prober, since it began. */
6267
- const droppedSoFar = (records) => {
6268
- for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
6269
- return 0;
6270
- };
6271
- /** This function gives the argv of the client for a cell, with a deadline far after all that it waits for. */
6272
- const probeArgs = (cell, lab) => {
6273
- const { prediction, probe } = cell;
6274
- const longest = (prediction.detects === true && prediction.driver !== void 0 ? longestOf(cell) : prediction.detects ? latestEndOf(prediction) : valueIn(prediction.window, "s")) + (cell.idleBefore === void 0 ? 0 : valueIn(cell.idleBefore, "s"));
6275
- const args = [
6276
- lab.targetIp,
6277
- String(lab.targetPort),
6278
- "--mode",
6279
- probe.mode
6280
- ];
6281
- if (probe.ws !== void 0) {
6282
- const { ws } = probe;
6283
- const waits = (prediction.detects !== false ? prediction.reconnects?.waits ?? [] : []).reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
6284
- args.push("--websocket", ws.socket);
6285
- args.push("--heartbeat", ws.heartbeat === null ? "off" : "on");
6286
- if (ws.heartbeat !== null) args.push("--heartbeat-interval-ms", String(valueIn(ws.heartbeat.interval, "ms")), "--heartbeat-timeout-ms", String(valueIn(ws.heartbeat.timeout, "ms")), "--heartbeat-misses", String(ws.heartbeat.misses));
6287
- args.push("--reconnect", ws.reconnect === null ? "off" : "on");
6288
- if (ws.reconnect !== null) args.push("--reconnect-initial-ms", String(valueIn(ws.reconnect.initialDelay, "ms")), "--reconnect-multiplier", String(ws.reconnect.multiplier), "--reconnect-max-ms", String(valueIn(ws.reconnect.maxDelay, "ms")), "--reconnect-attempts", String(ws.reconnect.attempts), "--reconnect-jitter", String(ws.reconnect.jitter));
6936
+ /**
6937
+ * This function gives the peers that a fault on a broker cuts it from. Only `group` has a law: the
6938
+ * other brokers and the controller, and the client keeps its route to the broker.
6939
+ */
6940
+ const kafkaCutOf = (kafka, members, controller, part, cuts) => {
6941
+ if (cuts !== "group") throw new Error(`a Kafka cell has no cut ${cuts}`);
6942
+ return { peers: [...kafka.brokers.filter((name) => name !== part).map((name) => members[name].ip), controller.ip] };
6943
+ };
6944
+ /** This function gives the addresses that the client and the admin bootstrap from. */
6945
+ const bootstrapOf = (kafka) => kafka.brokers.map((name) => `${name}:${kafka.port}`).join(",");
6946
+ /** This function gives the name of the topic of a run. A warm-up has the index -1. */
6947
+ const topicOf = (environment, index) => `cells-e${environment}-r${index + 1}`;
6948
+ const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6949
+ /**
6950
+ * This function opens one environment of a Kafka cell:
6951
+ * - one new network, with each node, the client and the commander on it.
6952
+ * - the controller first, then each broker: the stock image with its own command. The brokers
6953
+ * register with the controller, thus they need it first.
6954
+ * - an injector sidecar for each node, in its network and process namespaces.
6955
+ * - a client that holds its own network namespace, with a privileged sidecar for its sysctls.
6956
+ * - the prober, which watches the namespace of the client.
6957
+ */
6958
+ const openKafkaLab = async (kafka) => {
6959
+ const image = await harnessImage();
6960
+ const clientImage = await kafkaClientImage();
6961
+ const { GenericContainer, Network, Wait, getContainerRuntimeClient } = await containers();
6962
+ const runtime = await getContainerRuntimeClient();
6963
+ const opened = [];
6964
+ let network;
6965
+ const close = async () => {
6966
+ for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
6967
+ await network?.stop().catch(() => void 0);
6968
+ };
6969
+ const ipOf = async (container) => {
6970
+ const ip = (await runtime.container.inspect(runtime.container.getById(container.getId()))).NetworkSettings.Networks[network.getName()]?.IPAddress;
6971
+ if (ip === void 0 || ip === "") throw new Error(`${container.getName()} has no address on ${network.getName()}`);
6972
+ return ip;
6973
+ };
6974
+ try {
6975
+ network = await new Network().start();
6976
+ const started = network;
6977
+ const boxes = /* @__PURE__ */ new Map();
6978
+ const injectors = /* @__PURE__ */ new Map();
6979
+ const parts = {};
6980
+ const raw = {};
6981
+ const openNode = async (name, environment) => {
6982
+ let partial = "";
6983
+ const box = await new GenericContainer(kafka.image).withNetwork(started).withNetworkAliases(name).withHostname(name).withEnvironment(environment).withWaitStrategy(Wait.forLogMessage(kafka.ready)).withStartupTimeout(18e4).withLogConsumer((stream) => stream.on("data", (chunk) => {
6984
+ partial += chunk.toString();
6985
+ const split = partial.split("\n");
6986
+ partial = split.pop() ?? "";
6987
+ for (const text of split) {
6988
+ const tail = raw[name] ??= [];
6989
+ tail.push(text);
6990
+ if (tail.length > 400) tail.shift();
6991
+ }
6992
+ })).start();
6993
+ opened.push(box);
6994
+ boxes.set(name, box);
6995
+ const injector = await withPid(new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${box.getId()}`).start();
6996
+ opened.push(injector);
6997
+ injectors.set(name, injector);
6998
+ parts[name] = {
6999
+ ip: await ipOf(box),
7000
+ netns: await netnsOf(injector)
7001
+ };
7002
+ };
7003
+ await openNode(kafka.controller, controllerEnv(kafka));
7004
+ for (const name of kafka.brokers) await openNode(name, brokerEnv(kafka, name));
7005
+ const members = Object.fromEntries(kafka.brokers.map((name) => [name, parts[name]]));
7006
+ const controller = parts[kafka.controller];
7007
+ const holder = await new GenericContainer(image).withNetwork(network).start();
7008
+ opened.push(holder);
7009
+ const admin = await new GenericContainer(image).withNetworkMode(`container:${holder.getId()}`).withPrivilegedMode().start();
7010
+ opened.push(admin);
7011
+ const clientIp = await ipOf(holder);
7012
+ const commander = await new GenericContainer(clientImage).withNetwork(network).withCommand([
7013
+ "sh",
7014
+ "-c",
7015
+ "while :; do sleep 3600; done"
7016
+ ]).start();
7017
+ opened.push(commander);
7018
+ const clientNetns = await netnsOf(admin);
7019
+ const records = lines();
7020
+ const prober = await new GenericContainer(image).withPrivilegedMode().withNetworkMode("none").withCommand(["/opt/harness/prober", String(clientNetns)]).withLogConsumer(records.consume).start();
7021
+ opened.push(prober);
7022
+ const begun = await records.until(["begin", "fail"], 3e4, "prober");
7023
+ if (begun.ev === "fail") throw new Error(`the prober failed: ${String(begun["why"])}`);
7024
+ const box = (name) => {
7025
+ const found = boxes.get(name);
7026
+ if (found === void 0) throw new Error(`the group has no node ${name}`);
7027
+ return found;
7028
+ };
7029
+ const injectorOf = (name) => {
7030
+ const found = injectors.get(name);
7031
+ if (found === void 0) throw new Error(`the group has no node ${name}`);
7032
+ return found;
7033
+ };
7034
+ const lastLine = (text) => text.split("\n").filter((line) => line.trim() !== "").at(-1) ?? "";
7035
+ const stamped = async (where, argv) => {
7036
+ const stamp = JSON.parse(lastLine(await output(where, argv)));
7037
+ return {
7038
+ t0: stamp.t0,
7039
+ t1: stamp.t1
7040
+ };
7041
+ };
7042
+ const after = (at) => at === void 0 ? [] : ["--at", String(Math.round(at))];
7043
+ const bootstrap = bootstrapOf(kafka);
7044
+ const tool = async (...argv) => lastLine(await output(commander, [
7045
+ "java",
7046
+ CLIENT_HEAP,
7047
+ "-cp",
7048
+ CLASS_PATH,
7049
+ "AdminKafka",
7050
+ bootstrap,
7051
+ ...argv
7052
+ ]));
7053
+ const view = async (topic) => JSON.parse(await tool("view", ...topic === void 0 ? [] : [topic]));
7054
+ return {
7055
+ kafka,
7056
+ clientIp,
7057
+ members,
7058
+ controller,
7059
+ netns: { client: clientNetns },
7060
+ kernel: records.seen,
7061
+ raw,
7062
+ async sysctl(values) {
7063
+ const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
7064
+ if (settings.length > 0) await output(admin, [
7065
+ "sysctl",
7066
+ "-q",
7067
+ "-w",
7068
+ ...settings
7069
+ ]);
7070
+ const read = {};
7071
+ for (const key of Object.keys(values)) read[key] = await output(admin, [
7072
+ "sysctl",
7073
+ "-n",
7074
+ key
7075
+ ]);
7076
+ return read;
7077
+ },
7078
+ async brokersUp(within = CONVERGE_MS) {
7079
+ const begin = Date.now();
7080
+ let last = "no view yet";
7081
+ while (Date.now() - begin < within) {
7082
+ const read = await view().catch((error) => error.message.slice(0, 200));
7083
+ if (typeof read !== "string" && read.brokers.length === kafka.brokers.length) return;
7084
+ last = typeof read === "string" ? read : `${read.brokers.length} brokers are not fenced`;
7085
+ await sleep$1(1e3);
7086
+ }
7087
+ throw new Error(`the brokers did not come up in ${within} ms: ${last}`);
7088
+ },
7089
+ async create(topic) {
7090
+ const ids = kafka.brokers.map((name) => nodeIdOf(kafka, name)).join(",");
7091
+ const reply = await tool("create", topic, String(kafka.minInsync), ids);
7092
+ if (!reply.includes("\"ok\":true")) throw new Error(`the create of the topic failed: ${reply}`);
7093
+ },
7094
+ async converge(topic, within = CONVERGE_MS) {
7095
+ const begin = Date.now();
7096
+ let calm = 0;
7097
+ let last = {
7098
+ primary: null,
7099
+ healthy: false,
7100
+ why: "no view yet"
7101
+ };
7102
+ while (Date.now() - begin < within) {
7103
+ last = await view(topic).then((read) => kafkaViewOf(kafka, read), (error) => ({
7104
+ primary: null,
7105
+ healthy: false,
7106
+ why: error.message.slice(0, 200)
7107
+ }));
7108
+ calm = last.healthy ? calm + 1 : 0;
7109
+ if (calm >= 2 && last.primary !== null) return last.primary;
7110
+ await sleep$1(1e3);
7111
+ }
7112
+ throw new Error(`the partition did not converge in ${within} ms: ${last.why}`);
7113
+ },
7114
+ async holds(topic) {
7115
+ const reply = await tool("holds", topic);
7116
+ const { held } = JSON.parse(reply);
7117
+ if (held === void 0) throw new Error(`the read of the partition failed: ${reply}`);
7118
+ return held.map(Number).filter((n) => Number.isInteger(n) && n > 0);
7119
+ },
7120
+ async client(args) {
7121
+ const events = lines();
7122
+ const container = await new GenericContainer(clientImage).withNetworkMode(`container:${holder.getId()}`).withCommand([
7123
+ "java",
7124
+ CLIENT_HEAP,
7125
+ "-cp",
7126
+ CLASS_PATH,
7127
+ "ClientKafka",
7128
+ bootstrap,
7129
+ ...args
7130
+ ]).withLogConsumer(events.consume).start();
7131
+ return {
7132
+ events: events.seen,
7133
+ until: (kinds, within) => events.until(kinds, within, "client"),
7134
+ stop: () => container.stop({ timeout: 2 }).then(() => void 0).catch(() => void 0)
7135
+ };
7136
+ },
7137
+ fault(kind, part, cuts, at) {
7138
+ const cut = kafkaCutOf(kafka, members, controller, part, cuts);
7139
+ return stamped(injectorOf(part), [
7140
+ "/opt/harness/inject",
7141
+ kind,
7142
+ cut.peers.join(","),
7143
+ ...after(at)
7144
+ ]);
7145
+ },
7146
+ lift(kind, part, cuts, at) {
7147
+ const cut = kafkaCutOf(kafka, members, controller, part, cuts);
7148
+ return stamped(injectorOf(part), [
7149
+ "/opt/harness/inject",
7150
+ kind === "rst" ? "clear-rst" : "clear",
7151
+ cut.peers.join(","),
7152
+ ...after(at)
7153
+ ]);
7154
+ },
7155
+ async forget() {
7156
+ await output(admin, [
7157
+ "ip",
7158
+ "tcp_metrics",
7159
+ "flush",
7160
+ "all"
7161
+ ]);
7162
+ },
7163
+ async settle() {
7164
+ const from = records.seen.length;
7165
+ const clock = await new Promise((resolve) => {
7166
+ const begin = Date.now();
7167
+ const look = () => {
7168
+ const line = records.seen.slice(from).find((record) => record.ev === "clock");
7169
+ if (line !== void 0) resolve(line);
7170
+ else if (Date.now() - begin > 5e3) resolve(null);
7171
+ else setTimeout(look, 50);
7172
+ };
7173
+ look();
7174
+ });
7175
+ return clock === null ? null : Number(clock["lost"] ?? 0);
7176
+ },
7177
+ async manifest(sysctls) {
7178
+ const config = await output(admin, ["zcat", "/proc/config.gz"]).catch(() => "");
7179
+ const inspect = async (container) => runtime.container.inspect(runtime.container.getById(container.getId()));
7180
+ const settings = {
7181
+ settings: Object.entries(kafka.settings).map(([property, value]) => `${property}=${value}`).join("; "),
7182
+ heap: kafka.heap,
7183
+ "min.insync.replicas": String(kafka.minInsync)
7184
+ };
7185
+ const list = async (names) => {
7186
+ const out = [];
7187
+ for (const name of names) {
7188
+ const version = lastLine(await output(box(name), ["/opt/kafka/bin/kafka-topics.sh", "--version"]).catch(() => ""));
7189
+ out.push({
7190
+ name,
7191
+ ip: parts[name].ip,
7192
+ id: (await inspect(box(name))).Image,
7193
+ settings: {
7194
+ version,
7195
+ ...settings
7196
+ }
7197
+ });
7198
+ }
7199
+ return out;
7200
+ };
7201
+ const brokers = await list(kafka.brokers);
7202
+ return {
7203
+ kernel: await output(holder, ["uname", "-r"]),
7204
+ architecture: await output(holder, ["uname", "-m"]),
7205
+ hz: /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1] ?? null,
7206
+ boot_id: await output(holder, ["cat", "/proc/sys/kernel/random/boot_id"]),
7207
+ docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
7208
+ target: {
7209
+ image: kafka.image,
7210
+ id: brokers[0].id,
7211
+ settings: brokers[0].settings,
7212
+ sysctls: {}
7213
+ },
7214
+ harness: {
7215
+ image,
7216
+ id: (await inspect(holder)).Image,
7217
+ packages: (await output(holder, [
7218
+ "apk",
7219
+ "info",
7220
+ "-v"
7221
+ ])).split("\n").sort()
7222
+ },
7223
+ client: {
7224
+ kind: "image",
7225
+ image: clientImage
7226
+ },
7227
+ netns: {
7228
+ client: clientNetns,
7229
+ target: members[kafka.brokers[0]].netns
7230
+ },
7231
+ sysctls,
7232
+ group: {
7233
+ members: brokers,
7234
+ sentinels: await list([kafka.controller]),
7235
+ observer: { image: clientImage }
7236
+ }
7237
+ };
7238
+ },
7239
+ close
7240
+ };
7241
+ } catch (error) {
7242
+ await close();
7243
+ throw error;
7244
+ }
7245
+ };
7246
+ /** This count is the number of acknowledged writes that a run waits for before its fault: the stream is steady then. */
7247
+ const STEADY = 20;
7248
+ /**
7249
+ * This time is how long a run continues after its lift. The record then shows the step-down of the
7250
+ * leader and the writes after it. It has no source. It is above the largest step-down of the
7251
+ * law, which is approximately 2 s after the lift.
7252
+ */
7253
+ const KAFKA_TAIL_MS = 1e4;
7254
+ /** This function gives the arguments of the client of a Kafka cell: the topic, the writers, the stream of writes and the settings. */
7255
+ const kafkaArgs = (cell, topic) => [
7256
+ "--topic",
7257
+ topic,
7258
+ "--writers",
7259
+ String(cell.kafka.writers),
7260
+ "--interval-ms",
7261
+ String(cell.probe.interval === void 0 ? 50 : valueIn(cell.probe.interval, "ms")),
7262
+ "--max-s",
7263
+ String(Math.ceil(deadlineOf(cell) / 1e3 + (cell.hold === void 0 ? 0 : valueIn(cell.hold, "s")) + 120)),
7264
+ ...Object.entries(cell.driverOptions ?? {}).flatMap(([name, value]) => ["--set", `${name}=${String(value)}`])
7265
+ ];
7266
+ /** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
7267
+ const waitFor = (client, match, within) => new Promise((resolve) => {
7268
+ const started = Date.now();
7269
+ const look = () => {
7270
+ const found = client.events.find(match);
7271
+ if (found !== void 0 || Date.now() - started > within) resolve(found);
7272
+ else setTimeout(look, 20);
7273
+ };
7274
+ look();
7275
+ });
7276
+ /** This function gives the count of lost records in the latest clock line of the prober. */
7277
+ const droppedSoFar$1 = (records) => {
7278
+ for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
7279
+ return 0;
7280
+ };
7281
+ /**
7282
+ * This function does one run of a Kafka cell:
7283
+ * - Each broker is up, the topic of the run exists, and its partition is whole. The client starts,
7284
+ * and its stream of writes gets steady.
7285
+ * - The fault cuts the leader from the other brokers and the controller, and it lifts at its instant
7286
+ * in the VM. The client keeps its route to the leader, thus only the group knows of the cut.
7287
+ * - The run waits for the lift and for the tail after it.
7288
+ * - Each fault goes, the client stops, and the partition converges again. The admin then reads
7289
+ * the whole partition, thus the record tells which writes the group kept.
7290
+ */
7291
+ const kafkaOnce = async (lab, cell, environment, index) => {
7292
+ if (cell.fault !== "partition") throw new Error(`a Kafka cell has no fault ${cell.fault}`);
7293
+ if (cell.hold === void 0) throw new Error("a Kafka cell needs a hold: its law reads it");
7294
+ const place = cell.faultAt ?? { role: "primary" };
7295
+ if (place.role !== "primary") throw new Error(`a Kafka cell has no fault on the ${place.role}`);
7296
+ const cuts = place.cuts ?? "group";
7297
+ const topic = topicOf(environment, index);
7298
+ await lab.brokersUp();
7299
+ await lab.create(topic);
7300
+ const leader = await lab.converge(topic);
7301
+ await lab.forget();
7302
+ const from = lab.kernel.length;
7303
+ const lostBefore = droppedSoFar$1(lab.kernel);
7304
+ const client = await lab.client(kafkaArgs(cell, topic));
7305
+ let fault = null;
7306
+ let heal = null;
7307
+ const undo = [];
7308
+ try {
7309
+ await client.until(["ready"], 6e4);
7310
+ 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`);
7311
+ fault = await lab.fault("blackhole", leader, cuts);
7312
+ undo.push(() => lab.lift("blackhole", leader, cuts));
7313
+ heal = await lab.lift("blackhole", leader, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
7314
+ undo.length = 0;
7315
+ await sleep$1(KAFKA_TAIL_MS);
7316
+ } finally {
7317
+ for (const step of undo.reverse()) await step().catch(() => void 0);
7318
+ await client.stop();
7319
+ }
7320
+ const settled = await lab.settle();
7321
+ const lost = settled === null ? null : settled - lostBefore;
7322
+ const after = await lab.converge(topic).catch(() => null);
7323
+ const holds = after === null ? null : await lab.holds(topic);
7324
+ return {
7325
+ environment,
7326
+ index,
7327
+ events: [...client.events],
7328
+ fault,
7329
+ ...heal === null ? {} : { heal },
7330
+ kernel: lab.kernel.slice(from),
7331
+ netns: lab.netns.client,
7332
+ targetNetns: lab.members[leader].netns,
7333
+ lost,
7334
+ group: {
7335
+ members: lab.members,
7336
+ sentinels: { [lab.kafka.controller]: lab.controller },
7337
+ primary: leader,
7338
+ at: leader,
7339
+ timeline: [],
7340
+ after,
7341
+ holds
7342
+ }
7343
+ };
7344
+ };
7345
+ //#endregion
7346
+ //#region src/run.ts
7347
+ /**
7348
+ * This function measures and judges the raw records of a cell, with the code of the harness at
7349
+ * this time. `runCell` uses it during a cell. A holder of the raw records of a cell can use it
7350
+ * again later. Thus the records get a new analysis after a change to observe.ts or to the verdict.
7351
+ */
7352
+ const judge = (cell, runs) => {
7353
+ const measured = runs.filter((run) => run.warmup !== true).map((run) => measure(cell, run));
7354
+ return {
7355
+ measured,
7356
+ verdict: verdictOf(cell.prediction, measured, noiseOf(cell))
7357
+ };
7358
+ };
7359
+ /** This function gives the count of lost records in the latest clock line of the prober, since it began. */
7360
+ const droppedSoFar = (records) => {
7361
+ for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
7362
+ return 0;
7363
+ };
7364
+ /** This function gives the argv of the client for a cell, with a deadline far after all that it waits for. */
7365
+ const probeArgs = (cell, lab) => {
7366
+ const { prediction, probe } = cell;
7367
+ const longest = (prediction.detects === true && prediction.driver !== void 0 ? longestOf(cell) : prediction.detects ? latestEndOf(prediction) : valueIn(prediction.window, "s")) + (cell.idleBefore === void 0 ? 0 : valueIn(cell.idleBefore, "s"));
7368
+ const args = [
7369
+ lab.targetIp,
7370
+ String(lab.targetPort),
7371
+ "--mode",
7372
+ probe.mode
7373
+ ];
7374
+ if (probe.ws !== void 0) {
7375
+ const { ws } = probe;
7376
+ const waits = (prediction.detects !== false ? prediction.reconnects?.waits ?? [] : []).reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
7377
+ args.push("--websocket", ws.socket);
7378
+ args.push("--heartbeat", ws.heartbeat === null ? "off" : "on");
7379
+ if (ws.heartbeat !== null) args.push("--heartbeat-interval-ms", String(valueIn(ws.heartbeat.interval, "ms")), "--heartbeat-timeout-ms", String(valueIn(ws.heartbeat.timeout, "ms")), "--heartbeat-misses", String(ws.heartbeat.misses));
7380
+ args.push("--reconnect", ws.reconnect === null ? "off" : "on");
7381
+ if (ws.reconnect !== null) args.push("--reconnect-initial-ms", String(valueIn(ws.reconnect.initialDelay, "ms")), "--reconnect-multiplier", String(ws.reconnect.multiplier), "--reconnect-max-ms", String(valueIn(ws.reconnect.maxDelay, "ms")), "--reconnect-attempts", String(ws.reconnect.attempts), "--reconnect-jitter", String(ws.reconnect.jitter));
6289
7382
  args.push("--request-timeout-ms", String(valueIn(ws.requestTimeout, "ms")));
6290
7383
  args.push("--connection-timeout-ms", String(valueIn(ws.connectionTimeout, "ms")));
6291
7384
  if (ws.hold !== void 0) args.push("--hold-ms", String(valueIn(ws.hold, "ms")));
@@ -6319,6 +7412,7 @@ const probeArgs = (cell, lab) => {
6319
7412
  if (probe.driver?.name === "pg" || probe.driver?.name === "hikari" || probe.driver?.name === "amqp") {
6320
7413
  if (probe.driver.name !== "amqp" && probe.driver.query !== void 0) args.push("--query", probe.driver.query);
6321
7414
  if (probe.driver.name !== "amqp" && probe.driver.inflight !== void 0) args.push("--inflight-query", probe.driver.inflight);
7415
+ if (probe.driver.name === "hikari" && probe.driver.burst !== void 0) args.push("--burst", String(probe.driver.burst.count), "--burst-gap-ms", String(Math.round(valueIn(probe.driver.burst.gap, "ms"))));
6322
7416
  if (cell.before === true) args.push("--before");
6323
7417
  if (cell.driverOptions !== void 0) args.push("--options", JSON.stringify(cell.driverOptions));
6324
7418
  args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
@@ -6480,6 +7574,19 @@ const dials = (probe, count, within) => new Promise((resolve) => {
6480
7574
  };
6481
7575
  look();
6482
7576
  });
7577
+ /** This time is the most that a run waits for the lines of the target after the end of its client. */
7578
+ const LINES_MS = 5e3;
7579
+ /**
7580
+ * This function waits for the lines of a run's counts. The lines of the target come on their own
7581
+ * stream, thus they can come after the last line of the client. A line above a count comes in the
7582
+ * pause after the wait, thus it is in the run that wrote it.
7583
+ */
7584
+ const linesCounted = async (lab, from, counts) => {
7585
+ const started = Date.now();
7586
+ const short = () => counts.some((each) => countOf(lab.server.slice(from), each.match) < each.count);
7587
+ while (short() && Date.now() - started < LINES_MS) await sleep(20);
7588
+ await sleep(250);
7589
+ };
6483
7590
  /** This function gives the netem specification of a cell: its latency, then the netem of its fault when it has one. */
6484
7591
  const netemSpec = (cell, fault) => {
6485
7592
  const parts = [...cell.latency === void 0 ? [] : [`delay ${valueIn(cell.latency, "ms")}ms`], ...fault === void 0 ? [] : [fault]];
@@ -6680,6 +7787,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
6680
7787
  const gap = cell.probe.http?.gap === void 0 ? 0 : valueIn(cell.probe.http.gap, "s");
6681
7788
  const window = valueIn(prediction.detects ? prediction.high : prediction.window, "s");
6682
7789
  await probe.until(HTTP_DONE, (gap + window) * 2e3 + 1e4).catch(() => void 0);
7790
+ if (prediction.detects === null && prediction.counts !== void 0) await linesCounted(lab, serverFrom, prediction.counts);
6683
7791
  } else if (cell.fault === "stall") {
6684
7792
  if (cell.stall === void 0) throw new Error(`${cell.name} says no stall's length`);
6685
7793
  const window = valueIn(prediction.detects ? prediction.high : prediction.window, "s");
@@ -6794,6 +7902,8 @@ const inEnvironment = async (cell, environment, count, say) => {
6794
7902
  if (cell.group !== void 0) return inGroupEnvironment(cell, environment, count, say);
6795
7903
  if (cell.replicaSet !== void 0) return inReplicaSetEnvironment(cell, environment, count, say);
6796
7904
  if (cell.cluster !== void 0) return inClusterEnvironment(cell, environment, count, say);
7905
+ if (cell.kafka !== void 0) return inKafkaEnvironment(cell, environment, count, say);
7906
+ if (cell.pair !== void 0) return inPairEnvironment(cell, environment, count, say);
6797
7907
  const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
6798
7908
  try {
6799
7909
  const settings = await lab.configure(cell.targetSettings ?? {});
@@ -6891,6 +8001,36 @@ const inReplicaSetEnvironment = async (cell, environment, count, say) => {
6891
8001
  }
6892
8002
  };
6893
8003
  /**
8004
+ * This function does the part of the runs of a pair cell in one environment. The warm-up is a dry
8005
+ * run. The run promotes the standby, thus a pair cell has an environment for each run.
8006
+ */
8007
+ const inPairEnvironment = async (cell, environment, count, say) => {
8008
+ const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
8009
+ try {
8010
+ await lab.warm();
8011
+ const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
8012
+ const standby = await openStandby(lab, cell);
8013
+ const warmup = {
8014
+ ...await pairOnce(lab, standby, cell, environment, -1, true),
8015
+ warmup: true
8016
+ };
8017
+ say(line(cell, warmup, measure(cell, warmup)));
8018
+ const runs = [];
8019
+ for (let index = 0; index < count; index += 1) {
8020
+ const run = await pairOnce(lab, standby, cell, environment, index);
8021
+ runs.push(run);
8022
+ say(line(cell, run, measure(cell, run)));
8023
+ }
8024
+ return {
8025
+ manifest,
8026
+ runs,
8027
+ warmup
8028
+ };
8029
+ } finally {
8030
+ await lab.close();
8031
+ }
8032
+ };
8033
+ /**
6894
8034
  * This function does the part of the runs of a cluster cell in one environment. The warm-up is a
6895
8035
  * whole run with its fault, as for a replica set. The queue converges before each run.
6896
8036
  */
@@ -6919,6 +8059,34 @@ const inClusterEnvironment = async (cell, environment, count, say) => {
6919
8059
  }
6920
8060
  };
6921
8061
  /**
8062
+ * This function does the part of the runs of a Kafka cell in one environment. The warm-up is a whole
8063
+ * run with its fault, as for a cluster. Each run makes its own topic.
8064
+ */
8065
+ const inKafkaEnvironment = async (cell, environment, count, say) => {
8066
+ const lab = await openKafkaLab(cell.kafka);
8067
+ try {
8068
+ const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
8069
+ const warmup = {
8070
+ ...await kafkaOnce(lab, cell, environment, -1),
8071
+ warmup: true
8072
+ };
8073
+ say(line(cell, warmup, measure(cell, warmup)));
8074
+ const runs = [];
8075
+ for (let index = 0; index < count; index += 1) {
8076
+ const run = await kafkaOnce(lab, cell, environment, index);
8077
+ runs.push(run);
8078
+ say(line(cell, run, measure(cell, run)));
8079
+ }
8080
+ return {
8081
+ manifest,
8082
+ runs,
8083
+ warmup
8084
+ };
8085
+ } finally {
8086
+ await lab.close();
8087
+ }
8088
+ };
8089
+ /**
6922
8090
  * This function does the runs of a cell. They go across new environments, and each environment records
6923
8091
  * its manifest. Then the verdict judges the runs. A median across the edge of the band extends
6924
8092
  * the cell from 11 runs to 22, and then to 44, each extension in new environments. With a
@@ -6972,6 +8140,6 @@ const runCell = async (cell, say = () => {}) => {
6972
8140
  }
6973
8141
  };
6974
8142
  //#endregion
6975
- export { AWS_FROM, CLIENT_NAMES, CLUSTER_STAGES, CONVERGE_MS, DAEMONS, DROP_HANDLING, FABRIC_CLIENT, FABRIC_DEVICE, FABRIC_TARGET, FRR_IMAGE, GIVES_AWAY, HASH_SCRIPT, INJECT, KEPT, LINGER_MS, LISTING, LOCK_NAMES, LOSS_MARGIN, MEMBER_MARKS, MONITOR_DOWN, NODE_MARKS, NOISE, PINNED, PUMP, PUMP_SLACK, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, REPOSITORY, ROUTERS, SEED_DOCUMENT, SHIPPED_CLIENTS, SHIPPED_NAMES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, amqpClientImage, basesOf, buildOf, buildRecordOf, callOf, cargoVersions, clockOffsetOf, closeOrExit, cloudOf, clusterArgs, clusterConf, clusterCutOf, clusterHostsOf, clusterOnce, clusterStagesOf, clusterViewOf, commitProblems, comparable, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, drillOnce, drillSpan, dropFinding, dropOf, endpointAfter, errorsOf, exitOf, fieldOf, fieldsOf, firstUnanswered, frrConf, goHttpImage, goVersions, groupOnce, groupVerdictOf, grpcClientImage, harnessImage, harnessRecord, harnessVersion, hashOf, highestHeld, hikariClientImage, hzOf, imageOf, imageProblems, inContext, infoOf, initiateScript, isHopPlace, jarVersions, jitterOf, jitterOfLines, judge, lagsOf, laterMove, latestEndOf, leafLacks, leftAfter, locksOf, logNameOf, manifestProblems, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, monotonicAt, monotonicOf, nodeClientImage, nodeCommand, nodeHttpImage, nodeLineOf, nodeNameOf, nodesOnMonotonic, noiseOf, npmVersions, onMonotonic, openClusterLab, openFabric, openGroupLab, openLab, openReplicaSetLab, pairingOf, pairingsOf, peersOf, pgClientImage, pingsOf, plainOf, planOf, programLines, programOf, quantileOf, readCpu, readHost, readOplog, reconnectFinding, reconnectOf, recoveryFindings, recoveryOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, restamped, routeLine, routerImage, routesOf, runCell, seedScript, sentinelConfig, sessionsOf, shippedClientRecord, shippedSource, sliceQuery, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, standaloneCommand, statusViewOf, stealShare, stopScript, subjectClient, subjectProblems, subscriptionsOf, substrateHere, substrateKey, substrateOf, substrateOn, targetImageOf, targetSourceOf, templated, toolJsonOf, topologyOf, toxicBody, unlockedManifests, unpinnedBases, unpoolable, verdictOf, versionsOf, viewOf, writesOf };
8143
+ export { AWS_FROM, CLIENT_NAMES, CLUSTER_STAGES, CONVERGE_MS, DAEMONS, DEBUG, DIRTY, DROP_HANDLING, FABRIC_CLIENT, FABRIC_DEVICE, FABRIC_TARGET, FRR_IMAGE, GIVES_AWAY, HASH_SCRIPT, INJECT, INSERT, KAFKA_TAIL_MS, KEPT, LINGER_MS, LISTING, LOCK_NAMES, LOSS_MARGIN, MEMBER_MARKS, MONITOR_DOWN, NODE_MARKS, NOISE, PINNED, PUMP, PUMP_SLACK, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, REPOSITORY, RESTART, ROUTERS, SEED_DOCUMENT, SHIPPED_CLIENTS, SHIPPED_NAMES, STAGES, STANDBY_READY, STATUS_SCRIPT, STEPS, STEP_DOWN, amqpClientImage, basesOf, bootstrapOf, brokerEnv, buildOf, buildRecordOf, callOf, cancelsOf, cargoVersions, clockOffsetOf, closeOrExit, cloudOf, clusterArgs, clusterConf, clusterCutOf, clusterHostsOf, clusterOnce, clusterStagesOf, clusterViewOf, commitProblems, comparable, controllerEnv, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, drillOnce, drillSpan, dropFinding, dropOf, endpointAfter, envOf, errorsOf, exitOf, fieldOf, fieldsOf, firstUnanswered, frrConf, goHttpImage, goVersions, groupOnce, groupVerdictOf, grpcClientImage, harnessImage, harnessRecord, harnessVersion, hashOf, highestHeld, hikariClientImage, hzOf, imageOf, imageProblems, inContext, infoOf, initiateScript, isHopPlace, jarVersions, jitterOf, jitterOfLines, judge, kafkaArgs, kafkaClientImage, kafkaCutOf, kafkaOnce, kafkaViewOf, lagsOf, laterMove, latestEndOf, leafLacks, leftAfter, locksOf, logNameOf, manifestProblems, matches, measure, measureDriver, measureGroup, measurePorts, measureQueue, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, monotonicAt, monotonicOf, nodeClientImage, nodeCommand, nodeHttpImage, nodeIdOf, nodeLineOf, nodeNameOf, nodesOnMonotonic, noiseOf, npmVersions, numbersOf, onMonotonic, openClusterLab, openFabric, openGroupLab, openKafkaLab, openLab, openReplicaSetLab, openStandby, pairArgs, pairOnce, pairingOf, pairingsOf, peersOf, pgClientImage, pingsOf, plainOf, planOf, programLines, programOf, quantileOf, readCpu, readHost, readOplog, reconnectFinding, reconnectOf, recordProblems, recoveryFindings, recoveryOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, restamped, routeLine, routerImage, routesOf, runCell, seedScript, sentinelConfig, sessionsOf, shippedClientRecord, shippedSource, sliceQuery, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, standaloneCommand, standbyCommand, statusViewOf, stealShare, stopScript, subjectClient, subjectProblems, subscriptionsOf, substrateHere, substrateKey, substrateOf, substrateOn, targetImageOf, targetSourceOf, templated, toolJsonOf, topicOf, topologyOf, toxicBody, unlockedManifests, unpinnedBases, unpoolable, verdictOf, versionsOf, viewOf, writesOf };
6976
8144
 
6977
8145
  //# sourceMappingURL=index.js.map