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