@mark1russell7/harness 0.11.16 → 0.11.17

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 { C as targetImageOf, E as unpinnedBases, S as subjectProblems, T as unlockedManifests, _ as planOf, a as SHIPPED_NAMES, b as routerImage, c as harnessImage, d as imageProblems, f as inContext, g as pgClientImage, h as nodeHttpImage, i as SHIPPED_CLIENTS, l as hikariClientImage, m as nodeClientImage, n as PINNED, o as SUBJECTS, p as mongoClientImage, r as REPOSITORY, s as goHttpImage, t as CLIENT_NAMES, u as imageOf, v as proceduresImage, w as targetSourceOf, x as subjectClient, y as programOf } from "./image-RM60Paqe.js";
1
+ import { C as subjectProblems, D as unpinnedBases, E as unlockedManifests, S as subjectClient, T as targetSourceOf, _ as pgClientImage, a as SHIPPED_NAMES, b as programOf, c as goHttpImage, d as imageOf, f as imageProblems, g as nodeHttpImage, h as nodeClientImage, i as SHIPPED_CLIENTS, l as harnessImage, m as mongoClientImage, n as PINNED, o as SUBJECTS, p as inContext, r as REPOSITORY, s as amqpClientImage, t as CLIENT_NAMES, u as hikariClientImage, v as planOf, w as targetImageOf, x as routerImage, y as proceduresImage } from "./image-67OW9r9u.js";
2
2
  import { quantity, valueIn } from "@mark1russell7/quantity";
3
3
  import { createHash } from "node:crypto";
4
4
  //#region src/cell.ts
