@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/README.md +20 -5
- package/build/{cell-B3-BSTit.d.ts → cell-DZm8zXN4.d.ts} +239 -8
- package/build/{image-BMdV7yWD.js → image-EZ8ioZ3L.js} +9 -2
- package/build/image-EZ8ioZ3L.js.map +1 -0
- package/build/index.d.ts +269 -4
- package/build/index.js +1284 -116
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +1 -1
- package/build/schema.js +1 -1
- package/image/Dockerfile +1 -1
- package/image/amqp/client-cluster.mjs +68 -31
- package/image/hikari/ClientHikari.java +81 -6
- package/image/kafka/AdminKafka.java +117 -0
- package/image/kafka/ClientKafka.java +234 -0
- package/image/kafka/Dockerfile +23 -0
- package/image/kafka/jars.sha256 +5 -0
- package/image/native/src/bin/ports.rs +374 -0
- package/image/native/src/bin/queue.rs +553 -0
- package/image/node/client-sentinel.mjs +28 -12
- package/image/pg/client-pg.mjs +31 -4
- package/package.json +5 -1
- package/build/image-BMdV7yWD.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
678
|
-
established: established === void 0 ? null : between$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1140
|
-
socket: seen === void 0 ? null : between$
|
|
1141
|
-
noticed: report === void 0 || seen === void 0 ? null : between$
|
|
1142
|
-
steps: (prediction.from === "last-received" ? records.filter((record) => record.ev === "probe" && record.what === "keepalive" && record.mono > reference) : []).map((record) => between$
|
|
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
|
|
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$
|
|
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$
|
|
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`
|
|
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`
|
|
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$
|
|
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$
|
|
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
|
-
])
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
4553
|
-
})()) !== void 0) await sleep$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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/
|
|
6848
|
+
//#region src/kafka.ts
|
|
6254
6849
|
/**
|
|
6255
|
-
* This
|
|
6256
|
-
*
|
|
6257
|
-
*
|
|
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
|
-
|
|
6260
|
-
|
|
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
|
-
|
|
6263
|
-
|
|
6931
|
+
primary,
|
|
6932
|
+
healthy: true,
|
|
6933
|
+
why: "healthy"
|
|
6264
6934
|
};
|
|
6265
6935
|
};
|
|
6266
|
-
/**
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
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
|