@@ -1031,8 +1031,8 @@ const measureDriver = (cell, run) => {
1031
1031
  /**
1032
1032
  * This module measures a run of a group cell. It gives the first write that a primary accepted
1033
1033
  * after the fault, and the timeline of the group. The Sentinels of a group announce its timeline.
1034
- * The logs of the members of a replica set and its client tell it. The module also gives what
1035
- * became of each write.
1034
+ * The logs of the members of a replica set or of the nodes of a cluster, and its client, tell it. The
1035
+ * module also gives what became of each write.
1036
1036
  */
1037
1037
  /** These are the stages of a timeline under Sentinels, in their sequence after a fault. */
1038
1038
  const STAGES = [
@@ -1152,6 +1152,58 @@ const replicaSetStagesOf = (run, from, sentAfter, lands) => {
1152
1152
  if (accepted !== void 0) out["first-write"] = between$1(from, accepted.t);
1153
1153
  return out;
1154
1154
  };
1155
+ /** These are the stages of the timeline of a cluster of RabbitMQ, in their usual sequence after a fault. */
1156
+ const CLUSTER_STAGES = [
1157
+ "monitor-down",
1158
+ "elected",
1159
+ "followed",
1160
+ "old-leader-steps-down",
1161
+ "first-write"
1162
+ ];
1163
+ /**
1164
+ * These are the log lines of RabbitMQ that mark each stage of a cluster, by their message (ra 3.1.10,
1165
+ * ra_server_proc.erl:871, :1336-1345 and :1974). `at` tells where the line comes from: the node of the
1166
+ * old leader, or another node.
1167
+ */
1168
+ const NODE_MARKS = {
1169
+ "monitor-down": {
1170
+ message: /Leader monitor down/,
1171
+ at: "other"
1172
+ },
1173
+ elected: {
1174
+ message: /: candidate -> leader in term/,
1175
+ at: "other"
1176
+ },
1177
+ followed: {
1178
+ message: /detected a new leader/,
1179
+ at: "other"
1180
+ },
1181
+ "old-leader-steps-down": {
1182
+ message: /: leader -> \w+ in term/,
1183
+ at: "old"
1184
+ }
1185
+ };
1186
+ /**
1187
+ * This function gives the time of each stage of a cluster from an instant, in a run. The stages of
1188
+ * the nodes come from their log lines (`ev` "rabbit"), whose times the lab puts on the monotonic
1189
+ * clock. `first-write` is the first confirm of a publish that the client sent after the commit of
1190
+ * the fault (`sentAfter`), through any node.
1191
+ */
1192
+ const clusterStagesOf = (run, from, sentAfter) => {
1193
+ const out = {};
1194
+ const old = run.group?.primary ?? "";
1195
+ for (const line of run.group?.timeline ?? []) {
1196
+ if (line.ev !== "rabbit" || line.t < from) continue;
1197
+ const member = String(line["member"] ?? "");
1198
+ for (const stage of Object.keys(NODE_MARKS)) {
1199
+ const mark = NODE_MARKS[stage];
1200
+ if (out[stage] === void 0 && mark.at === "old" === (member === old) && mark.message.test(String(line["msg"] ?? ""))) out[stage] = between$1(from, line.t);
1201
+ }
1202
+ }
1203
+ const accepted = acceptedAfter(run, sentAfter, "same");
1204
+ if (accepted !== void 0) out["first-write"] = between$1(from, accepted.t);
1205
+ return out;
1206
+ };
1155
1207
  /** This function gives the first write that a primary accepted, of a write sent after an instant. It is on a new primary for `new`. */
1156
1208
  const acceptedAfter = (run, sentAfter, lands) => {
1157
1209
  const sent = /* @__PURE__ */ new Map();
@@ -1296,9 +1348,10 @@ const measureGroup = (cell, run) => {
1296
1348
  const timeline = run.group?.timeline ?? [];
1297
1349
  const heal = run.heal ?? null;
1298
1350
  const lands = expected?.lands ?? "new";
1299
- const replica = cell.replicaSet !== void 0;
1351
+ const cluster = cell.cluster !== void 0;
1352
+ const replica = cell.replicaSet !== void 0 || cluster;
1300
1353
  const stall = prediction.detects === false && prediction.recovers !== void 0;
1301
- const stages = replica ? replicaSetStagesOf(run, t0, run.fault.t1, lands) : stagesOf(timeline, t0);
1354
+ const stages = cluster ? clusterStagesOf(run, t0, run.fault.t1) : replica ? replicaSetStagesOf(run, t0, run.fault.t1, lands) : stagesOf(timeline, t0);
1302
1355
  const after = lands === "same" && heal !== null && (!replica || stall) ? heal.t0 : t0;
1303
1356
  const sentAt = /* @__PURE__ */ new Map();
1304
1357
  for (const event of events) if (event.ev === "sent") sentAt.set(Number(event["n"]), event.t);
@@ -1306,7 +1359,7 @@ const measureGroup = (cell, run) => {
1306
1359
  if (event.ev !== "ack" || event.t <= after) return false;
1307
1360
  if (replica && (sentAt.get(Number(event["n"])) ?? -Infinity) <= run.fault.t1) return false;
1308
1361
  const member = memberAt(run, event["at"]);
1309
- return member !== null && (lands === "new" ? member !== primary : true);
1362
+ return member !== null && (lands === "new" && !cluster ? member !== primary : true);
1310
1363
  });
1311
1364
  const to = accepted === void 0 ? null : memberAt(run, accepted["at"]);
1312
1365
  if (replica) {
@@ -1317,7 +1370,7 @@ const measureGroup = (cell, run) => {
1317
1370
  from: primary,
1318
1371
  to,
1319
1372
  stages,
1320
- writes: writesOf(run, t0, primary),
1373
+ writes: writesOf(run, t0, cluster ? "" : primary),
1321
1374
  errors: errorsOf(events, t0),
1322
1375
  ...cell.calibrates === "sentinels" ? { lags: lagsOf(run, kernel) } : {}
1323
1376
  };
@@ -1336,8 +1389,9 @@ const measureGroup = (cell, run) => {
1336
1389
  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`;
1337
1390
  }
1338
1391
  }
1392
+ const firstAddress = cluster ? String(events.find((event) => event.ev === "connected")?.["at"] ?? "") : `${run.group?.members[primary]?.ip}:${(cell.group ?? cell.replicaSet)?.port}`;
1339
1393
  const assumed = prediction.detects === null ? void 0 : prediction.rtt;
1340
- const first = kernel.find((record) => record.netns === run.netns && record.dst === `${run.group?.members[primary]?.ip}:${(cell.group ?? cell.replicaSet)?.port}` && record.tcp?.srtt !== void 0);
1394
+ const first = kernel.find((record) => record.netns === run.netns && record.dst === firstAddress && record.tcp?.srtt !== void 0);
1341
1395
  const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
1342
1396
  if (voided === null && assumed !== void 0 && rtt !== null && rtt > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
1343
1397
  if (voided === null && prediction.detects === true && prediction.from === "fault" && (run.fault.t1 - run.fault.t0) / 1e6 > valueIn(INJECT, "ms")) voided = `the commit of the fault took ${Math.round((run.fault.t1 - run.fault.t0) / 1e5) / 10} ms, longer than the noise of the pipeline: its start is not known`;
@@ -1359,7 +1413,7 @@ const measureGroup = (cell, run) => {
1359
1413
  const at = stages[stage];
1360
1414
  if (at !== void 0) findings.push(`${stage} came ${at.value} s after the fault, where it should not`);
1361
1415
  }
1362
- if (accepted !== void 0 && lands === "same" && to !== primary) findings.push(`the client wrote to ${to ?? String(accepted["at"])}, where it should stay on ${primary}`);
1416
+ 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}`);
1363
1417
  for (const error of expected?.errors ?? []) {
1364
1418
  const met = group.errors.find((each) => each.name === error.name);
1365
1419
  const noise = valueIn(noiseOf(cell), "s");
@@ -1614,7 +1668,7 @@ const requestOf = (events) => {
1614
1668
  };
1615
1669
  /** This function reads the socket of one run against its cell, as `measure` does without the second request of an HTTP client. */
1616
1670
  const measureSocket = (cell, run) => {
1617
- if (cell.group !== void 0 || cell.replicaSet !== void 0) {
1671
+ if (cell.group !== void 0 || cell.replicaSet !== void 0 || cell.cluster !== void 0) {
1618
1672
  const read = measureGroup(cell, run);
1619
1673
  return {
1620
1674
  environment: run.environment,
@@ -2983,7 +3037,7 @@ const viewOf = (group, ips, sentinels, members) => {
2983
3037
  why: "healthy"
2984
3038
  };
2985
3039
  };
2986
- const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3040
+ const sleep$3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2987
3041
  /** This time is the longest that a group takes to converge before a run, or after it. */
2988
3042
  const CONVERGE_MS = 24e4;
2989
3043
  /**
@@ -3178,7 +3232,7 @@ const openGroupLab = async (group, options = {}) => {
3178
3232
  }));
3179
3233
  calm = last.healthy ? calm + 1 : 0;
3180
3234
  if (calm >= 2 && last.primary !== null) return last.primary;
3181
- await sleep$2(1e3);
3235
+ await sleep$3(1e3);
3182
3236
  }
3183
3237
  throw new Error(`the group did not converge in ${within} ms: ${last.why}`);
3184
3238
  },
@@ -3256,7 +3310,7 @@ const openGroupLab = async (group, options = {}) => {
3256
3310
  const started = Date.now();
3257
3311
  while (await cli(member, ["PING"]).catch(() => "") !== "PONG") {
3258
3312
  if (Date.now() - started > 3e4) throw new Error(`${member} did not start again`);
3259
- await sleep$2(200);
3313
+ await sleep$3(200);
3260
3314
  }
3261
3315
  },
3262
3316
  failover(coordinated) {
@@ -3374,9 +3428,9 @@ const openGroupLab = async (group, options = {}) => {
3374
3428
  }
3375
3429
  };
3376
3430
  /** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
3377
- const STEADY$1 = 20;
3431
+ const STEADY$2 = 20;
3378
3432
  /** This time is how long a run continues after its outcome, so that the record shows what came next. */
3379
- const TAIL_MS$2 = 2e3;
3433
+ const TAIL_MS$3 = 2e3;
3380
3434
  /** This function gives the address of a member, with the port of the group. */
3381
3435
  const addressOf = (lab, member) => `${lab.members[member].ip}:${lab.group.port}`;
3382
3436
  /** This function gives the latest time that a group cell waits for its outcome, in ms from the fault. */
@@ -3399,7 +3453,7 @@ const clientArgs = (cell, lab) => {
3399
3453
  ];
3400
3454
  };
3401
3455
  /** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
3402
- const waitFor$1 = (client, match, within) => new Promise((resolve) => {
3456
+ const waitFor$2 = (client, match, within) => new Promise((resolve) => {
3403
3457
  const started = Date.now();
3404
3458
  const look = () => {
3405
3459
  const found = client.events.find(match);
@@ -3415,7 +3469,7 @@ const subscriptionsOf = (timeline) => {
3415
3469
  return [...latest.values()];
3416
3470
  };
3417
3471
  /** This function gives the count of lost records in the latest clock line of the prober. */
3418
- const droppedSoFar$2 = (records) => {
3472
+ const droppedSoFar$3 = (records) => {
3419
3473
  for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
3420
3474
  return 0;
3421
3475
  };
@@ -3437,7 +3491,7 @@ const groupOnce = async (lab, cell, environment, index) => {
3437
3491
  await lab.forget();
3438
3492
  const from = lab.kernel.length;
3439
3493
  const timelineFrom = lab.timeline.length;
3440
- const lostBefore = droppedSoFar$2(lab.kernel);
3494
+ const lostBefore = droppedSoFar$3(lab.kernel);
3441
3495
  const client = await lab.client(clientArgs(cell, lab));
3442
3496
  const lands = cell.prediction.group?.lands ?? "new";
3443
3497
  const place = cell.faultAt ?? { role: "primary" };
@@ -3447,7 +3501,7 @@ const groupOnce = async (lab, cell, environment, index) => {
3447
3501
  const undo = [];
3448
3502
  try {
3449
3503
  await client.until(["ready"], 6e4);
3450
- if (await waitFor$1(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY$1, 3e4) === void 0) throw new Error(`the client did not write ${STEADY$1} times in 30 s`);
3504
+ 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`);
3451
3505
  let healing = null;
3452
3506
  switch (cell.fault) {
3453
3507
  case "blackhole":
@@ -3461,7 +3515,7 @@ const groupOnce = async (lab, cell, environment, index) => {
3461
3515
  undo.push(() => lab.lift(kind, part, cuts));
3462
3516
  if (cell.stall !== void 0) {
3463
3517
  const t1 = fault.t1;
3464
- const sent = await waitFor$1(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
3518
+ const sent = await waitFor$2(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
3465
3519
  if (sent !== void 0) healing = lab.lift(kind, part, cuts, sent.t + valueIn(cell.stall, "s") * 1e9);
3466
3520
  } else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
3467
3521
  break;
@@ -3495,11 +3549,11 @@ const groupOnce = async (lab, cell, environment, index) => {
3495
3549
  const t0 = fault.t0;
3496
3550
  const old = addressOf(lab, primary);
3497
3551
  const deadline = deadlineOf(cell);
3498
- if ((lands === "new" ? await waitFor$1(client, (event) => event.ev === "ack" && event.t > t0 && typeof event["at"] === "string" && event["at"] !== old, deadline) : await (async () => {
3552
+ if ((lands === "new" ? await waitFor$2(client, (event) => event.ev === "ack" && event.t > t0 && typeof event["at"] === "string" && event["at"] !== old, deadline) : await (async () => {
3499
3553
  await healed;
3500
3554
  const after = heal?.t0 ?? t0;
3501
- return waitFor$1(client, (event) => event.ev === "ack" && event.t > after, deadline);
3502
- })()) !== void 0) await sleep$2(TAIL_MS$2);
3555
+ return waitFor$2(client, (event) => event.ev === "ack" && event.t > after, deadline);
3556
+ })()) !== void 0) await sleep$3(TAIL_MS$3);
3503
3557
  await healed;
3504
3558
  } finally {
3505
3559
  for (const step of undo.reverse()) await step().catch(() => void 0);
@@ -3662,7 +3716,7 @@ const statusViewOf = (set, ips, status) => {
3662
3716
  why: "healthy"
3663
3717
  };
3664
3718
  };
3665
- const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3719
+ const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3666
3720
  /** These are the arguments of replSetStepDown: the time that the old primary cannot stand again, and the wait for a secondary to catch up. */
3667
3721
  const STEP_DOWN = {
3668
3722
  seconds: 60,
@@ -3803,7 +3857,7 @@ const openReplicaSetLab = async (set) => {
3803
3857
  calm = last.healthy ? calm + 1 : 0;
3804
3858
  if (last.primary !== null) known = last.primary;
3805
3859
  if (calm >= 2 && last.primary !== null) return last.primary;
3806
- await sleep$1(1e3);
3860
+ await sleep$2(1e3);
3807
3861
  }
3808
3862
  throw new Error(`the set did not converge in ${within} ms: ${last.why}`);
3809
3863
  };
@@ -3909,7 +3963,7 @@ const openReplicaSetLab = async (set) => {
3909
3963
  const started = Date.now();
3910
3964
  while (!(await shell(member, "print(db.hello().ok)").catch(() => "")).includes("1")) {
3911
3965
  if (Date.now() - started > 6e4) throw new Error(`${member} did not start again`);
3912
- await sleep$1(500);
3966
+ await sleep$2(500);
3913
3967
  }
3914
3968
  },
3915
3969
  stepDown(primary) {
@@ -4004,9 +4058,9 @@ const openReplicaSetLab = async (set) => {
4004
4058
  }
4005
4059
  };
4006
4060
  /** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
4007
- const STEADY = 20;
4061
+ const STEADY$1 = 20;
4008
4062
  /** This time is how long a run continues after its outcome, so that the record shows what came next. */
4009
- const TAIL_MS$1 = 2e3;
4063
+ const TAIL_MS$2 = 2e3;
4010
4064
  /** This function gives the arguments of the client of a replica set cell: each member, and the stream of writes. */
4011
4065
  const replicaSetArgs = (cell, members) => {
4012
4066
  const set = cell.replicaSet;
@@ -4030,7 +4084,7 @@ const replicaSetArgs = (cell, members) => {
4030
4084
  ];
4031
4085
  };
4032
4086
  /** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
4033
- const waitFor = (client, match, within) => new Promise((resolve) => {
4087
+ const waitFor$1 = (client, match, within) => new Promise((resolve) => {
4034
4088
  const started = Date.now();
4035
4089
  const look = () => {
4036
4090
  const found = client.events.find(match);
@@ -4040,7 +4094,7 @@ const waitFor = (client, match, within) => new Promise((resolve) => {
4040
4094
  look();
4041
4095
  });
4042
4096
  /** This function gives the count of lost records in the latest clock line of the prober. */
4043
- const droppedSoFar$1 = (records) => {
4097
+ const droppedSoFar$2 = (records) => {
4044
4098
  for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
4045
4099
  return 0;
4046
4100
  };
@@ -4065,7 +4119,7 @@ const replicaSetOnce = async (lab, cell, environment, index) => {
4065
4119
  await lab.forget();
4066
4120
  const from = lab.kernel.length;
4067
4121
  const logFrom = lab.log.length;
4068
- const lostBefore = droppedSoFar$1(lab.kernel);
4122
+ const lostBefore = droppedSoFar$2(lab.kernel);
4069
4123
  const client = await lab.client(replicaSetArgs(cell, lab.members));
4070
4124
  const lands = cell.prediction.group?.lands ?? "new";
4071
4125
  const place = cell.faultAt ?? { role: "primary" };
@@ -4075,7 +4129,7 @@ const replicaSetOnce = async (lab, cell, environment, index) => {
4075
4129
  const undo = [];
4076
4130
  try {
4077
4131
  await client.until(["ready"], 6e4);
4078
- 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`);
4132
+ if (await waitFor$1(client, (event) => event.ev === "ack" && Number(event["n"]) >= STEADY$1, 3e4) === void 0) throw new Error(`the client did not write ${STEADY$1} times in 30 s`);
4079
4133
  let healing = null;
4080
4134
  switch (cell.fault) {
4081
4135
  case "blackhole":
@@ -4089,7 +4143,7 @@ const replicaSetOnce = async (lab, cell, environment, index) => {
4089
4143
  undo.push(() => lab.lift(kind, part, cuts));
4090
4144
  if (cell.stall !== void 0) {
4091
4145
  const t1 = fault.t1;
4092
- const sent = await waitFor(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
4146
+ const sent = await waitFor$1(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
4093
4147
  healing = lab.lift(kind, part, cuts, (sent?.t ?? t1) + valueIn(cell.stall, "s") * 1e9);
4094
4148
  } else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
4095
4149
  break;
@@ -4124,10 +4178,10 @@ const replicaSetOnce = async (lab, cell, environment, index) => {
4124
4178
  const t1 = fault.t1;
4125
4179
  const old = `${lab.members[primary].ip}:${set.port}`;
4126
4180
  const sentAt = (n) => client.events.find((event) => event.ev === "sent" && Number(event["n"]) === n)?.t ?? -Infinity;
4127
- if (await waitFor(client, (event) => event.ev === "ack" && sentAt(Number(event["n"])) > t1 && (lands === "same" || typeof event["at"] === "string" && event["at"] !== old), deadlineOf(cell)) !== void 0) await sleep$1(TAIL_MS$1);
4181
+ 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);
4128
4182
  if (healing !== null) {
4129
4183
  await healed.catch(() => void 0);
4130
- await sleep$1(TAIL_MS$1);
4184
+ await sleep$2(TAIL_MS$2);
4131
4185
  }
4132
4186
  } finally {
4133
4187
  for (const step of undo.reverse()) await step().catch(() => void 0);
@@ -4159,171 +4213,909 @@ const replicaSetOnce = async (lab, cell, environment, index) => {
4159
4213
  };
4160
4214
  };
4161
4215
  //#endregion
4162
- //#region src/run.ts
4216
+ //#region src/cluster.ts
4217
+ /** This form is a line of the console log of RabbitMQ 4.3: the time with its offset, the level, the process and the message. */
4218
+ const LINE = /^(\d{4}-\d\d-\d\d) (\d\d:\d\d:\d\d)\.(\d{3})(\d*)([+-]\d\d:\d\d) \[(\w+)\] <[^>]*> (.*)$/;
4219
+ /** This form is an escape of ANSI: RabbitMQ colors each line of its console by its level. */
4220
+ const ANSI = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*m", "g");
4221
+ /** This function gives a line of the console without its colors. */
4222
+ const plainOf = (text) => text.replace(ANSI, "").trim();
4223
+ /** This text is the name of a quorum queue in the log of Ra: its friendly name. */
4224
+ const logNameOf = (queue) => `queue '${queue}' in vhost '/'`;
4163
4225
  /**
4164
- * This function measures and judges the raw records of a cell, with the code of the harness at
4165
- * this time. `runCell` uses it during a cell. A holder of the raw records of a cell can use it
4166
- * again later. Thus the records get a new analysis after a change to observe.ts or to the verdict.
4226
+ * This function reads one log line of a node, or gives null for a line that a run does not keep. A
4227
+ * run keeps each line of the queue, each warning and each error, and each line of the maintenance
4228
+ * mode of a drain.
4167
4229
  */
4168
- const judge = (cell, runs) => {
4169
- const measured = runs.filter((run) => run.warmup !== true).map((run) => measure(cell, run));
4230
+ const nodeLineOf = (member, text, queue) => {
4231
+ const found = LINE.exec(plainOf(text));
4232
+ if (found === null) return null;
4233
+ const [, day, time, ms, us, offset, level, msg] = found;
4234
+ if (!(msg.includes(logNameOf(queue)) || level === "warning" || level === "error" || msg.includes("maintenance"))) return null;
4235
+ const real = Date.parse(`${day}T${time}.${ms}${offset}`) + (us === "" ? 0 : Number(`0.${us}`));
4236
+ if (!Number.isFinite(real)) return null;
4170
4237
  return {
4171
- measured,
4172
- verdict: verdictOf(cell.prediction, measured, noiseOf(cell))
4238
+ ev: "rabbit",
4239
+ t: 0,
4240
+ member,
4241
+ real,
4242
+ level,
4243
+ msg
4173
4244
  };
4174
4245
  };
4175
- /** This function gives the count of lost records in the latest clock line of the prober, since it began. */
4176
- const droppedSoFar = (records) => {
4177
- for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
4178
- return 0;
4179
- };
4180
- /** This function gives the argv of the client for a cell, with a deadline far after all that it waits for. */
4181
- const probeArgs = (cell, lab) => {
4182
- const { prediction, probe } = cell;
4183
- 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"));
4184
- const args = [
4185
- lab.targetIp,
4186
- String(lab.targetPort),
4187
- "--mode",
4188
- probe.mode
4189
- ];
4190
- if (probe.ws !== void 0) {
4191
- const { ws } = probe;
4192
- const waits = (prediction.detects !== false ? prediction.reconnects?.waits ?? [] : []).reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
4193
- args.push("--websocket", ws.socket);
4194
- args.push("--heartbeat", ws.heartbeat === null ? "off" : "on");
4195
- 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));
4196
- args.push("--reconnect", ws.reconnect === null ? "off" : "on");
4197
- 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));
4198
- args.push("--request-timeout-ms", String(valueIn(ws.requestTimeout, "ms")));
4199
- args.push("--connection-timeout-ms", String(valueIn(ws.connectionTimeout, "ms")));
4200
- if (ws.hold !== void 0) args.push("--hold-ms", String(valueIn(ws.hold, "ms")));
4201
- args.push("--max-s", String(Math.ceil((longest + waits) * 2 + 10)));
4202
- return args;
4203
- }
4204
- if (probe.http !== void 0) {
4205
- const { http } = probe;
4206
- const gap = http.gap === void 0 ? 0 : valueIn(http.gap, "s");
4207
- args.push("--method", http.method);
4208
- if (http.gap !== void 0) args.push("--gap-ms", String(valueIn(http.gap, "ms")));
4209
- if (http.hold !== void 0) args.push("--hold-ms", String(valueIn(http.hold, "ms")));
4210
- if (http.bytes !== void 0) args.push("--bytes", String(http.bytes));
4211
- for (const [name, value] of Object.entries(http.options ?? {})) args.push(`--${name}`, String(value));
4212
- args.push("--linger-ms", String(LINGER_MS));
4213
- args.push("--max-s", String(Math.ceil(longest * 2 + gap + 10)));
4214
- return args;
4215
- }
4216
- if (probe.request !== void 0) args.push("--request", probe.request);
4217
- if (probe.count !== void 0) args.push("--count", String(probe.count));
4218
- if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
4219
- if (probe.driver?.name === "mongodb") {
4220
- args.push("--set", probe.driver.set);
4221
- if (probe.driver.op !== void 0) args.push("--op", probe.driver.op);
4222
- if (cell.writeConcern !== void 0) args.push("--w", String(cell.writeConcern));
4223
- if (cell.before === true) args.push("--before");
4224
- if (cell.driverOptions !== void 0) args.push("--options", JSON.stringify(cell.driverOptions));
4225
- args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
4226
- return args;
4227
- }
4228
- if (probe.driver?.name === "pg" || probe.driver?.name === "hikari" || probe.driver?.name === "amqp") {
4229
- if (probe.driver.name !== "amqp" && probe.driver.query !== void 0) args.push("--query", probe.driver.query);
4230
- if (probe.driver.name !== "amqp" && probe.driver.inflight !== void 0) args.push("--inflight-query", probe.driver.inflight);
4231
- if (cell.before === true) args.push("--before");
4232
- if (cell.driverOptions !== void 0) args.push("--options", JSON.stringify(cell.driverOptions));
4233
- args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
4234
- return args;
4235
- }
4236
- if (probe.driver !== void 0) {
4237
- args.push("--keepalive-ms", String(valueIn(probe.driver.keepAlive, "ms")));
4238
- args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
4239
- return args;
4240
- }
4241
- if (probe.keepalive) args.push("--keepalive");
4242
- if (probe.userTimeout !== void 0) args.push("--user-timeout-ms", String(valueIn(probe.userTimeout, "ms")));
4243
- args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
4244
- return args;
4245
- };
4246
+ /** This function puts the log lines of the nodes on the monotonic clock, with the offset of a client. */
4247
+ const nodesOnMonotonic = (log, offset) => log.map((line) => ({
4248
+ ...line,
4249
+ t: offset === null ? 0 : Math.round(line.real * 1e6 - offset)
4250
+ }));
4246
4251
  /**
4247
- * This function waits for an error on the socket of the observer, or for its end of file
4248
- * (CLOSE_WAIT, state 8). The prober records both. A cell whose observer is the server, or a proxy
4249
- * of a topology, waits for that. A target with its own detector can also drop the connection
4250
- * itself (`dropped`). The promise resolves after `within` ms at the latest.
4252
+ * This function reads the JSON output of a tool of RabbitMQ (`--formatter json`). The tools print a
4253
+ * value over one line or over more than one, thus it reads from the first bracket to the end.
4251
4254
  */
4252
- const observerError = (cell, partial, within, dropped = () => false) => new Promise((resolve) => {
4253
- const started = Date.now();
4254
- const look = () => {
4255
- const run = partial();
4256
- const at = socketOf(cell, run);
4257
- if (at !== void 0 && run.kernel.some((record) => record.netns === at.netns && record.cookie === at.cookie && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && record.new === 8)) || dropped() || Date.now() - started > within) resolve();
4258
- else setTimeout(look, 50);
4259
- };
4260
- look();
4261
- });
4262
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4255
+ const toolJsonOf = (text) => {
4256
+ const at = text.search(/[[{]/);
4257
+ if (at === -1) throw new Error(`no JSON in the output: ${text.slice(0, 200)}`);
4258
+ return JSON.parse(text.slice(at));
4259
+ };
4260
+ /** This function gives the name of a node in the Erlang distribution: `rabbit@<name>`. */
4261
+ const nodeNameOf = (member) => `rabbit@${member}`;
4263
4262
  /**
4264
- * This time is the pause between the client's first reply and the fault. It is more than Linux's
4265
- * TCP_DELACK_MAX of 200 ms. Thus the delayed ACK of the reply is out before the fault.
4263
+ * This function gives the file of rabbitmq.conf of each node. The nodes form the cluster from the list
4264
+ * of their names, and the user guest connects from each address. The lines of the cluster come last.
4266
4265
  */
4267
- const QUIET_MS = 250;
4266
+ const clusterConf = (cluster) => [
4267
+ "cluster_formation.peer_discovery_backend = classic_config",
4268
+ ...cluster.members.map((name, at) => `cluster_formation.classic_config.nodes.${at + 1} = ${nodeNameOf(name)}`),
4269
+ "loopback_users.guest = false",
4270
+ ...cluster.conf,
4271
+ ""
4272
+ ].join("\n");
4268
4273
  /**
4269
- * This time is how long an HTTP client waits after its second request, before it exits. The exit
4270
- * closes its sockets, thus that close comes after the window of the cell.
4274
+ * This function gives the command of each node. It writes the cookie of the server and of the tools.
4275
+ * Then it starts the server as the child of a shell. A fin can then stop the server, and the container
4276
+ * stays up.
4271
4277
  */
4272
- const LINGER_MS = 1e3;
4273
- /** These are the lines that end the workload of an HTTP client: the end of its second request, its deadline, or its failure. */
4274
- const HTTP_DONE = [
4275
- "done",
4276
- "deadline",
4277
- "fail"
4278
- ];
4278
+ const nodeCommand = (cluster) => [
4279
+ "for f in /var/lib/rabbitmq/.erlang.cookie /root/.erlang.cookie; do cp /tmp/cookie $f; chmod 400 $f; done",
4280
+ "chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie",
4281
+ `${cluster.programs.start} & while :; do sleep 3600; done`
4282
+ ].join("; ");
4279
4283
  /**
4280
- * This function waits for the request that an in-flight client sends on the signal: its second
4281
- * send, after the PING that made it ready. Its stamp is when the request first met the fault.
4284
+ * This function judges the status of the queue: its leader, and if the queue is healthy. A healthy
4285
+ * queue has one leader and a follower on each other node. Each follower is in the term of the leader,
4286
+ * at the commit index of the leader.
4282
4287
  */
4283
- const requestSent = (probe, within) => new Promise((resolve, reject) => {
4284
- const started = Date.now();
4285
- const look = () => {
4286
- const sent = probe.events.filter((event) => event.ev === "sent")[1];
4287
- if (sent !== void 0) resolve(sent);
4288
- else if (Date.now() - started > within) reject(/* @__PURE__ */ new Error(`the client sent no request within ${within} ms of being told to`));
4289
- else setTimeout(look, 1);
4288
+ const clusterViewOf = (cluster, rows) => {
4289
+ const byNode = new Map(cluster.members.map((name) => [nodeNameOf(name), name]));
4290
+ const leaders = rows.filter((row) => row["Raft State"] === "leader");
4291
+ if (leaders.length !== 1) return {
4292
+ primary: null,
4293
+ healthy: false,
4294
+ why: `the queue has ${leaders.length} leaders`
4290
4295
  };
4291
- look();
4292
- });
4293
- /** This time is how long a run of a driver cell continues after its outcome, so that the record shows what came next. */
4294
- const TAIL_MS = 1e3;
4295
- /** This function waits for the outcome of a run of a driver cell, or for `within` ms. The outcome is the end of its band and of each later band. */
4296
- const driverOutcome = async (cell, current, within) => {
4297
- const started = Date.now();
4298
- while (Date.now() - started < within) {
4299
- if (settled(cell, current())) return;
4300
- await sleep(250);
4296
+ const leader = leaders[0];
4297
+ const primary = byNode.get(leader["Node Name"]) ?? null;
4298
+ if (primary === null) return {
4299
+ primary: null,
4300
+ healthy: false,
4301
+ why: `no node has the name ${leader["Node Name"]}`
4302
+ };
4303
+ if (rows.length !== cluster.members.length) return {
4304
+ primary,
4305
+ healthy: false,
4306
+ why: `the queue has ${rows.length} members`
4307
+ };
4308
+ for (const row of rows) {
4309
+ if (row !== leader && row["Raft State"] !== "follower") return {
4310
+ primary,
4311
+ healthy: false,
4312
+ why: `${row["Node Name"]} is ${row["Raft State"]}`
4313
+ };
4314
+ if (row.Term !== leader.Term) return {
4315
+ primary,
4316
+ healthy: false,
4317
+ why: `${row["Node Name"]} is in term ${row.Term}`
4318
+ };
4319
+ if (row["Commit Index"] !== leader["Commit Index"]) return {
4320
+ primary,
4321
+ healthy: false,
4322
+ why: `${row["Node Name"]} is behind the leader`
4323
+ };
4301
4324
  }
4325
+ return {
4326
+ primary,
4327
+ healthy: true,
4328
+ why: "healthy"
4329
+ };
4302
4330
  };
4303
- /** This function gives the longest wait of a run of a driver cell for its outcome, in ms. */
4304
- const driverWithin = (cell) => (longestOf(cell) * 1.2 + 10) * 1e3;
4305
4331
  /**
4306
- * This function does one run of a cell whose fault holds before the client starts (`Cell.before`).
4307
- * The fault is a network fault at its place. The client then starts into it, and the run waits for
4308
- * each band of the driver. Then the fault lifts.
4332
+ * This function gives the peers that a fault on a node cuts it from. `all` is each other node and the
4333
+ * client, `group` is the other nodes, and `clients` is the client. `one` is the first other node in
4334
+ * the sequence of the cluster, and `one-way` is the same node for the segments from the part only.
4309
4335
  */
4310
- const beforeOnce = async (lab, cell, environment, index) => {
4311
- if (cell.fault !== "rst" && cell.fault !== "blackhole" && cell.fault !== "unreachable") throw new Error(`${cell.name}: a fault before the client starts is a network fault, not ${cell.fault}`);
4312
- await lab.forget();
4313
- const from = lab.kernel.length;
4314
- const middleboxFrom = lab.middlebox.length;
4315
- const lostBefore = droppedSoFar(lab.kernel);
4316
- const faultAt = cell.faultAt ?? "target";
4317
- const kind = cell.fault === "unreachable" ? "reject" : cell.fault;
4318
- const options = cell.fault === "unreachable" ? { code: cell.icmp ?? "host" } : {};
4319
- const fault = await lab.fault(kind, faultAt, options);
4320
- let probe;
4336
+ const clusterCutOf = (cluster, members, client, part, cuts) => {
4337
+ const others = cluster.members.filter((name) => name !== part).map((name) => members[name].ip);
4338
+ if (cuts === "clients") return { peers: [client] };
4339
+ if (cuts === "group") return { peers: others };
4340
+ if (cuts === "one") return { peers: others.slice(0, 1) };
4341
+ if (cuts === "one-way") return {
4342
+ peers: others.slice(0, 1),
4343
+ direction: "out"
4344
+ };
4345
+ return { peers: [...others, client] };
4346
+ };
4347
+ const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4348
+ /**
4349
+ * This function opens one environment of a cluster cell:
4350
+ * - one new network, with each node, the client and the admin on it.
4351
+ * - each node: the stock image, its server as the child of a shell, with its hostname as the name of
4352
+ * the node. A log consumer reads its console. The nodes form the cluster from the list of their
4353
+ * names, and the lab waits until each node runs in it.
4354
+ * - the admin declares the quorum queue on the first node, thus its member there is the first leader.
4355
+ * - an injector sidecar for each node, in its network and process namespaces.
4356
+ * - a client that holds its own network namespace, with a privileged sidecar for its sysctls.
4357
+ * - the prober, which watches the namespace of the client.
4358
+ */
4359
+ const openClusterLab = async (cluster) => {
4360
+ const image = await harnessImage();
4361
+ const amqp = await amqpClientImage();
4362
+ const { GenericContainer, Network, Wait, getContainerRuntimeClient } = await containers();
4363
+ const runtime = await getContainerRuntimeClient();
4364
+ const opened = [];
4365
+ let network;
4366
+ const close = async () => {
4367
+ for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
4368
+ await network?.stop().catch(() => void 0);
4369
+ };
4370
+ const ipOf = async (container) => {
4371
+ const ip = (await runtime.container.inspect(runtime.container.getById(container.getId()))).NetworkSettings.Networks[network.getName()]?.IPAddress;
4372
+ if (ip === void 0 || ip === "") throw new Error(`${container.getName()} has no address on ${network.getName()}`);
4373
+ return ip;
4374
+ };
4321
4375
  try {
4322
- probe = await lab.probe(probeArgs(cell, lab));
4323
- const started = probe;
4324
- const current = () => ({
4325
- environment,
4326
- index,
4376
+ network = await new Network().start();
4377
+ const boxes = /* @__PURE__ */ new Map();
4378
+ const injectors = /* @__PURE__ */ new Map();
4379
+ const members = {};
4380
+ const log = [];
4381
+ const raw = {};
4382
+ for (const name of cluster.members) {
4383
+ let partial = "";
4384
+ const box = await new GenericContainer(cluster.image).withNetwork(network).withNetworkAliases(name).withHostname(name).withCopyContentToContainer([{
4385
+ content: clusterConf(cluster),
4386
+ target: "/etc/rabbitmq/conf.d/90-harness.conf"
4387
+ }, {
4388
+ content: cluster.cookie,
4389
+ target: "/tmp/cookie"
4390
+ }]).withCommand([
4391
+ "sh",
4392
+ "-c",
4393
+ nodeCommand(cluster)
4394
+ ]).withWaitStrategy(Wait.forLogMessage(cluster.ready)).withStartupTimeout(18e4).withLogConsumer((stream) => stream.on("data", (chunk) => {
4395
+ partial += chunk.toString();
4396
+ const split = partial.split("\n");
4397
+ partial = split.pop() ?? "";
4398
+ for (const text of split) {
4399
+ const tail = raw[name] ??= [];
4400
+ tail.push(text);
4401
+ if (tail.length > 400) tail.shift();
4402
+ const line = nodeLineOf(name, text.trim(), cluster.queue);
4403
+ if (line !== null) log.push(line);
4404
+ }
4405
+ })).start();
4406
+ opened.push(box);
4407
+ boxes.set(name, box);
4408
+ const injector = await withPid(new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${box.getId()}`).start();
4409
+ opened.push(injector);
4410
+ injectors.set(name, injector);
4411
+ members[name] = {
4412
+ ip: await ipOf(box),
4413
+ netns: await netnsOf(injector)
4414
+ };
4415
+ }
4416
+ const holder = await new GenericContainer(image).withNetwork(network).start();
4417
+ opened.push(holder);
4418
+ const admin = await new GenericContainer(image).withNetworkMode(`container:${holder.getId()}`).withPrivilegedMode().start();
4419
+ opened.push(admin);
4420
+ const clientIp = await ipOf(holder);
4421
+ const commander = await new GenericContainer(amqp).withNetwork(network).withCommand([
4422
+ "sh",
4423
+ "-c",
4424
+ "while :; do sleep 3600; done"
4425
+ ]).start();
4426
+ opened.push(commander);
4427
+ const clientNetns = await netnsOf(admin);
4428
+ const records = lines();
4429
+ const prober = await new GenericContainer(image).withPrivilegedMode().withNetworkMode("none").withCommand(["/opt/harness/prober", String(clientNetns)]).withLogConsumer(records.consume).start();
4430
+ opened.push(prober);
4431
+ const begun = await records.until(["begin", "fail"], 3e4, "prober");
4432
+ if (begun.ev === "fail") throw new Error(`the prober failed: ${String(begun["why"])}`);
4433
+ const box = (name) => {
4434
+ const found = boxes.get(name);
4435
+ if (found === void 0) throw new Error(`the cluster has no node ${name}`);
4436
+ return found;
4437
+ };
4438
+ const injectorOf = (name) => {
4439
+ const found = injectors.get(name);
4440
+ if (found === void 0) throw new Error(`the cluster has no node ${name}`);
4441
+ return found;
4442
+ };
4443
+ const lastLine = (text) => text.split("\n").filter((line) => line.trim() !== "").at(-1) ?? "";
4444
+ const stamped = async (where, argv) => {
4445
+ const stamp = JSON.parse(lastLine(await output(where, argv)));
4446
+ return {
4447
+ t0: stamp.t0,
4448
+ t1: stamp.t1
4449
+ };
4450
+ };
4451
+ const after = (at) => at === void 0 ? [] : ["--at", String(Math.round(at))];
4452
+ /** This function runs a command of the tools of RabbitMQ on a node, between two stamps of its wall clock. */
4453
+ const walled = async (name, command) => {
4454
+ const text = await output(box(name), [
4455
+ "sh",
4456
+ "-c",
4457
+ `date +%s%N; ${command} >&2; date +%s%N`
4458
+ ]);
4459
+ const stamps = text.split("\n").map((line) => line.trim()).filter((line) => /^\d{18,}$/.test(line)).map(Number);
4460
+ if (stamps.length < 2) throw new Error(`${name}: no stamps around ${command}: ${text.slice(0, 300)}`);
4461
+ return {
4462
+ wall0: stamps[0],
4463
+ wall1: stamps.at(-1)
4464
+ };
4465
+ };
4466
+ const status = async (name) => {
4467
+ const text = await output(box(name), [
4468
+ "rabbitmq-queues",
4469
+ "-q",
4470
+ "quorum_status",
4471
+ cluster.queue,
4472
+ "--formatter",
4473
+ "json"
4474
+ ]);
4475
+ return clusterViewOf(cluster, toolJsonOf(text));
4476
+ };
4477
+ let known = cluster.members[0];
4478
+ /** This function gives the view of the queue: from the node that had the leader last, then from each other node. */
4479
+ const view = async () => {
4480
+ let last = {
4481
+ primary: null,
4482
+ healthy: false,
4483
+ why: "no node answered"
4484
+ };
4485
+ for (const name of [known, ...cluster.members.filter((each) => each !== known)]) {
4486
+ const read = await status(name).catch((error) => ({
4487
+ primary: null,
4488
+ healthy: false,
4489
+ why: `${name}: ${error.message.slice(0, 200)}`
4490
+ }));
4491
+ if (read.primary !== null) return read;
4492
+ last = read;
4493
+ }
4494
+ return last;
4495
+ };
4496
+ const converge = async (within = CONVERGE_MS) => {
4497
+ const started = Date.now();
4498
+ let calm = 0;
4499
+ let last = {
4500
+ primary: null,
4501
+ healthy: false,
4502
+ why: "no view yet"
4503
+ };
4504
+ while (Date.now() - started < within) {
4505
+ last = await view();
4506
+ calm = last.healthy ? calm + 1 : 0;
4507
+ if (last.primary !== null) known = last.primary;
4508
+ if (calm >= 2 && last.primary !== null) return last.primary;
4509
+ await sleep$1(1e3);
4510
+ }
4511
+ throw new Error(`the queue did not converge in ${within} ms: ${last.why}`);
4512
+ };
4513
+ const started = Date.now();
4514
+ for (;;) {
4515
+ const text = await output(box(cluster.members[0]), [
4516
+ "rabbitmq-diagnostics",
4517
+ "-q",
4518
+ "cluster_status",
4519
+ "--formatter",
4520
+ "json"
4521
+ ]).catch(() => "{}");
4522
+ const running = (text.trim() === "" ? {} : toolJsonOf(text)).running_nodes ?? [];
4523
+ if (cluster.members.every((name) => running.includes(nodeNameOf(name)))) break;
4524
+ if (Date.now() - started > 12e4) throw new Error(`the cluster did not form in 120 s: ${running.join(", ")} run`);
4525
+ await sleep$1(1e3);
4526
+ }
4527
+ const declared = lastLine(await output(commander, [
4528
+ "node",
4529
+ "/opt/harness/amqp/admin-cluster.mjs",
4530
+ `${members[cluster.members[0]].ip}:${cluster.port}`,
4531
+ "declare",
4532
+ cluster.queue
4533
+ ]));
4534
+ if (!declared.includes("\"ok\":true")) throw new Error(`the declare of the queue failed: ${declared}`);
4535
+ await converge();
4536
+ return {
4537
+ cluster,
4538
+ clientIp,
4539
+ members,
4540
+ netns: { client: clientNetns },
4541
+ kernel: records.seen,
4542
+ log,
4543
+ raw,
4544
+ async sysctl(values) {
4545
+ const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
4546
+ if (settings.length > 0) await output(admin, [
4547
+ "sysctl",
4548
+ "-q",
4549
+ "-w",
4550
+ ...settings
4551
+ ]);
4552
+ const read = {};
4553
+ for (const key of Object.keys(values)) read[key] = await output(admin, [
4554
+ "sysctl",
4555
+ "-n",
4556
+ key
4557
+ ]);
4558
+ return read;
4559
+ },
4560
+ converge,
4561
+ async flush(leader) {
4562
+ await output(box(leader), [
4563
+ "rabbitmqctl",
4564
+ "-q",
4565
+ "purge_queue",
4566
+ cluster.queue
4567
+ ]);
4568
+ },
4569
+ async holds(leader) {
4570
+ const reply = lastLine(await output(commander, [
4571
+ "node",
4572
+ "/opt/harness/amqp/admin-cluster.mjs",
4573
+ `${members[leader].ip}:${cluster.port}`,
4574
+ "holds",
4575
+ cluster.queue
4576
+ ]));
4577
+ const held = JSON.parse(reply).held;
4578
+ if (held === void 0) throw new Error(`the read of the queue failed: ${reply}`);
4579
+ return held.map(Number).filter((n) => Number.isInteger(n) && n > 0);
4580
+ },
4581
+ partOf(role, leader) {
4582
+ if (role === "primary") return leader;
4583
+ if (role === "sentinel") throw new Error("a cluster has no Sentinel");
4584
+ const follower = cluster.members.find((name) => name !== leader);
4585
+ if (follower === void 0) throw new Error("the cluster has no follower");
4586
+ return follower;
4587
+ },
4588
+ async client(args) {
4589
+ const events = lines();
4590
+ const container = await new GenericContainer(amqp).withNetworkMode(`container:${holder.getId()}`).withCommand([
4591
+ "node",
4592
+ "/opt/harness/amqp/client-cluster.mjs",
4593
+ ...args
4594
+ ]).withLogConsumer(events.consume).start();
4595
+ return {
4596
+ events: events.seen,
4597
+ until: (kinds, within) => events.until(kinds, within, "client"),
4598
+ stop: () => container.stop({ timeout: 2 }).then(() => void 0).catch(() => void 0)
4599
+ };
4600
+ },
4601
+ fault(kind, part, cuts, at) {
4602
+ const cut = clusterCutOf(cluster, members, clientIp, part, cuts);
4603
+ const direction = cut.direction === void 0 ? [] : ["--direction", cut.direction];
4604
+ return stamped(injectorOf(part), [
4605
+ "/opt/harness/inject",
4606
+ kind,
4607
+ cut.peers.join(","),
4608
+ ...direction,
4609
+ ...after(at)
4610
+ ]);
4611
+ },
4612
+ lift(kind, part, cuts, at) {
4613
+ const cut = clusterCutOf(cluster, members, clientIp, part, cuts);
4614
+ const direction = cut.direction === void 0 ? [] : ["--direction", cut.direction];
4615
+ return stamped(injectorOf(part), [
4616
+ "/opt/harness/inject",
4617
+ kind === "rst" ? "clear-rst" : "clear",
4618
+ cut.peers.join(","),
4619
+ ...direction,
4620
+ ...after(at)
4621
+ ]);
4622
+ },
4623
+ signal(part, signal, at) {
4624
+ return stamped(injectorOf(part), [
4625
+ "/opt/harness/inject",
4626
+ "signal",
4627
+ cluster.programs.server,
4628
+ signal,
4629
+ ...after(at)
4630
+ ]);
4631
+ },
4632
+ async restart(member) {
4633
+ await output(box(member), [
4634
+ "sh",
4635
+ "-c",
4636
+ `${cluster.programs.start} > /proc/1/fd/1 2>&1 &`
4637
+ ]);
4638
+ const begun = Date.now();
4639
+ while (!await output(box(member), [
4640
+ "rabbitmq-diagnostics",
4641
+ "-q",
4642
+ "check_running"
4643
+ ]).then(() => true).catch(() => false)) {
4644
+ if (Date.now() - begun > 12e4) throw new Error(`${member} did not start again`);
4645
+ await sleep$1(1e3);
4646
+ }
4647
+ },
4648
+ stepDown(leader, to) {
4649
+ return walled(leader, `rabbitmq-queues -q transfer_leadership ${cluster.queue} ${nodeNameOf(to)}`);
4650
+ },
4651
+ drain(member) {
4652
+ return walled(member, "rabbitmq-upgrade -q drain");
4653
+ },
4654
+ async revive(member) {
4655
+ await output(box(member), [
4656
+ "rabbitmq-upgrade",
4657
+ "-q",
4658
+ "revive"
4659
+ ]);
4660
+ },
4661
+ async forget() {
4662
+ await output(admin, [
4663
+ "ip",
4664
+ "tcp_metrics",
4665
+ "flush",
4666
+ "all"
4667
+ ]);
4668
+ },
4669
+ async settle() {
4670
+ const from = records.seen.length;
4671
+ const clock = await new Promise((resolve) => {
4672
+ const begun = Date.now();
4673
+ const look = () => {
4674
+ const line = records.seen.slice(from).find((record) => record.ev === "clock");
4675
+ if (line !== void 0) resolve(line);
4676
+ else if (Date.now() - begun > 5e3) resolve(null);
4677
+ else setTimeout(look, 50);
4678
+ };
4679
+ look();
4680
+ });
4681
+ return clock === null ? null : Number(clock["lost"] ?? 0);
4682
+ },
4683
+ async manifest(sysctls) {
4684
+ const config = await output(admin, ["zcat", "/proc/config.gz"]).catch(() => "");
4685
+ const inspect = async (container) => runtime.container.inspect(runtime.container.getById(container.getId()));
4686
+ const list = [];
4687
+ for (const name of cluster.members) {
4688
+ const version = lastLine(await output(box(name), [
4689
+ "rabbitmqctl",
4690
+ "-q",
4691
+ "version"
4692
+ ]).catch(() => ""));
4693
+ list.push({
4694
+ name,
4695
+ ip: members[name].ip,
4696
+ id: (await inspect(box(name))).Image,
4697
+ settings: {
4698
+ version,
4699
+ conf: cluster.conf.join("; ")
4700
+ }
4701
+ });
4702
+ }
4703
+ return {
4704
+ kernel: await output(holder, ["uname", "-r"]),
4705
+ architecture: await output(holder, ["uname", "-m"]),
4706
+ hz: /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1] ?? null,
4707
+ boot_id: await output(holder, ["cat", "/proc/sys/kernel/random/boot_id"]),
4708
+ docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
4709
+ target: {
4710
+ image: cluster.image,
4711
+ id: list[0].id,
4712
+ settings: list[0].settings,
4713
+ sysctls: {}
4714
+ },
4715
+ harness: {
4716
+ image,
4717
+ id: (await inspect(holder)).Image,
4718
+ packages: (await output(holder, [
4719
+ "apk",
4720
+ "info",
4721
+ "-v"
4722
+ ])).split("\n").sort()
4723
+ },
4724
+ client: {
4725
+ kind: "amqp",
4726
+ image: amqp
4727
+ },
4728
+ netns: {
4729
+ client: clientNetns,
4730
+ target: members[cluster.members[0]].netns
4731
+ },
4732
+ sysctls,
4733
+ group: {
4734
+ members: list,
4735
+ sentinels: [],
4736
+ observer: { image: amqp }
4737
+ }
4738
+ };
4739
+ },
4740
+ close
4741
+ };
4742
+ } catch (error) {
4743
+ await close();
4744
+ throw error;
4745
+ }
4746
+ };
4747
+ /** This count is the number of confirmed publishes that a run waits for before its fault: the stream is steady then. */
4748
+ const STEADY = 20;
4749
+ /** This time is how long a run continues after its outcome, so that the record shows what came next. */
4750
+ const TAIL_MS$1 = 2e3;
4751
+ /**
4752
+ * This function gives the hosts of the client, in the sequence of its connects: its node first, then
4753
+ * each other node. The node of a follower is the last follower in the sequence of the cluster. A fault
4754
+ * on one member (`one`, `one-way`) cuts the first follower, thus not the node of the client.
4755
+ */
4756
+ const clusterHostsOf = (cell, members, leader) => {
4757
+ const cluster = cell.cluster;
4758
+ const first = cell.clientAt === "leader" ? leader : cluster.members.filter((name) => name !== leader).at(-1);
4759
+ return [first, ...cluster.members.filter((name) => name !== first)].map((name) => `${members[name].ip}:${cluster.port}`);
4760
+ };
4761
+ /** This form is the NOTICE of a leader of the queue that starts to give its leadership away: to await_condition, for a transfer. */
4762
+ const GIVES_AWAY = /: leader -> await_condition in term/;
4763
+ /** This form is the INFO of a follower whose monitor of the leader goes down (ra_server_proc.erl:871). A drain stops the leader so. */
4764
+ const MONITOR_DOWN = /Leader monitor down/;
4765
+ /** This function gives the arguments of the client of a cluster cell: its hosts, the queue, and the stream of publishes. */
4766
+ const clusterArgs = (cell, members, leader) => [
4767
+ clusterHostsOf(cell, members, leader).join(","),
4768
+ "--queue",
4769
+ cell.cluster.queue,
4770
+ "--interval-ms",
4771
+ String(cell.probe.interval === void 0 ? 50 : valueIn(cell.probe.interval, "ms")),
4772
+ "--max-s",
4773
+ String(Math.ceil(deadlineOf(cell) / 1e3 + (cell.hold === void 0 ? 0 : valueIn(cell.hold, "s")) + 120)),
4774
+ ...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)]
4775
+ ];
4776
+ /** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
4777
+ const waitFor = (client, match, within) => new Promise((resolve) => {
4778
+ const started = Date.now();
4779
+ const look = () => {
4780
+ const found = client.events.find(match);
4781
+ if (found !== void 0 || Date.now() - started > within) resolve(found);
4782
+ else setTimeout(look, 20);
4783
+ };
4784
+ look();
4785
+ });
4786
+ /** This function gives the count of lost records in the latest clock line of the prober. */
4787
+ const droppedSoFar$1 = (records) => {
4788
+ for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
4789
+ return 0;
4790
+ };
4791
+ /** This function puts a wall stamp on the monotonic clock, with the clock lines of the client. Without one it gives null. */
4792
+ const monotonicOf = (stamp, events) => {
4793
+ const offset = clockOffsetOf(events);
4794
+ return offset === null ? null : {
4795
+ t0: Math.round(stamp.wall0 - offset),
4796
+ t1: Math.round(stamp.wall1 - offset)
4797
+ };
4798
+ };
4799
+ /**
4800
+ * This function gives the instant of a change of the leader that a command of the tools starts, as the
4801
+ * stamps of a fault. The command starts an Erlang VM first, thus its stamps hold more than the change.
4802
+ * A line of the log marks the start of the change. For a step-down it is the NOTICE of the old leader
4803
+ * (ra_server_proc.erl:1336-1345), and for a drain the INFO of a follower. Its time on the monotonic
4804
+ * clock is the stamp. Without that line, the stamps of the command are the stamps.
4805
+ */
4806
+ const stampOf = async (lab, client, logFrom, marks, wall) => {
4807
+ const started = Date.now();
4808
+ while (Date.now() - started < 5e3) {
4809
+ const line = nodesOnMonotonic(lab.log.slice(logFrom), clockOffsetOf(client.events)).find(marks);
4810
+ if (line !== void 0 && line.t > 0) return {
4811
+ stamp: {
4812
+ t0: line.t,
4813
+ t1: line.t
4814
+ },
4815
+ line
4816
+ };
4817
+ await sleep$1(100);
4818
+ }
4819
+ return {
4820
+ stamp: monotonicOf(wall, client.events),
4821
+ line: null
4822
+ };
4823
+ };
4824
+ /**
4825
+ * This function puts the stamp of a fault from a line of the log on the monotonic clock again. It uses
4826
+ * the offset of all the clock lines of the client, as the timeline of the run does. Thus the line of the
4827
+ * stamp is at the stamp. The offset of the clock lines at the fault can be up to 1 ms smaller.
4828
+ */
4829
+ const restamped = (line, events) => {
4830
+ const [on] = nodesOnMonotonic([line], clockOffsetOf(events));
4831
+ return on === void 0 || on.t <= 0 ? null : {
4832
+ t0: on.t,
4833
+ t1: on.t
4834
+ };
4835
+ };
4836
+ /**
4837
+ * This function does one run of a cluster cell:
4838
+ * - The queue converges, and it is empty. The client connects through its node, and its stream of
4839
+ * publishes gets steady.
4840
+ * - The fault occurs on its node, as the leader of this run makes the roles. A fault with a hold
4841
+ * lifts at its instant in the VM. A step-down moves the leadership to a follower, and a drain puts
4842
+ * the node in maintenance mode. The stamp of each of them is a line of the log (`stampOf`).
4843
+ * - The run waits for its outcome: the first confirm of a publish sent after the commit of the fault.
4844
+ * Then it waits for the end of the hold, thus the record shows what the cluster did in the whole hold.
4845
+ * - Each fault goes, the client stops, and the queue converges again. The sequence numbers that it
4846
+ * then holds tell which publishes it kept. The log lines of the nodes go onto the monotonic clock.
4847
+ */
4848
+ const clusterOnce = async (lab, cell, environment, index) => {
4849
+ const leader = await lab.converge();
4850
+ await lab.flush(leader);
4851
+ await lab.forget();
4852
+ const from = lab.kernel.length;
4853
+ const logFrom = lab.log.length;
4854
+ const lostBefore = droppedSoFar$1(lab.kernel);
4855
+ const client = await lab.client(clusterArgs(cell, lab.members, leader));
4856
+ const place = cell.faultAt ?? { role: "primary" };
4857
+ let fault = null;
4858
+ let heal = null;
4859
+ let at = null;
4860
+ let wall = null;
4861
+ let stampLine = null;
4862
+ const undo = [];
4863
+ try {
4864
+ await client.until(["ready"], 6e4);
4865
+ 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`);
4866
+ let healing = null;
4867
+ switch (cell.fault) {
4868
+ case "blackhole":
4869
+ case "partition":
4870
+ case "rst": {
4871
+ const kind = cell.fault === "rst" ? "rst" : "blackhole";
4872
+ const cuts = place.cuts ?? (cell.fault === "partition" ? "group" : "all");
4873
+ const part = lab.partOf(place.role, leader);
4874
+ at = part;
4875
+ fault = await lab.fault(kind, part, cuts);
4876
+ undo.push(() => lab.lift(kind, part, cuts));
4877
+ if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
4878
+ break;
4879
+ }
4880
+ case "freeze-process": {
4881
+ const part = lab.partOf(place.role, leader);
4882
+ at = part;
4883
+ fault = await lab.signal(part, "STOP");
4884
+ undo.push(() => lab.signal(part, "CONT"));
4885
+ if (cell.hold !== void 0) healing = lab.signal(part, "CONT", fault.t0 + valueIn(cell.hold, "s") * 1e9);
4886
+ break;
4887
+ }
4888
+ case "fin": {
4889
+ const part = lab.partOf(place.role, leader);
4890
+ at = part;
4891
+ fault = await lab.signal(part, "KILL");
4892
+ undo.push(() => lab.restart(part));
4893
+ break;
4894
+ }
4895
+ case "step-down":
4896
+ at = leader;
4897
+ wall = await lab.stepDown(leader, lab.partOf("replica", leader));
4898
+ ({stamp: fault, line: stampLine} = await stampOf(lab, client, logFrom, (line) => line.member === leader && GIVES_AWAY.test(line.msg), wall));
4899
+ break;
4900
+ case "drain": {
4901
+ const part = lab.partOf(place.role, leader);
4902
+ at = part;
4903
+ wall = await lab.drain(part);
4904
+ undo.push(() => lab.revive(part));
4905
+ ({stamp: fault, line: stampLine} = await stampOf(lab, client, logFrom, (line) => line.member !== part && MONITOR_DOWN.test(line.msg), wall));
4906
+ break;
4907
+ }
4908
+ default: throw new Error(`a cluster cell has no fault ${cell.fault}`);
4909
+ }
4910
+ if (fault === null) throw new Error("the client wrote no clock line: the stamps of the command have no place on the monotonic clock");
4911
+ const healed = healing === null ? Promise.resolve(null) : healing.then((stamp) => {
4912
+ heal = stamp;
4913
+ undo.length = 0;
4914
+ return stamp;
4915
+ });
4916
+ healed.catch(() => void 0);
4917
+ const t1 = fault.t1;
4918
+ const sentAt = (n) => client.events.find((event) => event.ev === "sent" && Number(event["n"]) === n)?.t ?? -Infinity;
4919
+ if (await waitFor(client, (event) => event.ev === "ack" && sentAt(Number(event["n"])) > t1, deadlineOf(cell)) !== void 0) await sleep$1(TAIL_MS$1);
4920
+ if (healing !== null) {
4921
+ await healed.catch(() => void 0);
4922
+ await sleep$1(TAIL_MS$1);
4923
+ }
4924
+ } finally {
4925
+ for (const step of undo.reverse()) await step().catch(() => void 0);
4926
+ await client.stop();
4927
+ }
4928
+ const settled = await lab.settle();
4929
+ const lost = settled === null ? null : settled - lostBefore;
4930
+ const after = await lab.converge().catch(() => null);
4931
+ const holds = after === null ? null : await lab.holds(after);
4932
+ return {
4933
+ environment,
4934
+ index,
4935
+ events: [...client.events],
4936
+ fault: stampLine === null ? fault : restamped(stampLine, client.events) ?? fault,
4937
+ ...heal === null ? {} : { heal },
4938
+ kernel: lab.kernel.slice(from),
4939
+ netns: lab.netns.client,
4940
+ targetNetns: lab.members[at ?? leader].netns,
4941
+ lost,
4942
+ group: {
4943
+ members: lab.members,
4944
+ sentinels: {},
4945
+ primary: leader,
4946
+ at,
4947
+ timeline: nodesOnMonotonic(lab.log.slice(logFrom), clockOffsetOf(client.events)),
4948
+ after,
4949
+ holds
4950
+ }
4951
+ };
4952
+ };
4953
+ //#endregion
4954
+ //#region src/run.ts
4955
+ /**
4956
+ * This function measures and judges the raw records of a cell, with the code of the harness at
4957
+ * this time. `runCell` uses it during a cell. A holder of the raw records of a cell can use it
4958
+ * again later. Thus the records get a new analysis after a change to observe.ts or to the verdict.
4959
+ */
4960
+ const judge = (cell, runs) => {
4961
+ const measured = runs.filter((run) => run.warmup !== true).map((run) => measure(cell, run));
4962
+ return {
4963
+ measured,
4964
+ verdict: verdictOf(cell.prediction, measured, noiseOf(cell))
4965
+ };
4966
+ };
4967
+ /** This function gives the count of lost records in the latest clock line of the prober, since it began. */
4968
+ const droppedSoFar = (records) => {
4969
+ for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
4970
+ return 0;
4971
+ };
4972
+ /** This function gives the argv of the client for a cell, with a deadline far after all that it waits for. */
4973
+ const probeArgs = (cell, lab) => {
4974
+ const { prediction, probe } = cell;
4975
+ 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"));
4976
+ const args = [
4977
+ lab.targetIp,
4978
+ String(lab.targetPort),
4979
+ "--mode",
4980
+ probe.mode
4981
+ ];
4982
+ if (probe.ws !== void 0) {
4983
+ const { ws } = probe;
4984
+ const waits = (prediction.detects !== false ? prediction.reconnects?.waits ?? [] : []).reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
4985
+ args.push("--websocket", ws.socket);
4986
+ args.push("--heartbeat", ws.heartbeat === null ? "off" : "on");
4987
+ 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));
4988
+ args.push("--reconnect", ws.reconnect === null ? "off" : "on");
4989
+ 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));
4990
+ args.push("--request-timeout-ms", String(valueIn(ws.requestTimeout, "ms")));
4991
+ args.push("--connection-timeout-ms", String(valueIn(ws.connectionTimeout, "ms")));
4992
+ if (ws.hold !== void 0) args.push("--hold-ms", String(valueIn(ws.hold, "ms")));
4993
+ args.push("--max-s", String(Math.ceil((longest + waits) * 2 + 10)));
4994
+ return args;
4995
+ }
4996
+ if (probe.http !== void 0) {
4997
+ const { http } = probe;
4998
+ const gap = http.gap === void 0 ? 0 : valueIn(http.gap, "s");
4999
+ args.push("--method", http.method);
5000
+ if (http.gap !== void 0) args.push("--gap-ms", String(valueIn(http.gap, "ms")));
5001
+ if (http.hold !== void 0) args.push("--hold-ms", String(valueIn(http.hold, "ms")));
5002
+ if (http.bytes !== void 0) args.push("--bytes", String(http.bytes));
5003
+ for (const [name, value] of Object.entries(http.options ?? {})) args.push(`--${name}`, String(value));
5004
+ args.push("--linger-ms", String(LINGER_MS));
5005
+ args.push("--max-s", String(Math.ceil(longest * 2 + gap + 10)));
5006
+ return args;
5007
+ }
5008
+ if (probe.request !== void 0) args.push("--request", probe.request);
5009
+ if (probe.count !== void 0) args.push("--count", String(probe.count));
5010
+ if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
5011
+ if (probe.driver?.name === "mongodb") {
5012
+ args.push("--set", probe.driver.set);
5013
+ if (probe.driver.op !== void 0) args.push("--op", probe.driver.op);
5014
+ if (cell.writeConcern !== void 0) args.push("--w", String(cell.writeConcern));
5015
+ if (cell.before === true) args.push("--before");
5016
+ if (cell.driverOptions !== void 0) args.push("--options", JSON.stringify(cell.driverOptions));
5017
+ args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
5018
+ return args;
5019
+ }
5020
+ if (probe.driver?.name === "pg" || probe.driver?.name === "hikari" || probe.driver?.name === "amqp") {
5021
+ if (probe.driver.name !== "amqp" && probe.driver.query !== void 0) args.push("--query", probe.driver.query);
5022
+ if (probe.driver.name !== "amqp" && probe.driver.inflight !== void 0) args.push("--inflight-query", probe.driver.inflight);
5023
+ if (cell.before === true) args.push("--before");
5024
+ if (cell.driverOptions !== void 0) args.push("--options", JSON.stringify(cell.driverOptions));
5025
+ args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
5026
+ return args;
5027
+ }
5028
+ if (probe.driver !== void 0) {
5029
+ args.push("--keepalive-ms", String(valueIn(probe.driver.keepAlive, "ms")));
5030
+ args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
5031
+ return args;
5032
+ }
5033
+ if (probe.keepalive) args.push("--keepalive");
5034
+ if (probe.userTimeout !== void 0) args.push("--user-timeout-ms", String(valueIn(probe.userTimeout, "ms")));
5035
+ args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
5036
+ return args;
5037
+ };
5038
+ /**
5039
+ * This function waits for an error on the socket of the observer, or for its end of file
5040
+ * (CLOSE_WAIT, state 8). The prober records both. A cell whose observer is the server, or a proxy
5041
+ * of a topology, waits for that. A target with its own detector can also drop the connection
5042
+ * itself (`dropped`). The promise resolves after `within` ms at the latest.
5043
+ */
5044
+ const observerError = (cell, partial, within, dropped = () => false) => new Promise((resolve) => {
5045
+ const started = Date.now();
5046
+ const look = () => {
5047
+ const run = partial();
5048
+ const at = socketOf(cell, run);
5049
+ if (at !== void 0 && run.kernel.some((record) => record.netns === at.netns && record.cookie === at.cookie && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && record.new === 8)) || dropped() || Date.now() - started > within) resolve();
5050
+ else setTimeout(look, 50);
5051
+ };
5052
+ look();
5053
+ });
5054
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5055
+ /**
5056
+ * This time is the pause between the client's first reply and the fault. It is more than Linux's
5057
+ * TCP_DELACK_MAX of 200 ms. Thus the delayed ACK of the reply is out before the fault.
5058
+ */
5059
+ const QUIET_MS = 250;
5060
+ /**
5061
+ * This time is how long an HTTP client waits after its second request, before it exits. The exit
5062
+ * closes its sockets, thus that close comes after the window of the cell.
5063
+ */
5064
+ const LINGER_MS = 1e3;
5065
+ /** These are the lines that end the workload of an HTTP client: the end of its second request, its deadline, or its failure. */
5066
+ const HTTP_DONE = [
5067
+ "done",
5068
+ "deadline",
5069
+ "fail"
5070
+ ];
5071
+ /**
5072
+ * This function waits for the request that an in-flight client sends on the signal: its second
5073
+ * send, after the PING that made it ready. Its stamp is when the request first met the fault.
5074
+ */
5075
+ const requestSent = (probe, within) => new Promise((resolve, reject) => {
5076
+ const started = Date.now();
5077
+ const look = () => {
5078
+ const sent = probe.events.filter((event) => event.ev === "sent")[1];
5079
+ if (sent !== void 0) resolve(sent);
5080
+ else if (Date.now() - started > within) reject(/* @__PURE__ */ new Error(`the client sent no request within ${within} ms of being told to`));
5081
+ else setTimeout(look, 1);
5082
+ };
5083
+ look();
5084
+ });
5085
+ /** This time is how long a run of a driver cell continues after its outcome, so that the record shows what came next. */
5086
+ const TAIL_MS = 1e3;
5087
+ /** This function waits for the outcome of a run of a driver cell, or for `within` ms. The outcome is the end of its band and of each later band. */
5088
+ const driverOutcome = async (cell, current, within) => {
5089
+ const started = Date.now();
5090
+ while (Date.now() - started < within) {
5091
+ if (settled(cell, current())) return;
5092
+ await sleep(250);
5093
+ }
5094
+ };
5095
+ /** This function gives the longest wait of a run of a driver cell for its outcome, in ms. */
5096
+ const driverWithin = (cell) => (longestOf(cell) * 1.2 + 10) * 1e3;
5097
+ /**
5098
+ * This function does one run of a cell whose fault holds before the client starts (`Cell.before`).
5099
+ * The fault is a network fault at its place. The client then starts into it, and the run waits for
5100
+ * each band of the driver. Then the fault lifts.
5101
+ */
5102
+ const beforeOnce = async (lab, cell, environment, index) => {
5103
+ if (cell.fault !== "rst" && cell.fault !== "blackhole" && cell.fault !== "unreachable") throw new Error(`${cell.name}: a fault before the client starts is a network fault, not ${cell.fault}`);
5104
+ await lab.forget();
5105
+ const from = lab.kernel.length;
5106
+ const middleboxFrom = lab.middlebox.length;
5107
+ const lostBefore = droppedSoFar(lab.kernel);
5108
+ const faultAt = cell.faultAt ?? "target";
5109
+ const kind = cell.fault === "unreachable" ? "reject" : cell.fault;
5110
+ const options = cell.fault === "unreachable" ? { code: cell.icmp ?? "host" } : {};
5111
+ const fault = await lab.fault(kind, faultAt, options);
5112
+ let probe;
5113
+ try {
5114
+ probe = await lab.probe(probeArgs(cell, lab));
5115
+ const started = probe;
5116
+ const current = () => ({
5117
+ environment,
5118
+ index,
4327
5119
  events: started.events,
4328
5120
  fault,
4329
5121
  kernel: lab.kernel.slice(from),
@@ -4625,6 +5417,7 @@ const line = (cell, run, read) => `${cell.name}: env ${run.environment} ${run.wa
4625
5417
  const inEnvironment = async (cell, environment, count, say) => {
4626
5418
  if (cell.group !== void 0) return inGroupEnvironment(cell, environment, count, say);
4627
5419
  if (cell.replicaSet !== void 0) return inReplicaSetEnvironment(cell, environment, count, say);
5420
+ if (cell.cluster !== void 0) return inClusterEnvironment(cell, environment, count, say);
4628
5421
  const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
4629
5422
  try {
4630
5423
  const settings = await lab.configure(cell.targetSettings ?? {});
@@ -4717,6 +5510,34 @@ const inReplicaSetEnvironment = async (cell, environment, count, say) => {
4717
5510
  }
4718
5511
  };
4719
5512
  /**
5513
+ * This function does the part of the runs of a cluster cell in one environment. The warm-up is a
5514
+ * whole run with its fault, as for a replica set. The queue converges before each run.
5515
+ */
5516
+ const inClusterEnvironment = async (cell, environment, count, say) => {
5517
+ const lab = await openClusterLab(cell.cluster);
5518
+ try {
5519
+ const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
5520
+ const warmup = {
5521
+ ...await clusterOnce(lab, cell, environment, -1),
5522
+ warmup: true
5523
+ };
5524
+ say(line(cell, warmup, measure(cell, warmup)));
5525
+ const runs = [];
5526
+ for (let index = 0; index < count; index += 1) {
5527
+ const run = await clusterOnce(lab, cell, environment, index);
5528
+ runs.push(run);
5529
+ say(line(cell, run, measure(cell, run)));
5530
+ }
5531
+ return {
5532
+ manifest,
5533
+ runs,
5534
+ warmup
5535
+ };
5536
+ } finally {
5537
+ await lab.close();
5538
+ }
5539
+ };
5540
+ /**
4720
5541
  * This function does the runs of a cell. They go across new environments, and each environment records
4721
5542
  * its manifest. Then the verdict judges the runs. A median across the edge of the band extends
4722
5543
  * the cell from 11 runs to 22, and then to 44, each extension in new environments. With a
@@ -4770,6 +5591,6 @@ const runCell = async (cell, say = () => {}) => {
4770
5591
  }
4771
5592
  };
4772
5593
  //#endregion
4773
- export { CLIENT_NAMES, CONVERGE_MS, INJECT, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, NOISE, PINNED, PUMP, PUMP_SLACK, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, REPOSITORY, SHIPPED_CLIENTS, SHIPPED_NAMES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, SUBJECTS, callOf, clockOffsetOf, closeOrExit, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, dropFinding, dropOf, endpointAfter, errorsOf, exitOf, fieldOf, fieldsOf, firstUnanswered, goHttpImage, groupOnce, groupVerdictOf, harnessImage, hikariClientImage, hzOf, imageOf, imageProblems, inContext, infoOf, initiateScript, isHopPlace, judge, lagsOf, latestEndOf, leftAfter, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, nodeClientImage, nodeHttpImage, noiseOf, onMonotonic, openGroupLab, openLab, openReplicaSetLab, peersOf, pgClientImage, pingsOf, planOf, proceduresImage, programLines, programOf, quantileOf, reconnectFinding, reconnectOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subjectClient, subjectProblems, subscriptionsOf, targetImageOf, targetSourceOf, templated, toxicBody, unlockedManifests, unpinnedBases, verdictOf, viewOf, writesOf };
5594
+ export { CLIENT_NAMES, CLUSTER_STAGES, CONVERGE_MS, GIVES_AWAY, INJECT, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, MONITOR_DOWN, NODE_MARKS, NOISE, PINNED, PUMP, PUMP_SLACK, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, REPOSITORY, SHIPPED_CLIENTS, SHIPPED_NAMES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, SUBJECTS, amqpClientImage, callOf, clockOffsetOf, closeOrExit, clusterArgs, clusterConf, clusterCutOf, clusterHostsOf, clusterOnce, clusterStagesOf, clusterViewOf, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, dropFinding, dropOf, endpointAfter, errorsOf, exitOf, fieldOf, fieldsOf, firstUnanswered, goHttpImage, groupOnce, groupVerdictOf, harnessImage, hikariClientImage, hzOf, imageOf, imageProblems, inContext, infoOf, initiateScript, isHopPlace, judge, lagsOf, latestEndOf, leftAfter, logNameOf, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, monotonicOf, nodeClientImage, nodeCommand, nodeHttpImage, nodeLineOf, nodeNameOf, nodesOnMonotonic, noiseOf, onMonotonic, openClusterLab, openGroupLab, openLab, openReplicaSetLab, peersOf, pgClientImage, pingsOf, plainOf, planOf, proceduresImage, programLines, programOf, quantileOf, reconnectFinding, reconnectOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, restamped, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subjectClient, subjectProblems, subscriptionsOf, targetImageOf, targetSourceOf, templated, toolJsonOf, toxicBody, unlockedManifests, unpinnedBases, verdictOf, viewOf, writesOf };
4774
5595
 
4775
5596
  //# sourceMappingURL=index.js.map