@mark1russell7/harness 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -5
- package/build/index.d.ts +595 -13
- package/build/index.js +1249 -25
- package/build/index.js.map +1 -1
- package/image/go-http/Dockerfile +14 -0
- package/image/go-http/go.mod +3 -0
- package/image/go-http/main.go +324 -0
- package/image/native/src/bin/inject.rs +182 -4
- package/image/node/Dockerfile +3 -2
- package/image/node/client-sentinel.mjs +106 -0
- package/image/node/observer.mjs +39 -0
- package/image/node-http/Dockerfile +12 -0
- package/image/node-http/client-agent.mjs +163 -0
- package/image/node-http/client-undici.mjs +141 -0
- package/image/node-http/lines.mjs +59 -0
- package/image/node-http/npm-lock.json +22 -0
- package/image/node-http/package.json +8 -0
- package/package.json +13 -2
package/build/index.js
CHANGED
|
@@ -5,7 +5,13 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { dirname, join, relative } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
//#region src/cell.ts
|
|
9
|
+
/** This function tells if a fault place is a hop of a topology. */
|
|
10
|
+
const isHopPlace = (place) => typeof place === "object" && "hop" in place;
|
|
11
|
+
//#endregion
|
|
8
12
|
//#region src/verdict.ts
|
|
13
|
+
/** This function gives the words of the outcome of a second request: its connection, a retry, and its end. */
|
|
14
|
+
const requestWords = (outcome) => `${outcome.connection}${outcome.retried ? ", a retry" : ""}, ${outcome.pending === true ? "pending" : outcome.error ?? "a reply"}`;
|
|
9
15
|
/**
|
|
10
16
|
* This amount is the noise of the pipeline itself, δ_abs, as the calibration measured it. It is
|
|
11
17
|
* the 99.9th percentile of the whole latency of a reset, at the concurrency cap or less. Its
|
|
@@ -111,6 +117,112 @@ const middle = (sorted) => {
|
|
|
111
117
|
* which does not exist at this time.
|
|
112
118
|
*/
|
|
113
119
|
const verdictOf = (prediction, runs) => {
|
|
120
|
+
const band = verdictOfBand(prediction, runs);
|
|
121
|
+
const counted = runs.filter((run) => run.void === null);
|
|
122
|
+
const verdict = counted.some((run) => run.request !== void 0) ? withRequests(band, counted) : band;
|
|
123
|
+
if (prediction.group === void 0 && !counted.some((run) => run.group !== void 0)) return verdict;
|
|
124
|
+
return {
|
|
125
|
+
...verdict,
|
|
126
|
+
group: groupVerdictOf(prediction, counted)
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* This function adds the outcomes of the second request of an HTTP client to a verdict. A cell
|
|
131
|
+
* whose outcome is a distribution gets them in its reason too.
|
|
132
|
+
*/
|
|
133
|
+
const withRequests = (verdict, counted) => {
|
|
134
|
+
const requests = {};
|
|
135
|
+
for (const run of counted) {
|
|
136
|
+
const words = run.request === null || run.request === void 0 ? "no second request" : requestWords(run.request);
|
|
137
|
+
requests[words] = (requests[words] ?? 0) + 1;
|
|
138
|
+
}
|
|
139
|
+
const listed = Object.entries(requests).map(([words, n]) => `${n} ${words}`).join("; ");
|
|
140
|
+
return {
|
|
141
|
+
...verdict,
|
|
142
|
+
requests,
|
|
143
|
+
why: verdict.decision === "measured" ? `${verdict.why} Second requests: ${listed}.` : verdict.why
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
/** This function gives the median of some counts, or null for none. */
|
|
147
|
+
const medianCount = (values) => values.length === 0 ? null : middle([...values].sort((a, b) => a - b));
|
|
148
|
+
/** This function sums what the group of a group cell did in its counted runs, and tests each stage that has a band. */
|
|
149
|
+
const groupVerdictOf = (prediction, counted) => {
|
|
150
|
+
const noise = valueIn(NOISE, "s");
|
|
151
|
+
const stamps = prediction.group?.stamps === void 0 ? 0 : valueIn(prediction.group.stamps, "s");
|
|
152
|
+
const stages = (prediction.group?.stages ?? []).map((band) => {
|
|
153
|
+
const interval = {
|
|
154
|
+
low: s(valueIn(band.low, "s") - noise),
|
|
155
|
+
high: s(valueIn(band.high, "s") + stamps + noise)
|
|
156
|
+
};
|
|
157
|
+
const outside = [];
|
|
158
|
+
const values = [];
|
|
159
|
+
for (const run of counted) {
|
|
160
|
+
const at = run.group?.stages[band.stage];
|
|
161
|
+
if (at === void 0) {
|
|
162
|
+
outside.push({
|
|
163
|
+
run: name(run),
|
|
164
|
+
why: `no Sentinel announced ${band.stage}`
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const value = valueIn(at, "s");
|
|
169
|
+
values.push(value);
|
|
170
|
+
if (value < valueIn(interval.low, "s") || value > valueIn(interval.high, "s")) outside.push({
|
|
171
|
+
run: name(run),
|
|
172
|
+
why: `${band.stage} at ${value} s is outside [${interval.low.value}, ${interval.high.value}] s`
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
values.sort((a, b) => a - b);
|
|
176
|
+
return {
|
|
177
|
+
stage: band.stage,
|
|
178
|
+
band: interval,
|
|
179
|
+
median: values.length === 0 ? null : s(middle(values)),
|
|
180
|
+
outside,
|
|
181
|
+
decision: outside.length === 0 && values.length > 0 ? "holds" : "fails"
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
const medians = {};
|
|
185
|
+
const seen = new Set(counted.flatMap((run) => Object.keys(run.group?.stages ?? {})));
|
|
186
|
+
for (const stage of seen) {
|
|
187
|
+
const values = counted.flatMap((run) => {
|
|
188
|
+
const at = run.group?.stages[stage];
|
|
189
|
+
return at === void 0 ? [] : [valueIn(at, "s")];
|
|
190
|
+
});
|
|
191
|
+
medians[stage] = s(middle(values.sort((a, b) => a - b)));
|
|
192
|
+
}
|
|
193
|
+
const lostCounts = counted.flatMap((run) => run.group?.writes.lost === null || run.group === void 0 ? [] : [run.group.writes.lost.length]);
|
|
194
|
+
const oldCounts = counted.flatMap((run) => run.group === void 0 ? [] : [run.group.writes.old]);
|
|
195
|
+
const unknown = counted.flatMap((run) => run.group?.writes.unknown ?? []);
|
|
196
|
+
const errors = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const run of counted) for (const error of run.group?.errors ?? []) errors.set(error.name, [...errors.get(error.name) ?? [], valueIn(error.first, "s")]);
|
|
198
|
+
const lags = counted.flatMap((run) => run.group?.lags ?? []);
|
|
199
|
+
return {
|
|
200
|
+
stages,
|
|
201
|
+
medians,
|
|
202
|
+
lost: {
|
|
203
|
+
runs: lostCounts.length,
|
|
204
|
+
total: lostCounts.reduce((sum, n) => sum + n, 0),
|
|
205
|
+
median: medianCount(lostCounts),
|
|
206
|
+
max: lostCounts.length === 0 ? null : Math.max(...lostCounts)
|
|
207
|
+
},
|
|
208
|
+
old: {
|
|
209
|
+
median: medianCount(oldCounts),
|
|
210
|
+
max: oldCounts.length === 0 ? null : Math.max(...oldCounts)
|
|
211
|
+
},
|
|
212
|
+
unknown: {
|
|
213
|
+
total: unknown.length,
|
|
214
|
+
held: unknown.filter((each) => each.held === true).length
|
|
215
|
+
},
|
|
216
|
+
errors: [...errors].map(([error, firsts]) => ({
|
|
217
|
+
name: error,
|
|
218
|
+
runs: firsts.length,
|
|
219
|
+
first: s(middle(firsts.sort((a, b) => a - b)))
|
|
220
|
+
})),
|
|
221
|
+
...lags.length === 0 ? {} : { lags: spreadOf(lags) }
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
/** This function gives the verdict of the band of a cell: its containment and the equivalence of its median. */
|
|
225
|
+
const verdictOfBand = (prediction, runs) => {
|
|
114
226
|
const counted = runs.filter((run) => run.void === null);
|
|
115
227
|
const voided = runs.length - counted.length;
|
|
116
228
|
const outside = counted.filter((run) => run.finding !== null).map((run) => ({
|
|
@@ -270,6 +382,252 @@ const verdictOf = (prediction, runs) => {
|
|
|
270
382
|
};
|
|
271
383
|
};
|
|
272
384
|
//#endregion
|
|
385
|
+
//#region src/timeline.ts
|
|
386
|
+
/**
|
|
387
|
+
* This module measures a run of a group cell. It gives the first write that a primary accepted
|
|
388
|
+
* after the fault, and the timeline of the group as its Sentinels announced it. It also gives what
|
|
389
|
+
* became of each write.
|
|
390
|
+
*/
|
|
391
|
+
/** These are the stages of a timeline, in their sequence after a fault. */
|
|
392
|
+
const STAGES = [
|
|
393
|
+
"+sdown",
|
|
394
|
+
"+odown",
|
|
395
|
+
"+try-failover",
|
|
396
|
+
"+elected-leader",
|
|
397
|
+
"+selected-slave",
|
|
398
|
+
"+promoted-slave",
|
|
399
|
+
"+switch-master",
|
|
400
|
+
"+convert-to-slave"
|
|
401
|
+
];
|
|
402
|
+
const between$1 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
|
|
403
|
+
/** This function gives the member whose address and port is `at`, or null. */
|
|
404
|
+
const memberAt = (run, at) => {
|
|
405
|
+
if (typeof at !== "string") return null;
|
|
406
|
+
const ip = at.split(":")[0];
|
|
407
|
+
return Object.entries(run.group?.members ?? {}).find(([, part]) => part.ip === ip)?.[0] ?? null;
|
|
408
|
+
};
|
|
409
|
+
/** This function gives the time of each stage of a timeline from an instant: the first announcement of each, after the instant. */
|
|
410
|
+
const stagesOf = (timeline, from) => {
|
|
411
|
+
const out = {};
|
|
412
|
+
for (const line of timeline) {
|
|
413
|
+
if (line.ev !== "sentinel" || line.t < from) continue;
|
|
414
|
+
const stage = STAGES.find((each) => each === line.event);
|
|
415
|
+
if (stage === void 0 || out[stage] !== void 0) continue;
|
|
416
|
+
if ((stage === "+sdown" || stage === "+odown") && !String(line.detail ?? "").startsWith("master ")) continue;
|
|
417
|
+
out[stage] = between$1(from, line.t);
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
};
|
|
421
|
+
/** This function gives what became of each write of a run, from the lines of the client and the keys that the group kept. */
|
|
422
|
+
const writesOf = (run, after, primary) => {
|
|
423
|
+
const { events } = run;
|
|
424
|
+
const sent = events.filter((event) => event.ev === "sent");
|
|
425
|
+
const done = events.find((event) => event.ev === "done")?.t ?? sent.at(-1)?.t ?? Infinity;
|
|
426
|
+
const acks = /* @__PURE__ */ new Map();
|
|
427
|
+
for (const event of events) if (event.ev === "ack") acks.set(Number(event["n"]), event);
|
|
428
|
+
const holds = run.group?.holds ?? null;
|
|
429
|
+
const kept = holds === null ? null : new Set(holds);
|
|
430
|
+
const old = [...acks.values()].filter((event) => event.t > after && memberAt(run, event["at"]) === primary).length;
|
|
431
|
+
const lost = kept === null ? null : [...acks.keys()].filter((n) => !kept.has(n)).sort((a, b) => a - b);
|
|
432
|
+
const unknown = sent.filter((event) => event.t < done && !acks.has(Number(event["n"]))).map((event) => {
|
|
433
|
+
const n = Number(event["n"]);
|
|
434
|
+
return {
|
|
435
|
+
n,
|
|
436
|
+
held: kept === null ? null : kept.has(n)
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
return {
|
|
440
|
+
sent: sent.length,
|
|
441
|
+
acked: acks.size,
|
|
442
|
+
old,
|
|
443
|
+
lost,
|
|
444
|
+
unknown
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
/** This function gives the errors that the client met after an instant: each kind, its first time, and its count. */
|
|
448
|
+
const errorsOf = (events, after) => {
|
|
449
|
+
const done = events.find((event) => event.ev === "done")?.t ?? Infinity;
|
|
450
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
451
|
+
for (const event of events) {
|
|
452
|
+
if (event.t <= after || event.t >= done) continue;
|
|
453
|
+
const name = event.ev === "error" ? String(event["errno"] ?? event["name"] ?? "error") : event.ev === "fail" ? String(event["name"] ?? "fail") : event.ev === "sentinel-error" ? "sentinel-error" : null;
|
|
454
|
+
if (name === null) continue;
|
|
455
|
+
const seen = kinds.get(name);
|
|
456
|
+
if (seen === void 0) kinds.set(name, {
|
|
457
|
+
first: event.t,
|
|
458
|
+
count: 1
|
|
459
|
+
});
|
|
460
|
+
else seen.count += 1;
|
|
461
|
+
}
|
|
462
|
+
return [...kinds].map(([name, { first, count }]) => ({
|
|
463
|
+
name,
|
|
464
|
+
first: between$1(after, first),
|
|
465
|
+
count
|
|
466
|
+
}));
|
|
467
|
+
};
|
|
468
|
+
/**
|
|
469
|
+
* This function gives the delay of each stamp of the observer after the write of its Sentinel that
|
|
470
|
+
* the observer read. That write is the latest write of the Sentinel's socket toward the observer,
|
|
471
|
+
* at the stamp or before it. The prober records each write (`tx`) on the clock of the stamps.
|
|
472
|
+
*/
|
|
473
|
+
const lagsOf = (run, kernel) => {
|
|
474
|
+
const timeline = run.group?.timeline ?? [];
|
|
475
|
+
const locals = /* @__PURE__ */ new Map();
|
|
476
|
+
for (const line of timeline) if (line.ev === "subscribed") locals.set(String(line.sentinel), String(line["local"]));
|
|
477
|
+
const out = [];
|
|
478
|
+
for (const line of timeline) {
|
|
479
|
+
if (line.ev !== "sentinel") continue;
|
|
480
|
+
const sentinel = String(line.sentinel);
|
|
481
|
+
const local = locals.get(sentinel);
|
|
482
|
+
if (local === void 0) continue;
|
|
483
|
+
const write = kernel.filter((record) => record.ev === "tx" && record.src === sentinel && record.dst === local && record.mono <= line.t).at(-1);
|
|
484
|
+
if (write !== void 0) out.push(between$1(write.mono, line.t));
|
|
485
|
+
}
|
|
486
|
+
return out;
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* This function reads one run of a group cell.
|
|
490
|
+
* - The reference is the start of the commit of the fault, as the injector stamped it. For a
|
|
491
|
+
* stall, it is the first write into the fault, as for the stall of a direct pair.
|
|
492
|
+
* - The outcome is the first write that a primary accepted after the fault. The prediction says
|
|
493
|
+
* which primary: a new one, or the same one after the lift.
|
|
494
|
+
* - Its time is the last segment with data that the client's socket to that primary received
|
|
495
|
+
* before the client saw the acceptance. The stamp of the client is the fallback.
|
|
496
|
+
* - A run is void when the prober lost records, or when the client did not become ready. It is
|
|
497
|
+
* also void when a Sentinel entered its TILT mode, or when the election split its votes. A band
|
|
498
|
+
* from the fault whose commit took longer than δ_abs is void too. So is a band whose hold lifted
|
|
499
|
+
* more than δ_abs late.
|
|
500
|
+
*/
|
|
501
|
+
const measureGroup = (cell, run) => {
|
|
502
|
+
const { events, kernel } = run;
|
|
503
|
+
const { prediction } = cell;
|
|
504
|
+
const expected = prediction.group;
|
|
505
|
+
const primary = run.group?.primary ?? "";
|
|
506
|
+
const ready = events.find((event) => event.ev === "ready");
|
|
507
|
+
const t0 = run.fault?.t0;
|
|
508
|
+
const empty = {
|
|
509
|
+
from: primary,
|
|
510
|
+
to: null,
|
|
511
|
+
stages: {},
|
|
512
|
+
writes: {
|
|
513
|
+
sent: 0,
|
|
514
|
+
acked: 0,
|
|
515
|
+
old: 0,
|
|
516
|
+
lost: null,
|
|
517
|
+
unknown: []
|
|
518
|
+
},
|
|
519
|
+
errors: []
|
|
520
|
+
};
|
|
521
|
+
const silent = {
|
|
522
|
+
outcome: { kind: "silent" },
|
|
523
|
+
elapsed: null,
|
|
524
|
+
socket: null,
|
|
525
|
+
noticed: null,
|
|
526
|
+
finding: null
|
|
527
|
+
};
|
|
528
|
+
if (ready === void 0) return {
|
|
529
|
+
...silent,
|
|
530
|
+
void: "the client never became ready",
|
|
531
|
+
group: empty
|
|
532
|
+
};
|
|
533
|
+
if (t0 === void 0 || run.fault === null) return {
|
|
534
|
+
...silent,
|
|
535
|
+
void: "the fault never committed",
|
|
536
|
+
group: empty
|
|
537
|
+
};
|
|
538
|
+
if (run.lost === null) return {
|
|
539
|
+
...silent,
|
|
540
|
+
void: "the prober wrote no clock line after the run, so nothing says what it dropped",
|
|
541
|
+
group: empty
|
|
542
|
+
};
|
|
543
|
+
if (run.lost > 0) return {
|
|
544
|
+
...silent,
|
|
545
|
+
void: `the prober dropped ${run.lost} records`,
|
|
546
|
+
group: empty
|
|
547
|
+
};
|
|
548
|
+
const timeline = run.group?.timeline ?? [];
|
|
549
|
+
const stages = stagesOf(timeline, t0);
|
|
550
|
+
const heal = run.heal ?? null;
|
|
551
|
+
const lands = expected?.lands ?? "new";
|
|
552
|
+
const after = lands === "same" && heal !== null ? heal.t0 : t0;
|
|
553
|
+
const accepted = events.find((event) => {
|
|
554
|
+
if (event.ev !== "ack" || event.t <= after) return false;
|
|
555
|
+
const member = memberAt(run, event["at"]);
|
|
556
|
+
return member !== null && (lands === "new" ? member !== primary : true);
|
|
557
|
+
});
|
|
558
|
+
const to = accepted === void 0 ? null : memberAt(run, accepted["at"]);
|
|
559
|
+
const group = {
|
|
560
|
+
from: primary,
|
|
561
|
+
to,
|
|
562
|
+
stages,
|
|
563
|
+
writes: writesOf(run, t0, primary),
|
|
564
|
+
errors: errorsOf(events, t0),
|
|
565
|
+
...cell.calibrates === "sentinels" ? { lags: lagsOf(run, kernel) } : {}
|
|
566
|
+
};
|
|
567
|
+
let voided = null;
|
|
568
|
+
const tilt = timeline.find((line) => line.ev === "sentinel" && line.event === "+tilt" && line.t >= t0);
|
|
569
|
+
if (tilt !== void 0) voided = `a Sentinel entered TILT ${between$1(t0, tilt.t).value} s after the fault: the host delayed it, not its timers`;
|
|
570
|
+
const split = timeline.find((line) => line.ev === "sentinel" && line.event === "-failover-abort-not-elected" && line.t >= t0);
|
|
571
|
+
const elected = timeline.find((line) => line.ev === "sentinel" && line.event === "+elected-leader" && line.t >= t0);
|
|
572
|
+
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";
|
|
573
|
+
const odown = stages["+odown"];
|
|
574
|
+
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";
|
|
575
|
+
if (voided === null && cell.hold !== void 0 && prediction.detects !== null) {
|
|
576
|
+
if (heal === null) voided = "the fault never lifted";
|
|
577
|
+
else {
|
|
578
|
+
const late = (heal.t0 - t0) / 1e9 - valueIn(cell.hold, "s");
|
|
579
|
+
if (late > valueIn(NOISE, "s")) voided = `the lift came ${Math.round(late * 1e4) / 10} ms after its hold: the injector was late, not the group`;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const assumed = prediction.detects === null ? void 0 : prediction.rtt;
|
|
583
|
+
const first = kernel.find((record) => record.netns === run.netns && record.dst === `${run.group?.members[primary]?.ip}:${cell.group?.port}` && record.tcp?.srtt !== void 0);
|
|
584
|
+
const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
|
|
585
|
+
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`;
|
|
586
|
+
if (voided === null && prediction.detects === true && prediction.from === "fault" && (run.fault.t1 - run.fault.t0) / 1e6 > valueIn(NOISE, "ms")) voided = `the commit of the fault took ${Math.round((run.fault.t1 - run.fault.t0) / 1e5) / 10} ms, longer than the noise of the pipeline: its start is not known`;
|
|
587
|
+
let reference = t0;
|
|
588
|
+
if (prediction.detects === false && prediction.recovers !== void 0) {
|
|
589
|
+
const into = events.find((event) => event.ev === "sent" && event.t > run.fault.t1);
|
|
590
|
+
if (into === void 0 || heal !== null && into.t > heal.t0) voided ??= "the write in flight went out before the fault held";
|
|
591
|
+
else {
|
|
592
|
+
reference = into.t;
|
|
593
|
+
const { heal: bounds } = prediction.recovers;
|
|
594
|
+
if (heal === null) voided ??= "the stall never ended";
|
|
595
|
+
else if (valueIn(between$1(reference, heal.t0), "s") <= valueIn(bounds.low, "s") || valueIn(between$1(reference, heal.t1), "s") >= valueIn(bounds.high, "s")) voided ??= `the stall ended ${between$1(reference, heal.t0).value}–${between$1(reference, heal.t1).value} s after the write, outside the ${bounds.low.value}–${bounds.high.value} s its prediction assumed`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const findings = [];
|
|
599
|
+
const switched = stages["+switch-master"];
|
|
600
|
+
if (expected?.failover === false && switched !== void 0) findings.push(`the group failed over ${switched.value} s after the fault, where it should not`);
|
|
601
|
+
if (accepted !== void 0 && lands === "same" && to !== primary) findings.push(`the client wrote to ${to ?? String(accepted["at"])}, where it should stay on ${primary}`);
|
|
602
|
+
for (const error of expected?.errors ?? []) {
|
|
603
|
+
const met = group.errors.find((each) => each.name === error.name);
|
|
604
|
+
const noise = valueIn(NOISE, "s");
|
|
605
|
+
if (met === void 0) findings.push(`the client never met ${error.name}`);
|
|
606
|
+
else if (met.first.value < valueIn(error.low, "s") - noise || met.first.value > valueIn(error.high, "s") + noise) findings.push(`the client met ${error.name} ${met.first.value} s after the fault, outside [${valueIn(error.low, "s")}, ${valueIn(error.high, "s")}] s`);
|
|
607
|
+
}
|
|
608
|
+
if (accepted === void 0) {
|
|
609
|
+
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");
|
|
610
|
+
return {
|
|
611
|
+
...silent,
|
|
612
|
+
void: voided,
|
|
613
|
+
finding: findings[0] ?? null,
|
|
614
|
+
group
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const address = String(accepted["at"]);
|
|
618
|
+
const segment = kernel.filter((record) => record.netns === run.netns && record.ev === "rx" && record.dst === address && (record.seg?.len.value ?? 0) > 0 && record.mono <= accepted.t && record.mono > after).at(-1);
|
|
619
|
+
const at = segment?.mono ?? accepted.t;
|
|
620
|
+
return {
|
|
621
|
+
outcome: { kind: "answered" },
|
|
622
|
+
elapsed: between$1(reference, at),
|
|
623
|
+
socket: between$1(reference, accepted.t),
|
|
624
|
+
noticed: segment === void 0 ? null : between$1(segment.mono, accepted.t),
|
|
625
|
+
void: voided,
|
|
626
|
+
finding: findings[0] ?? null,
|
|
627
|
+
group
|
|
628
|
+
};
|
|
629
|
+
};
|
|
630
|
+
//#endregion
|
|
273
631
|
//#region src/observe.ts
|
|
274
632
|
/** These are the errno numbers that the kernel reports to a socket, by the names that predictions use. */
|
|
275
633
|
const ERRNO = {
|
|
@@ -423,12 +781,82 @@ const crossingsOf = (run, hop, toClient, toTarget) => {
|
|
|
423
781
|
* it, and before the first keepalive probe. A live peer answers that probe, and the answer
|
|
424
782
|
* starts the idle time again. A run where the fault was late is void. A run whose records the
|
|
425
783
|
* prober lost is also void.
|
|
784
|
+
* - For an HTTP client, it also reads what the second request met (`requestOf`). An outcome other
|
|
785
|
+
* than the one that the prediction gives is a finding.
|
|
426
786
|
*/
|
|
427
787
|
const measure = (cell, run) => {
|
|
788
|
+
const read = measureSocket(cell, run);
|
|
789
|
+
const expected = cell.prediction.request;
|
|
790
|
+
if (cell.probe.http === void 0 && expected === void 0) return read;
|
|
791
|
+
const request = requestOf(run.events);
|
|
792
|
+
if (expected === void 0 || read.void !== null) return {
|
|
793
|
+
...read,
|
|
794
|
+
request
|
|
795
|
+
};
|
|
796
|
+
const differs = requestFinding(expected, request);
|
|
797
|
+
const finding = read.finding === null ? differs : differs === null ? read.finding : `${read.finding}. ${differs}`;
|
|
798
|
+
return {
|
|
799
|
+
...read,
|
|
800
|
+
request,
|
|
801
|
+
finding
|
|
802
|
+
};
|
|
803
|
+
};
|
|
804
|
+
/** This function tells how a request reading differs from its prediction, or gives null. */
|
|
805
|
+
const requestFinding = (expected, read) => {
|
|
806
|
+
if (read === null) return `the second request never went out, where it should meet: ${requestWords(expected)}`;
|
|
807
|
+
return read.connection === expected.connection && read.retried === expected.retried && read.error === expected.error && read.pending === (expected.pending === true) ? null : `the second request met: ${requestWords(read)}, not ${requestWords(expected)}`;
|
|
808
|
+
};
|
|
809
|
+
/**
|
|
810
|
+
* This function reads the second request of an HTTP client from its lines. Its first try is its
|
|
811
|
+
* first `request` line, and each `retry` line is a new try. Its end is its `error` line, or its
|
|
812
|
+
* `replied` line. A request with neither is pending. It gives null when the client wrote no try.
|
|
813
|
+
*
|
|
814
|
+
* A new try that gets no connection writes no `retry` line. Its dial writes a `dial-error` line.
|
|
815
|
+
* Thus a `dial-error` line after the first try and before the end is also a new try.
|
|
816
|
+
*/
|
|
817
|
+
const requestOf = (events) => {
|
|
818
|
+
const of = (event) => Number(event["i"]) === 2;
|
|
819
|
+
const tries = events.filter((event) => (event.ev === "request" || event.ev === "retry") && of(event));
|
|
820
|
+
const first = tries[0];
|
|
821
|
+
if (first === void 0) return null;
|
|
822
|
+
const failed = events.find((event) => event.ev === "error" && of(event));
|
|
823
|
+
const replied = events.some((event) => event.ev === "replied" && of(event));
|
|
824
|
+
const ready = events.find((event) => event.ev === "ready");
|
|
825
|
+
const sent = events.find((event) => event.ev === "sent" && of(event));
|
|
826
|
+
const end = failed ?? events.find((event) => event.ev === "done" && of(event));
|
|
827
|
+
const dialed = events.some((event) => event.ev === "dial-error" && event.t > first.t && (end === void 0 || event.t <= end.t));
|
|
828
|
+
return {
|
|
829
|
+
connection: first["reused"] === true ? "reused" : "new",
|
|
830
|
+
retried: dialed || tries.some((each) => each.ev === "retry"),
|
|
831
|
+
error: failed === void 0 ? null : String(failed["errno"]),
|
|
832
|
+
pending: failed === void 0 && !replied,
|
|
833
|
+
gap: ready === void 0 || sent === void 0 ? null : between(ready.t, sent.t)
|
|
834
|
+
};
|
|
835
|
+
};
|
|
836
|
+
/** This function reads the socket of one run against its cell, as `measure` does without the second request of an HTTP client. */
|
|
837
|
+
const measureSocket = (cell, run) => {
|
|
838
|
+
if (cell.group !== void 0) {
|
|
839
|
+
const read = measureGroup(cell, run);
|
|
840
|
+
return {
|
|
841
|
+
environment: run.environment,
|
|
842
|
+
index: run.index,
|
|
843
|
+
outcome: read.outcome,
|
|
844
|
+
elapsed: read.elapsed,
|
|
845
|
+
socket: read.socket,
|
|
846
|
+
noticed: read.noticed,
|
|
847
|
+
steps: [],
|
|
848
|
+
probes: 0,
|
|
849
|
+
retransmissions: run.kernel.filter((record) => record.netns === run.netns && record.ev === "retrans").length,
|
|
850
|
+
acks: read.group.writes.acked,
|
|
851
|
+
void: read.void,
|
|
852
|
+
finding: read.finding,
|
|
853
|
+
group: read.group
|
|
854
|
+
};
|
|
855
|
+
}
|
|
428
856
|
const { events, kernel } = run;
|
|
429
857
|
const ready = events.find((event) => event.ev === "ready");
|
|
430
858
|
const local = events.find((event) => event.ev === "connected")?.["local"];
|
|
431
|
-
const inflight = cell.probe.mode !== "idle";
|
|
859
|
+
const inflight = cell.probe.mode !== "idle" && cell.probe.mode !== "gap";
|
|
432
860
|
const request = inflight ? events.filter((event) => event.ev === "sent")[1] : ready;
|
|
433
861
|
const server = cell.observer === "server";
|
|
434
862
|
const place = socketOf(cell, run);
|
|
@@ -517,14 +945,15 @@ const measure = (cell, run) => {
|
|
|
517
945
|
const reported = socket.find((record) => record.ev === "error" && (record.err ?? 0) !== 0);
|
|
518
946
|
const seen = remote ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
|
|
519
947
|
const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8);
|
|
948
|
+
const own = cell.prediction.detects === true && cell.prediction.errno === "CLOSED" ? socket.find((record) => record.ev === "state" && oldState(record) === 1 && newState(record) === 4) : void 0;
|
|
520
949
|
const outcome = reported !== void 0 ? {
|
|
521
950
|
kind: "error",
|
|
522
951
|
errno: ERRNO[reported.err] ?? `E${reported.err}`
|
|
523
|
-
} : closeWait !== void 0 || seen?.ev === "eof" ? { kind: "eof" } : seen !== void 0 ? {
|
|
952
|
+
} : closeWait !== void 0 || seen?.ev === "eof" ? { kind: "eof" } : own !== void 0 ? { kind: "closed" } : seen !== void 0 ? {
|
|
524
953
|
kind: "error",
|
|
525
954
|
errno: String(seen["errno"])
|
|
526
955
|
} : { kind: "silent" };
|
|
527
|
-
const ended = reported?.mono ?? closeWait?.mono ?? seen?.t;
|
|
956
|
+
const ended = reported?.mono ?? closeWait?.mono ?? own?.mono ?? seen?.t;
|
|
528
957
|
const noticed = reported !== void 0 && seen !== void 0 ? between(reported.mono, seen.t) : null;
|
|
529
958
|
const { prediction } = cell;
|
|
530
959
|
const before = run.fault?.t1 ?? ready.t;
|
|
@@ -651,13 +1080,25 @@ const measure = (cell, run) => {
|
|
|
651
1080
|
if (!prediction.detects) {
|
|
652
1081
|
const window = valueIn(prediction.window, "s") * 1e9;
|
|
653
1082
|
if (ended === void 0 || ended - request.t > window) {
|
|
654
|
-
const
|
|
1083
|
+
const forgetIn = forgotten !== void 0 && forgotten.t - request.t <= window ? forgotten : void 0;
|
|
1084
|
+
const expected = prediction.middlebox?.forgets;
|
|
1085
|
+
const after = forgetIn === void 0 ? null : between(lastRx, forgetIn.t);
|
|
1086
|
+
let finding = null;
|
|
1087
|
+
if (expected === void 0) {
|
|
1088
|
+
if (after !== null) finding = `a router forgot the flow ${after.value} s after the last segment, where it should keep it`;
|
|
1089
|
+
} else if (after === null) finding = "no router forgot the flow in the window, where it should forget it";
|
|
1090
|
+
else {
|
|
1091
|
+
const noise = valueIn(NOISE, "s");
|
|
1092
|
+
const low = valueIn(expected.low, "s") - noise;
|
|
1093
|
+
const high = valueIn(expected.high, "s") + noise;
|
|
1094
|
+
if (after.value < low || after.value > high) finding = `a router forgot the flow ${after.value} s after the last segment, outside [${Math.round(low * 1e6) / 1e6}, ${Math.round(high * 1e6) / 1e6}] s`;
|
|
1095
|
+
}
|
|
655
1096
|
return {
|
|
656
1097
|
...base,
|
|
657
1098
|
...nothing,
|
|
658
1099
|
...chain,
|
|
659
1100
|
void: voided,
|
|
660
|
-
finding
|
|
1101
|
+
finding
|
|
661
1102
|
};
|
|
662
1103
|
}
|
|
663
1104
|
return {
|
|
@@ -688,8 +1129,9 @@ const measure = (cell, run) => {
|
|
|
688
1129
|
finding: `it ended without its reference event (${prediction.from})`
|
|
689
1130
|
};
|
|
690
1131
|
const steps = (prediction.from === "last-received" ? keepalives : prediction.from === "first-retransmission" ? retransmitted.filter((record) => record.why === "rto" && record.mono > reference) : []).map((record) => between(reference, record.mono));
|
|
691
|
-
const finding = (prediction.errno === "EOF" ? outcome.kind === "eof" : outcome.kind === "error" && outcome.errno === prediction.errno) ? null : `it ended with ${outcome.kind === "error" ? outcome.errno : "end of file"}, not ${prediction.errno}`;
|
|
692
|
-
|
|
1132
|
+
const finding = (prediction.errno === "EOF" ? outcome.kind === "eof" : prediction.errno === "CLOSED" ? outcome.kind === "closed" : outcome.kind === "error" && outcome.errno === prediction.errno) ? null : `it ended with ${outcome.kind === "error" ? outcome.errno : outcome.kind === "closed" ? "its own close" : "end of file"}, not ${prediction.errno}`;
|
|
1133
|
+
const programTimer = cell.probe.http !== void 0 && prediction.from === "last-received" && prediction.timer !== "keepalive";
|
|
1134
|
+
if (prediction.from === "last-received" && !programTimer || prediction.from === "first-retransmission") voided ??= delayed(reference, ended, prediction.from === "last-received" ? "keepalive" : "retransmit");
|
|
693
1135
|
const lag = (valueIn(prediction.high, "s") - valueIn(prediction.low, "s")) * 1e9;
|
|
694
1136
|
const slow = prediction.timer.startsWith("expire_") ? (run.middlebox ?? []).find((line) => line.ev === "pump-slow" && line.t <= ended && line.t + Number(line["took"]) * 1e9 >= ended - lag) : void 0;
|
|
695
1137
|
if (slow !== void 0) voided ??= `a listing of the router took ${Math.round(Number(slow["took"]) * 1e4) / 10} ms, longer than the bound of its prediction`;
|
|
@@ -746,6 +1188,18 @@ const NODE = {
|
|
|
746
1188
|
dockerfile: "harness/image/node/Dockerfile",
|
|
747
1189
|
from: ["harness/image/node"]
|
|
748
1190
|
};
|
|
1191
|
+
/** This image holds the Go HTTP client: the http.Transport of Go 1.27.2. */
|
|
1192
|
+
const GO_HTTP = {
|
|
1193
|
+
name: "loom-harness-go-http",
|
|
1194
|
+
dockerfile: "harness/image/go-http/Dockerfile",
|
|
1195
|
+
from: ["harness/image/go-http"]
|
|
1196
|
+
};
|
|
1197
|
+
/** This image holds the Node HTTP clients: the http.Agent of Node 24.21.0, and undici 8.12.2. */
|
|
1198
|
+
const NODE_HTTP = {
|
|
1199
|
+
name: "loom-harness-node-http",
|
|
1200
|
+
dockerfile: "harness/image/node-http/Dockerfile",
|
|
1201
|
+
from: ["harness/image/node-http"]
|
|
1202
|
+
};
|
|
749
1203
|
/** This image is the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow. */
|
|
750
1204
|
const ROUTER = {
|
|
751
1205
|
name: "loom-harness-router",
|
|
@@ -812,6 +1266,10 @@ const harnessImage = () => build(HARNESS);
|
|
|
812
1266
|
const nodeClientImage = () => build(NODE);
|
|
813
1267
|
/** This function gives the image of the router. */
|
|
814
1268
|
const routerImage = () => build(ROUTER);
|
|
1269
|
+
/** This function gives the image of the Go HTTP client. */
|
|
1270
|
+
const goHttpImage = () => build(GO_HTTP);
|
|
1271
|
+
/** This function gives the image of the Node HTTP clients. */
|
|
1272
|
+
const nodeHttpImage = () => build(NODE_HTTP);
|
|
815
1273
|
/**
|
|
816
1274
|
* This time is the interval of the listing of a router, when its hop does not set one. A listing
|
|
817
1275
|
* evicts each expired flow that it walks past, and the DESTROY event of that flow follows.
|
|
@@ -888,6 +1346,11 @@ const lines = (also) => {
|
|
|
888
1346
|
}
|
|
889
1347
|
};
|
|
890
1348
|
};
|
|
1349
|
+
/** This function makes a container share the process namespace of another container, for example `container:<id>`. */
|
|
1350
|
+
const withPid = (container, mode) => {
|
|
1351
|
+
container.hostConfig.PidMode = mode;
|
|
1352
|
+
return container;
|
|
1353
|
+
};
|
|
891
1354
|
/** This container class can share the process namespace of another container. */
|
|
892
1355
|
const shared = (Base) => class extends Base {
|
|
893
1356
|
withPidMode(mode) {
|
|
@@ -949,6 +1412,16 @@ const clientOf = async (kind, image) => {
|
|
|
949
1412
|
command: ["node", "/opt/harness/node/client-ioredis.mjs"],
|
|
950
1413
|
clientKind: kind
|
|
951
1414
|
};
|
|
1415
|
+
if (kind === "go-http") return {
|
|
1416
|
+
clientImage: await goHttpImage(),
|
|
1417
|
+
command: ["/opt/harness/client-go"],
|
|
1418
|
+
clientKind: kind
|
|
1419
|
+
};
|
|
1420
|
+
if (kind === "http-agent" || kind === "undici") return {
|
|
1421
|
+
clientImage: await nodeHttpImage(),
|
|
1422
|
+
command: ["node", `/opt/harness/node-http/client-${kind === "undici" ? "undici" : "agent"}.mjs`],
|
|
1423
|
+
clientKind: kind
|
|
1424
|
+
};
|
|
952
1425
|
return {
|
|
953
1426
|
clientImage: image,
|
|
954
1427
|
command: ["/opt/harness/client"],
|
|
@@ -989,6 +1462,7 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
989
1462
|
const runtime = await getContainerRuntimeClient();
|
|
990
1463
|
const opened = [];
|
|
991
1464
|
const networks = [];
|
|
1465
|
+
const serverLines = lines();
|
|
992
1466
|
const close = async () => {
|
|
993
1467
|
for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
|
|
994
1468
|
for (const network of networks.reverse()) await network.stop().catch(() => void 0);
|
|
@@ -1008,7 +1482,12 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1008
1482
|
try {
|
|
1009
1483
|
for (let at = 0; at <= hops.length; at += 1) networks.push(await new Network().start());
|
|
1010
1484
|
const last = networks[hops.length];
|
|
1011
|
-
|
|
1485
|
+
let stock = new GenericContainer(target.image).withNetwork(last).withWaitStrategy(Wait.forLogMessage(target.ready));
|
|
1486
|
+
if (target.files !== void 0) stock = stock.withCopyContentToContainer(Object.entries(target.files).map(([path, content]) => ({
|
|
1487
|
+
content,
|
|
1488
|
+
target: path
|
|
1489
|
+
})));
|
|
1490
|
+
if (target.lines === true) stock = stock.withLogConsumer(serverLines.consume);
|
|
1012
1491
|
const server = await (target.command === void 0 ? stock : stock.withCommand([...target.command])).start();
|
|
1013
1492
|
opened.push(server);
|
|
1014
1493
|
const serverIp = await ipOn(server, last);
|
|
@@ -1207,17 +1686,22 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1207
1686
|
* on its sockets. A router forwards past INPUT and OUTPUT, thus its rules are FORWARD rules on
|
|
1208
1687
|
* its device toward the side (`--via`).
|
|
1209
1688
|
*/
|
|
1210
|
-
const
|
|
1211
|
-
if (at
|
|
1689
|
+
const hopPlace = (at) => {
|
|
1690
|
+
if (!isHopPlace(at)) throw new Error(`a lab of a topology has no part ${JSON.stringify(at)}: a group cell opens a group lab`);
|
|
1691
|
+
return at;
|
|
1692
|
+
};
|
|
1693
|
+
const placeOf = (place) => {
|
|
1694
|
+
if (place === "client") return [
|
|
1212
1695
|
admin,
|
|
1213
1696
|
targetIp,
|
|
1214
1697
|
[]
|
|
1215
1698
|
];
|
|
1216
|
-
if (
|
|
1699
|
+
if (place === "target") return [
|
|
1217
1700
|
injector,
|
|
1218
1701
|
targetPeer,
|
|
1219
1702
|
[]
|
|
1220
1703
|
];
|
|
1704
|
+
const at = hopPlace(place);
|
|
1221
1705
|
const hop = hopNamed(at.hop);
|
|
1222
1706
|
const peer = at.toward === "client" ? hop.peer.client : hop.peer.target.split(":")[0];
|
|
1223
1707
|
const via = hop.hop.kind === "router" ? ["--via", hop.devices[at.toward]] : [];
|
|
@@ -1228,9 +1712,10 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1228
1712
|
];
|
|
1229
1713
|
};
|
|
1230
1714
|
/** This function gives the namespace and the device of netem at a place: an end, or the device of a hop toward a side. */
|
|
1231
|
-
const deviceAt = (
|
|
1232
|
-
if (
|
|
1233
|
-
if (
|
|
1715
|
+
const deviceAt = (place) => {
|
|
1716
|
+
if (place === "client") return [admin, "eth0"];
|
|
1717
|
+
if (place === "target") return [injector, "eth0"];
|
|
1718
|
+
const at = hopPlace(place);
|
|
1234
1719
|
const hop = hopNamed(at.hop);
|
|
1235
1720
|
return [hop.injector, hop.devices[at.toward]];
|
|
1236
1721
|
};
|
|
@@ -1254,6 +1739,7 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1254
1739
|
netns,
|
|
1255
1740
|
topology: placed,
|
|
1256
1741
|
middlebox,
|
|
1742
|
+
server: serverLines.seen,
|
|
1257
1743
|
kernel: records.seen,
|
|
1258
1744
|
async sysctl(values) {
|
|
1259
1745
|
const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
|
|
@@ -1340,7 +1826,7 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1340
1826
|
return stamped(where, args);
|
|
1341
1827
|
},
|
|
1342
1828
|
async offload(on, at = "target") {
|
|
1343
|
-
const devices =
|
|
1829
|
+
const devices = isHopPlace(at) ? Object.values(hopNamed(at.hop).devices) : [deviceAt(at)[1]];
|
|
1344
1830
|
const [where] = deviceAt(at);
|
|
1345
1831
|
for (const device of devices) await output(where, [
|
|
1346
1832
|
"/opt/harness/inject",
|
|
@@ -1519,7 +2005,9 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1519
2005
|
image: target.image,
|
|
1520
2006
|
id: (await inspect(server)).Image,
|
|
1521
2007
|
settings,
|
|
1522
|
-
sysctls: targetSysctls
|
|
2008
|
+
sysctls: targetSysctls,
|
|
2009
|
+
...target.files === void 0 ? {} : { files: Object.fromEntries(Object.entries(target.files).map(([path, content]) => [path, createHash("sha256").update(content).digest("hex")])) },
|
|
2010
|
+
...serverLines.seen[0] === void 0 ? {} : { first: serverLines.seen[0] }
|
|
1523
2011
|
},
|
|
1524
2012
|
harness: {
|
|
1525
2013
|
image,
|
|
@@ -1547,6 +2035,673 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1547
2035
|
}
|
|
1548
2036
|
};
|
|
1549
2037
|
//#endregion
|
|
2038
|
+
//#region src/group.ts
|
|
2039
|
+
/** This function gives the configuration file of a member: its port, its settings, and the primary that a replica follows. */
|
|
2040
|
+
const memberConfig = (group, primary) => [
|
|
2041
|
+
`port ${group.port}`,
|
|
2042
|
+
...Object.entries(group.memberSettings).map(([key, value]) => `${key} ${value}`),
|
|
2043
|
+
...primary === null ? [] : [`replicaof ${primary} ${group.port}`],
|
|
2044
|
+
""
|
|
2045
|
+
].join("\n");
|
|
2046
|
+
/** This function gives the configuration file of a Sentinel: its port, the group that it monitors, and the settings of the group. */
|
|
2047
|
+
const sentinelConfig = (group, primary) => [
|
|
2048
|
+
`port ${group.sentinelPort}`,
|
|
2049
|
+
`sentinel monitor ${group.name} ${primary} ${group.port} ${group.quorum}`,
|
|
2050
|
+
...Object.entries(group.sentinelSettings).map(([key, value]) => `sentinel ${key} ${group.name} ${value}`),
|
|
2051
|
+
""
|
|
2052
|
+
].join("\n");
|
|
2053
|
+
/**
|
|
2054
|
+
* This function gives the peers that a fault on a part cuts it from, by their addresses. `all` is
|
|
2055
|
+
* each other part and the client, `group` is the other parts, and `clients` is the client. The
|
|
2056
|
+
* observer is not a peer, thus the timeline of the group stays whole.
|
|
2057
|
+
*/
|
|
2058
|
+
const peersOf = (parts, client, part, cuts) => {
|
|
2059
|
+
const others = [...Object.entries(parts.members), ...Object.entries(parts.sentinels)].filter(([name]) => name !== part).map(([, at]) => at.ip);
|
|
2060
|
+
if (cuts === "clients") return [client];
|
|
2061
|
+
if (cuts === "group") return others;
|
|
2062
|
+
return [...others, client];
|
|
2063
|
+
};
|
|
2064
|
+
/** This function reads the flat reply of a command line client into its fields: name, value, name, value. */
|
|
2065
|
+
const fieldsOf = (text) => {
|
|
2066
|
+
const out = [];
|
|
2067
|
+
const words = text.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
2068
|
+
let current = null;
|
|
2069
|
+
for (let at = 0; at + 1 < words.length; at += 2) {
|
|
2070
|
+
const [key, value] = [words[at], words[at + 1]];
|
|
2071
|
+
if (key === "name" || current === null) {
|
|
2072
|
+
current = {};
|
|
2073
|
+
out.push(current);
|
|
2074
|
+
}
|
|
2075
|
+
current[key] = value;
|
|
2076
|
+
}
|
|
2077
|
+
return out;
|
|
2078
|
+
};
|
|
2079
|
+
/** This function reads the INFO of a server into its fields. */
|
|
2080
|
+
const infoOf = (text) => {
|
|
2081
|
+
const out = {};
|
|
2082
|
+
for (const line of text.split("\n")) {
|
|
2083
|
+
const at = line.indexOf(":");
|
|
2084
|
+
if (line.startsWith("#") || at === -1) continue;
|
|
2085
|
+
out[line.slice(0, at)] = line.slice(at + 1).trim();
|
|
2086
|
+
}
|
|
2087
|
+
return out;
|
|
2088
|
+
};
|
|
2089
|
+
/** This function judges the views of the Sentinels and of the members: the primary that they agree on, if the group is healthy. */
|
|
2090
|
+
const viewOf = (group, ips, sentinels, members) => {
|
|
2091
|
+
const others = String(group.sentinels.length - 1);
|
|
2092
|
+
const replicas = group.members.length - 1;
|
|
2093
|
+
const addresses = new Set(sentinels.map((each) => each.master["ip"]));
|
|
2094
|
+
if (addresses.size !== 1) return {
|
|
2095
|
+
primary: null,
|
|
2096
|
+
healthy: false,
|
|
2097
|
+
why: `the Sentinels name ${addresses.size} primaries`
|
|
2098
|
+
};
|
|
2099
|
+
const ip = [...addresses][0];
|
|
2100
|
+
const primary = Object.entries(ips).find(([, at]) => at === ip)?.[0] ?? null;
|
|
2101
|
+
if (primary === null) return {
|
|
2102
|
+
primary: null,
|
|
2103
|
+
healthy: false,
|
|
2104
|
+
why: `no member has the address ${ip}`
|
|
2105
|
+
};
|
|
2106
|
+
for (const [at, each] of sentinels.entries()) {
|
|
2107
|
+
if (each.master["flags"] !== "master") return {
|
|
2108
|
+
primary,
|
|
2109
|
+
healthy: false,
|
|
2110
|
+
why: `Sentinel ${at} flags the primary ${each.master["flags"]}`
|
|
2111
|
+
};
|
|
2112
|
+
if (each.master["num-other-sentinels"] !== others) return {
|
|
2113
|
+
primary,
|
|
2114
|
+
healthy: false,
|
|
2115
|
+
why: `Sentinel ${at} knows ${each.master["num-other-sentinels"]} other Sentinels`
|
|
2116
|
+
};
|
|
2117
|
+
const good = each.replicas.filter((replica) => replica["flags"] === "slave" && replica["master-link-status"] === "ok" && replica["master-host"] === ip);
|
|
2118
|
+
if (good.length !== replicas || each.replicas.length !== replicas) return {
|
|
2119
|
+
primary,
|
|
2120
|
+
healthy: false,
|
|
2121
|
+
why: `Sentinel ${at} has ${good.length} healthy replicas of ${each.replicas.length}`
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
const info = members[primary] ?? {};
|
|
2125
|
+
if (info["role"] !== "master") return {
|
|
2126
|
+
primary,
|
|
2127
|
+
healthy: false,
|
|
2128
|
+
why: `${primary} reports the role ${info["role"]}`
|
|
2129
|
+
};
|
|
2130
|
+
if (info["master_failover_state"] !== void 0 && info["master_failover_state"] !== "no-failover") return {
|
|
2131
|
+
primary,
|
|
2132
|
+
healthy: false,
|
|
2133
|
+
why: `${primary} is in the failover state ${info["master_failover_state"]}`
|
|
2134
|
+
};
|
|
2135
|
+
const online = Object.entries(info).filter(([key, value]) => /^slave\d+$/.test(key) && value.includes("state=online")).length;
|
|
2136
|
+
if (online !== replicas) return {
|
|
2137
|
+
primary,
|
|
2138
|
+
healthy: false,
|
|
2139
|
+
why: `${primary} has ${online} replicas online`
|
|
2140
|
+
};
|
|
2141
|
+
for (const name of group.members) {
|
|
2142
|
+
if (name === primary) continue;
|
|
2143
|
+
const each = members[name] ?? {};
|
|
2144
|
+
if (each["role"] !== "slave" || each["master_host"] !== ip || each["master_link_status"] !== "up") return {
|
|
2145
|
+
primary,
|
|
2146
|
+
healthy: false,
|
|
2147
|
+
why: `${name} is not a replica of ${primary} with its link up`
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
return {
|
|
2151
|
+
primary,
|
|
2152
|
+
healthy: true,
|
|
2153
|
+
why: "healthy"
|
|
2154
|
+
};
|
|
2155
|
+
};
|
|
2156
|
+
const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2157
|
+
/** This time is the longest that a group takes to converge before a run, or after it. */
|
|
2158
|
+
const CONVERGE_MS = 24e4;
|
|
2159
|
+
/**
|
|
2160
|
+
* This function opens one environment of a group cell:
|
|
2161
|
+
* - one new network, with each member, each Sentinel, the client and the observer on it.
|
|
2162
|
+
* - each member: the stock image, its server as the child of a shell, thus a fin can stop the
|
|
2163
|
+
* server and the container stays up. The first member is the primary, and the others follow it.
|
|
2164
|
+
* - each Sentinel: the stock image, its Sentinel as the child of a shell, which monitors the first member.
|
|
2165
|
+
* - an injector sidecar for each part, in its network and process namespaces.
|
|
2166
|
+
* - a client that holds its own network namespace, with a privileged sidecar for its sysctls.
|
|
2167
|
+
* - the observer of the Sentinels, in a namespace of its own.
|
|
2168
|
+
* - the prober, which watches the namespace of the client, and of each Sentinel for a calibration.
|
|
2169
|
+
*/
|
|
2170
|
+
const openGroupLab = async (group, options = {}) => {
|
|
2171
|
+
const image = await harnessImage();
|
|
2172
|
+
const node = await nodeClientImage();
|
|
2173
|
+
const { GenericContainer, Network, Wait, getContainerRuntimeClient } = await containers();
|
|
2174
|
+
const runtime = await getContainerRuntimeClient();
|
|
2175
|
+
const opened = [];
|
|
2176
|
+
let network;
|
|
2177
|
+
const close = async () => {
|
|
2178
|
+
for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
|
|
2179
|
+
await network?.stop().catch(() => void 0);
|
|
2180
|
+
};
|
|
2181
|
+
const ipOf = async (container) => {
|
|
2182
|
+
const ip = (await runtime.container.inspect(runtime.container.getById(container.getId()))).NetworkSettings.Networks[network.getName()]?.IPAddress;
|
|
2183
|
+
if (ip === void 0 || ip === "") throw new Error(`${container.getName()} has no address on ${network.getName()}`);
|
|
2184
|
+
return ip;
|
|
2185
|
+
};
|
|
2186
|
+
try {
|
|
2187
|
+
network = await new Network().start();
|
|
2188
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
2189
|
+
const injectors = /* @__PURE__ */ new Map();
|
|
2190
|
+
const parts = {
|
|
2191
|
+
members: {},
|
|
2192
|
+
sentinels: {}
|
|
2193
|
+
};
|
|
2194
|
+
/** This function starts one part: the stock image with its configuration, its program as the child of a shell, and its injector. */
|
|
2195
|
+
const start = async (name, program, file, content, ready) => {
|
|
2196
|
+
const box = await new GenericContainer(group.image).withNetwork(network).withNetworkAliases(name).withCopyContentToContainer([{
|
|
2197
|
+
content,
|
|
2198
|
+
target: file
|
|
2199
|
+
}]).withCommand([
|
|
2200
|
+
"sh",
|
|
2201
|
+
"-c",
|
|
2202
|
+
`${program} ${file} & while :; do sleep 3600; done`
|
|
2203
|
+
]).withWaitStrategy(Wait.forLogMessage(ready)).start();
|
|
2204
|
+
opened.push(box);
|
|
2205
|
+
boxes.set(name, box);
|
|
2206
|
+
const injector = await withPid(new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${box.getId()}`).start();
|
|
2207
|
+
opened.push(injector);
|
|
2208
|
+
injectors.set(name, injector);
|
|
2209
|
+
return {
|
|
2210
|
+
ip: await ipOf(box),
|
|
2211
|
+
netns: await netnsOf(injector)
|
|
2212
|
+
};
|
|
2213
|
+
};
|
|
2214
|
+
const [first, ...rest] = group.members;
|
|
2215
|
+
if (first === void 0) throw new Error("a group has no member");
|
|
2216
|
+
parts.members[first] = await start(first, group.programs.server, "/data/server.conf", memberConfig(group, null), group.ready.member);
|
|
2217
|
+
const primaryIp = parts.members[first].ip;
|
|
2218
|
+
for (const name of rest) parts.members[name] = await start(name, group.programs.server, "/data/server.conf", memberConfig(group, primaryIp), group.ready.member);
|
|
2219
|
+
for (const name of group.sentinels) parts.sentinels[name] = await start(name, group.programs.sentinel, "/data/sentinel.conf", sentinelConfig(group, primaryIp), group.ready.sentinel);
|
|
2220
|
+
const holder = await new GenericContainer(image).withNetwork(network).start();
|
|
2221
|
+
opened.push(holder);
|
|
2222
|
+
const admin = await new GenericContainer(image).withNetworkMode(`container:${holder.getId()}`).withPrivilegedMode().start();
|
|
2223
|
+
opened.push(admin);
|
|
2224
|
+
const clientIp = await ipOf(holder);
|
|
2225
|
+
const sentinelAddresses = group.sentinels.map((name) => `${parts.sentinels[name].ip}:${group.sentinelPort}`);
|
|
2226
|
+
const timeline = lines();
|
|
2227
|
+
const observer = await new GenericContainer(node).withNetwork(network).withCommand([
|
|
2228
|
+
"node",
|
|
2229
|
+
"/opt/harness/node/observer.mjs",
|
|
2230
|
+
sentinelAddresses.join(",")
|
|
2231
|
+
]).withLogConsumer(timeline.consume).start();
|
|
2232
|
+
opened.push(observer);
|
|
2233
|
+
const observerIp = await ipOf(observer);
|
|
2234
|
+
for (const address of sentinelAddresses) await new Promise((resolve, reject) => {
|
|
2235
|
+
const started = Date.now();
|
|
2236
|
+
const look = () => {
|
|
2237
|
+
if (timeline.seen.some((line) => line.ev === "subscribed" && line["sentinel"] === address)) resolve();
|
|
2238
|
+
else if (Date.now() - started > 3e4) reject(/* @__PURE__ */ new Error(`the observer never subscribed to ${address}`));
|
|
2239
|
+
else setTimeout(look, 50);
|
|
2240
|
+
};
|
|
2241
|
+
look();
|
|
2242
|
+
});
|
|
2243
|
+
const clientNetns = await netnsOf(admin);
|
|
2244
|
+
const watched = [clientNetns, ...options.watchSentinels === true ? group.sentinels.map((name) => parts.sentinels[name].netns) : []];
|
|
2245
|
+
const records = lines();
|
|
2246
|
+
const prober = await new GenericContainer(image).withPrivilegedMode().withNetworkMode("none").withCommand(["/opt/harness/prober", ...watched.map(String)]).withLogConsumer(records.consume).start();
|
|
2247
|
+
opened.push(prober);
|
|
2248
|
+
const begun = await records.until(["begin", "fail"], 3e4, "prober");
|
|
2249
|
+
if (begun.ev === "fail") throw new Error(`the prober failed: ${String(begun["why"])}`);
|
|
2250
|
+
const box = (name) => {
|
|
2251
|
+
const found = boxes.get(name);
|
|
2252
|
+
if (found === void 0) throw new Error(`the group has no part ${name}`);
|
|
2253
|
+
return found;
|
|
2254
|
+
};
|
|
2255
|
+
const injectorOf = (name) => {
|
|
2256
|
+
const found = injectors.get(name);
|
|
2257
|
+
if (found === void 0) throw new Error(`the group has no part ${name}`);
|
|
2258
|
+
return found;
|
|
2259
|
+
};
|
|
2260
|
+
const cli = (name, args) => {
|
|
2261
|
+
const port = group.sentinels.includes(name) ? group.sentinelPort : group.port;
|
|
2262
|
+
return output(box(name), [
|
|
2263
|
+
group.programs.cli,
|
|
2264
|
+
"-p",
|
|
2265
|
+
String(port),
|
|
2266
|
+
...args
|
|
2267
|
+
]);
|
|
2268
|
+
};
|
|
2269
|
+
const stamped = async (where, argv) => {
|
|
2270
|
+
const stamp = JSON.parse((await output(where, argv)).split("\n").at(-1));
|
|
2271
|
+
return {
|
|
2272
|
+
t0: stamp.t0,
|
|
2273
|
+
t1: stamp.t1
|
|
2274
|
+
};
|
|
2275
|
+
};
|
|
2276
|
+
const ips = Object.fromEntries(Object.entries(parts.members).map(([name, at]) => [name, at.ip]));
|
|
2277
|
+
const view = async () => {
|
|
2278
|
+
const sentinels = [];
|
|
2279
|
+
for (const name of group.sentinels) {
|
|
2280
|
+
const master = fieldsOf(await cli(name, [
|
|
2281
|
+
"SENTINEL",
|
|
2282
|
+
"MASTER",
|
|
2283
|
+
group.name
|
|
2284
|
+
]))[0] ?? {};
|
|
2285
|
+
const replicas = fieldsOf(await cli(name, [
|
|
2286
|
+
"SENTINEL",
|
|
2287
|
+
"REPLICAS",
|
|
2288
|
+
group.name
|
|
2289
|
+
]));
|
|
2290
|
+
sentinels.push({
|
|
2291
|
+
master,
|
|
2292
|
+
replicas
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
const members = {};
|
|
2296
|
+
for (const name of group.members) members[name] = infoOf(await cli(name, ["INFO", "replication"]));
|
|
2297
|
+
return viewOf(group, ips, sentinels, members);
|
|
2298
|
+
};
|
|
2299
|
+
const after = (at) => at === void 0 ? [] : ["--at", String(Math.round(at))];
|
|
2300
|
+
const roles = (role, primary) => {
|
|
2301
|
+
if (role === "primary") return primary;
|
|
2302
|
+
if (role === "sentinel") return group.sentinels[0];
|
|
2303
|
+
const replica = group.members.find((name) => name !== primary);
|
|
2304
|
+
if (replica === void 0) throw new Error("the group has no replica");
|
|
2305
|
+
return replica;
|
|
2306
|
+
};
|
|
2307
|
+
return {
|
|
2308
|
+
group,
|
|
2309
|
+
clientIp,
|
|
2310
|
+
observerIp,
|
|
2311
|
+
members: parts.members,
|
|
2312
|
+
sentinels: parts.sentinels,
|
|
2313
|
+
netns: {
|
|
2314
|
+
client: clientNetns,
|
|
2315
|
+
watched
|
|
2316
|
+
},
|
|
2317
|
+
kernel: records.seen,
|
|
2318
|
+
timeline: timeline.seen,
|
|
2319
|
+
async sysctl(values) {
|
|
2320
|
+
const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
|
|
2321
|
+
if (settings.length > 0) await output(admin, [
|
|
2322
|
+
"sysctl",
|
|
2323
|
+
"-q",
|
|
2324
|
+
"-w",
|
|
2325
|
+
...settings
|
|
2326
|
+
]);
|
|
2327
|
+
const read = {};
|
|
2328
|
+
for (const key of Object.keys(values)) read[key] = await output(admin, [
|
|
2329
|
+
"sysctl",
|
|
2330
|
+
"-n",
|
|
2331
|
+
key
|
|
2332
|
+
]);
|
|
2333
|
+
return read;
|
|
2334
|
+
},
|
|
2335
|
+
async converge(within = CONVERGE_MS) {
|
|
2336
|
+
const started = Date.now();
|
|
2337
|
+
let calm = 0;
|
|
2338
|
+
let last = {
|
|
2339
|
+
primary: null,
|
|
2340
|
+
healthy: false,
|
|
2341
|
+
why: "no view yet"
|
|
2342
|
+
};
|
|
2343
|
+
while (Date.now() - started < within) {
|
|
2344
|
+
last = await view().catch((error) => ({
|
|
2345
|
+
primary: null,
|
|
2346
|
+
healthy: false,
|
|
2347
|
+
why: error.message
|
|
2348
|
+
}));
|
|
2349
|
+
calm = last.healthy ? calm + 1 : 0;
|
|
2350
|
+
if (calm >= 2 && last.primary !== null) return last.primary;
|
|
2351
|
+
await sleep$1(1e3);
|
|
2352
|
+
}
|
|
2353
|
+
throw new Error(`the group did not converge in ${within} ms: ${last.why}`);
|
|
2354
|
+
},
|
|
2355
|
+
async flush(primary) {
|
|
2356
|
+
await cli(primary, ["FLUSHALL"]);
|
|
2357
|
+
const acked = Number(await cli(primary, [
|
|
2358
|
+
"WAIT",
|
|
2359
|
+
String(group.members.length - 1),
|
|
2360
|
+
"5000"
|
|
2361
|
+
]));
|
|
2362
|
+
if (acked !== group.members.length - 1) throw new Error(`${acked} replicas acknowledged the flush`);
|
|
2363
|
+
},
|
|
2364
|
+
async reset() {
|
|
2365
|
+
for (const name of group.sentinels) await cli(name, [
|
|
2366
|
+
"SENTINEL",
|
|
2367
|
+
"RESET",
|
|
2368
|
+
group.name
|
|
2369
|
+
]);
|
|
2370
|
+
},
|
|
2371
|
+
async holds(primary) {
|
|
2372
|
+
return (await cli(primary, [
|
|
2373
|
+
"--scan",
|
|
2374
|
+
"--pattern",
|
|
2375
|
+
"w:*"
|
|
2376
|
+
])).split("\n").map((key) => Number(key.trim().slice(2))).filter((n) => Number.isInteger(n) && n > 0).sort((a, b) => a - b);
|
|
2377
|
+
},
|
|
2378
|
+
partOf: roles,
|
|
2379
|
+
async client(args) {
|
|
2380
|
+
const events = lines();
|
|
2381
|
+
const container = await new GenericContainer(node).withNetworkMode(`container:${holder.getId()}`).withCommand([
|
|
2382
|
+
"node",
|
|
2383
|
+
"/opt/harness/node/client-sentinel.mjs",
|
|
2384
|
+
...args
|
|
2385
|
+
]).withLogConsumer(events.consume).start();
|
|
2386
|
+
return {
|
|
2387
|
+
events: events.seen,
|
|
2388
|
+
until: (kinds, within) => events.until(kinds, within, "client"),
|
|
2389
|
+
stop: () => container.stop({ timeout: 2 }).then(() => void 0).catch(() => void 0)
|
|
2390
|
+
};
|
|
2391
|
+
},
|
|
2392
|
+
fault(kind, part, cuts, at) {
|
|
2393
|
+
const peers = peersOf(parts, clientIp, part, cuts).join(",");
|
|
2394
|
+
return stamped(injectorOf(part), [
|
|
2395
|
+
"/opt/harness/inject",
|
|
2396
|
+
kind,
|
|
2397
|
+
peers,
|
|
2398
|
+
...after(at)
|
|
2399
|
+
]);
|
|
2400
|
+
},
|
|
2401
|
+
lift(kind, part, cuts, at) {
|
|
2402
|
+
const peers = peersOf(parts, clientIp, part, cuts).join(",");
|
|
2403
|
+
return stamped(injectorOf(part), [
|
|
2404
|
+
"/opt/harness/inject",
|
|
2405
|
+
kind === "rst" ? "clear-rst" : "clear",
|
|
2406
|
+
peers,
|
|
2407
|
+
...after(at)
|
|
2408
|
+
]);
|
|
2409
|
+
},
|
|
2410
|
+
signal(part, signal, at) {
|
|
2411
|
+
const program = group.sentinels.includes(part) ? group.programs.sentinel : group.programs.server;
|
|
2412
|
+
return stamped(injectorOf(part), [
|
|
2413
|
+
"/opt/harness/inject",
|
|
2414
|
+
"signal",
|
|
2415
|
+
program,
|
|
2416
|
+
signal,
|
|
2417
|
+
...after(at)
|
|
2418
|
+
]);
|
|
2419
|
+
},
|
|
2420
|
+
async restart(member) {
|
|
2421
|
+
await output(box(member), [
|
|
2422
|
+
"sh",
|
|
2423
|
+
"-c",
|
|
2424
|
+
`${group.programs.server} /data/server.conf --daemonize yes`
|
|
2425
|
+
]);
|
|
2426
|
+
const started = Date.now();
|
|
2427
|
+
while (await cli(member, ["PING"]).catch(() => "") !== "PONG") {
|
|
2428
|
+
if (Date.now() - started > 3e4) throw new Error(`${member} did not start again`);
|
|
2429
|
+
await sleep$1(200);
|
|
2430
|
+
}
|
|
2431
|
+
},
|
|
2432
|
+
failover(coordinated) {
|
|
2433
|
+
const sentinel = group.sentinels[0];
|
|
2434
|
+
return stamped(injectorOf(sentinel), [
|
|
2435
|
+
"/opt/harness/inject",
|
|
2436
|
+
"resp",
|
|
2437
|
+
`127.0.0.1:${group.sentinelPort}`,
|
|
2438
|
+
"SENTINEL",
|
|
2439
|
+
"FAILOVER",
|
|
2440
|
+
group.name,
|
|
2441
|
+
...coordinated ? ["COORDINATED"] : []
|
|
2442
|
+
]);
|
|
2443
|
+
},
|
|
2444
|
+
async forget() {
|
|
2445
|
+
await output(admin, [
|
|
2446
|
+
"ip",
|
|
2447
|
+
"tcp_metrics",
|
|
2448
|
+
"flush",
|
|
2449
|
+
"all"
|
|
2450
|
+
]);
|
|
2451
|
+
},
|
|
2452
|
+
async settle() {
|
|
2453
|
+
const from = records.seen.length;
|
|
2454
|
+
const clock = await new Promise((resolve) => {
|
|
2455
|
+
const started = Date.now();
|
|
2456
|
+
const look = () => {
|
|
2457
|
+
const line = records.seen.slice(from).find((record) => record.ev === "clock");
|
|
2458
|
+
if (line !== void 0) resolve(line);
|
|
2459
|
+
else if (Date.now() - started > 5e3) resolve(null);
|
|
2460
|
+
else setTimeout(look, 50);
|
|
2461
|
+
};
|
|
2462
|
+
look();
|
|
2463
|
+
});
|
|
2464
|
+
return clock === null ? null : Number(clock["lost"] ?? 0);
|
|
2465
|
+
},
|
|
2466
|
+
async manifest(sysctls) {
|
|
2467
|
+
const config = await output(admin, ["zcat", "/proc/config.gz"]).catch(() => "");
|
|
2468
|
+
const inspect = async (container) => runtime.container.inspect(runtime.container.getById(container.getId()));
|
|
2469
|
+
const memberSettings = async (name) => {
|
|
2470
|
+
const read = {};
|
|
2471
|
+
for (const key of Object.keys(group.memberSettings)) read[key] = (await cli(name, [
|
|
2472
|
+
"CONFIG",
|
|
2473
|
+
"GET",
|
|
2474
|
+
key
|
|
2475
|
+
])).split("\n")[1] ?? "";
|
|
2476
|
+
return read;
|
|
2477
|
+
};
|
|
2478
|
+
const sentinelSettings = async (name) => {
|
|
2479
|
+
const master = fieldsOf(await cli(name, [
|
|
2480
|
+
"SENTINEL",
|
|
2481
|
+
"MASTER",
|
|
2482
|
+
group.name
|
|
2483
|
+
]))[0] ?? {};
|
|
2484
|
+
const read = { quorum: master["quorum"] ?? "" };
|
|
2485
|
+
for (const key of Object.keys(group.sentinelSettings)) read[key] = master[key] ?? "";
|
|
2486
|
+
return read;
|
|
2487
|
+
};
|
|
2488
|
+
const members = [];
|
|
2489
|
+
for (const name of group.members) members.push({
|
|
2490
|
+
name,
|
|
2491
|
+
ip: parts.members[name].ip,
|
|
2492
|
+
id: (await inspect(box(name))).Image,
|
|
2493
|
+
settings: await memberSettings(name)
|
|
2494
|
+
});
|
|
2495
|
+
const sentinels = [];
|
|
2496
|
+
for (const name of group.sentinels) sentinels.push({
|
|
2497
|
+
name,
|
|
2498
|
+
ip: parts.sentinels[name].ip,
|
|
2499
|
+
id: (await inspect(box(name))).Image,
|
|
2500
|
+
settings: await sentinelSettings(name)
|
|
2501
|
+
});
|
|
2502
|
+
return {
|
|
2503
|
+
kernel: await output(holder, ["uname", "-r"]),
|
|
2504
|
+
architecture: await output(holder, ["uname", "-m"]),
|
|
2505
|
+
hz: /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1] ?? null,
|
|
2506
|
+
boot_id: await output(holder, ["cat", "/proc/sys/kernel/random/boot_id"]),
|
|
2507
|
+
docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
|
|
2508
|
+
target: {
|
|
2509
|
+
image: group.image,
|
|
2510
|
+
id: members[0].id,
|
|
2511
|
+
settings: members[0].settings,
|
|
2512
|
+
sysctls: {}
|
|
2513
|
+
},
|
|
2514
|
+
harness: {
|
|
2515
|
+
image,
|
|
2516
|
+
id: (await inspect(holder)).Image,
|
|
2517
|
+
packages: (await output(holder, [
|
|
2518
|
+
"apk",
|
|
2519
|
+
"info",
|
|
2520
|
+
"-v"
|
|
2521
|
+
])).split("\n").sort()
|
|
2522
|
+
},
|
|
2523
|
+
client: {
|
|
2524
|
+
kind: "ioredis",
|
|
2525
|
+
image: node
|
|
2526
|
+
},
|
|
2527
|
+
netns: {
|
|
2528
|
+
client: clientNetns,
|
|
2529
|
+
target: parts.members[first].netns
|
|
2530
|
+
},
|
|
2531
|
+
sysctls,
|
|
2532
|
+
group: {
|
|
2533
|
+
members,
|
|
2534
|
+
sentinels,
|
|
2535
|
+
observer: { image: node }
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
},
|
|
2539
|
+
close
|
|
2540
|
+
};
|
|
2541
|
+
} catch (error) {
|
|
2542
|
+
await close();
|
|
2543
|
+
throw error;
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
/** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
|
|
2547
|
+
const STEADY = 20;
|
|
2548
|
+
/** This time is how long a run continues after its outcome, so that the record shows what came next. */
|
|
2549
|
+
const TAIL_MS = 2e3;
|
|
2550
|
+
/** This function gives the address of a member, with the port of the group. */
|
|
2551
|
+
const addressOf = (lab, member) => `${lab.members[member].ip}:${lab.group.port}`;
|
|
2552
|
+
/** This function gives the latest time that a group cell waits for its outcome, in ms from the fault. */
|
|
2553
|
+
const deadlineOf = (cell) => {
|
|
2554
|
+
const { prediction } = cell;
|
|
2555
|
+
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;
|
|
2556
|
+
};
|
|
2557
|
+
/** This function gives the arguments of the client of a group cell. */
|
|
2558
|
+
const clientArgs = (cell, lab) => {
|
|
2559
|
+
return [
|
|
2560
|
+
"--sentinels",
|
|
2561
|
+
cell.group.sentinels.map((name) => `${lab.sentinels[name].ip}:${cell.group.sentinelPort}`).join(","),
|
|
2562
|
+
"--name",
|
|
2563
|
+
cell.group.name,
|
|
2564
|
+
"--interval-ms",
|
|
2565
|
+
String(cell.probe.interval === void 0 ? 50 : valueIn(cell.probe.interval, "ms")),
|
|
2566
|
+
"--max-s",
|
|
2567
|
+
String(Math.ceil(deadlineOf(cell) / 1e3 + 120)),
|
|
2568
|
+
...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)]
|
|
2569
|
+
];
|
|
2570
|
+
};
|
|
2571
|
+
/** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
|
|
2572
|
+
const waitFor = (client, match, within) => new Promise((resolve) => {
|
|
2573
|
+
const started = Date.now();
|
|
2574
|
+
const look = () => {
|
|
2575
|
+
const found = client.events.find(match);
|
|
2576
|
+
if (found !== void 0 || Date.now() - started > within) resolve(found);
|
|
2577
|
+
else setTimeout(look, 20);
|
|
2578
|
+
};
|
|
2579
|
+
look();
|
|
2580
|
+
});
|
|
2581
|
+
/** This function gives the latest subscription of the observer to each Sentinel, in some lines of the observer. */
|
|
2582
|
+
const subscriptionsOf = (timeline) => {
|
|
2583
|
+
const latest = /* @__PURE__ */ new Map();
|
|
2584
|
+
for (const line of timeline) if (line.ev === "subscribed") latest.set(String(line.sentinel), line);
|
|
2585
|
+
return [...latest.values()];
|
|
2586
|
+
};
|
|
2587
|
+
/** This function gives the count of lost records in the latest clock line of the prober. */
|
|
2588
|
+
const droppedSoFar$1 = (records) => {
|
|
2589
|
+
for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
|
|
2590
|
+
return 0;
|
|
2591
|
+
};
|
|
2592
|
+
/**
|
|
2593
|
+
* This function does one run of a group cell:
|
|
2594
|
+
* - The group converges, and its keys go. The client connects through the Sentinels, and its
|
|
2595
|
+
* stream of writes gets steady.
|
|
2596
|
+
* - The fault occurs on its part, as the primary of this run makes the roles. A fault with a
|
|
2597
|
+
* hold lifts at its instant in the VM. A stall lifts at its length after the first write into it.
|
|
2598
|
+
* - The run waits for its outcome: the first write that a new primary accepts, or the first one
|
|
2599
|
+
* after the lift on the same primary. Then it continues for a moment.
|
|
2600
|
+
* - Each fault goes, the client stops, and the group converges again. The keys that it then holds
|
|
2601
|
+
* tell which writes it kept.
|
|
2602
|
+
*/
|
|
2603
|
+
const groupOnce = async (lab, cell, environment, index) => {
|
|
2604
|
+
const group = cell.group;
|
|
2605
|
+
const primary = await lab.converge();
|
|
2606
|
+
await lab.flush(primary);
|
|
2607
|
+
await lab.forget();
|
|
2608
|
+
const from = lab.kernel.length;
|
|
2609
|
+
const timelineFrom = lab.timeline.length;
|
|
2610
|
+
const lostBefore = droppedSoFar$1(lab.kernel);
|
|
2611
|
+
const client = await lab.client(clientArgs(cell, lab));
|
|
2612
|
+
const lands = cell.prediction.group?.lands ?? "new";
|
|
2613
|
+
const place = cell.faultAt ?? { role: "primary" };
|
|
2614
|
+
let fault = null;
|
|
2615
|
+
let heal = null;
|
|
2616
|
+
let at = null;
|
|
2617
|
+
const undo = [];
|
|
2618
|
+
try {
|
|
2619
|
+
await client.until(["ready"], 6e4);
|
|
2620
|
+
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`);
|
|
2621
|
+
let healing = null;
|
|
2622
|
+
switch (cell.fault) {
|
|
2623
|
+
case "blackhole":
|
|
2624
|
+
case "partition":
|
|
2625
|
+
case "rst": {
|
|
2626
|
+
const kind = cell.fault === "rst" ? "rst" : "blackhole";
|
|
2627
|
+
const cuts = place.cuts ?? (cell.fault === "partition" ? "group" : "all");
|
|
2628
|
+
const part = lab.partOf(place.role, primary);
|
|
2629
|
+
at = part;
|
|
2630
|
+
fault = await lab.fault(kind, part, cuts);
|
|
2631
|
+
undo.push(() => lab.lift(kind, part, cuts));
|
|
2632
|
+
if (cell.stall !== void 0) {
|
|
2633
|
+
const t1 = fault.t1;
|
|
2634
|
+
const sent = await waitFor(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
|
|
2635
|
+
if (sent !== void 0) healing = lab.lift(kind, part, cuts, sent.t + valueIn(cell.stall, "s") * 1e9);
|
|
2636
|
+
} else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
|
|
2637
|
+
break;
|
|
2638
|
+
}
|
|
2639
|
+
case "freeze-process": {
|
|
2640
|
+
const part = lab.partOf(place.role, primary);
|
|
2641
|
+
at = part;
|
|
2642
|
+
fault = await lab.signal(part, "STOP");
|
|
2643
|
+
undo.push(() => lab.signal(part, "CONT"));
|
|
2644
|
+
if (cell.hold !== void 0) healing = lab.signal(part, "CONT", fault.t0 + valueIn(cell.hold, "s") * 1e9);
|
|
2645
|
+
break;
|
|
2646
|
+
}
|
|
2647
|
+
case "fin": {
|
|
2648
|
+
const part = lab.partOf(place.role, primary);
|
|
2649
|
+
at = part;
|
|
2650
|
+
fault = await lab.signal(part, "KILL");
|
|
2651
|
+
if (group.members.includes(part)) undo.push(() => lab.restart(part));
|
|
2652
|
+
break;
|
|
2653
|
+
}
|
|
2654
|
+
case "planned-failover":
|
|
2655
|
+
fault = await lab.failover(cell.coordinated === true);
|
|
2656
|
+
break;
|
|
2657
|
+
default: throw new Error(`a group cell has no fault ${cell.fault}`);
|
|
2658
|
+
}
|
|
2659
|
+
const healed = healing === null ? Promise.resolve(null) : healing.then((stamp) => {
|
|
2660
|
+
heal = stamp;
|
|
2661
|
+
undo.length = 0;
|
|
2662
|
+
return stamp;
|
|
2663
|
+
});
|
|
2664
|
+
healed.catch(() => void 0);
|
|
2665
|
+
const t0 = fault.t0;
|
|
2666
|
+
const old = addressOf(lab, primary);
|
|
2667
|
+
const deadline = deadlineOf(cell);
|
|
2668
|
+
if ((lands === "new" ? await waitFor(client, (event) => event.ev === "ack" && event.t > t0 && typeof event["at"] === "string" && event["at"] !== old, deadline) : await (async () => {
|
|
2669
|
+
await healed;
|
|
2670
|
+
const after = heal?.t0 ?? t0;
|
|
2671
|
+
return waitFor(client, (event) => event.ev === "ack" && event.t > after, deadline);
|
|
2672
|
+
})()) !== void 0) await sleep$1(TAIL_MS);
|
|
2673
|
+
await healed;
|
|
2674
|
+
} finally {
|
|
2675
|
+
for (const step of undo.reverse()) await step().catch(() => void 0);
|
|
2676
|
+
await client.stop();
|
|
2677
|
+
}
|
|
2678
|
+
const settled = await lab.settle();
|
|
2679
|
+
const lost = settled === null ? null : settled - lostBefore;
|
|
2680
|
+
const after = await lab.converge().catch(() => null);
|
|
2681
|
+
const holds = after === null ? null : await lab.holds(after);
|
|
2682
|
+
if (lab.timeline.slice(timelineFrom).some((line) => line.ev === "sentinel" && String(line.event).startsWith("-failover-abort"))) await lab.reset();
|
|
2683
|
+
return {
|
|
2684
|
+
environment,
|
|
2685
|
+
index,
|
|
2686
|
+
events: [...client.events],
|
|
2687
|
+
fault,
|
|
2688
|
+
...heal === null ? {} : { heal },
|
|
2689
|
+
kernel: lab.kernel.slice(from),
|
|
2690
|
+
netns: lab.netns.client,
|
|
2691
|
+
targetNetns: lab.members[primary].netns,
|
|
2692
|
+
lost,
|
|
2693
|
+
group: {
|
|
2694
|
+
members: lab.members,
|
|
2695
|
+
sentinels: lab.sentinels,
|
|
2696
|
+
primary,
|
|
2697
|
+
at,
|
|
2698
|
+
timeline: [...subscriptionsOf(lab.timeline.slice(0, timelineFrom)), ...lab.timeline.slice(timelineFrom)],
|
|
2699
|
+
after,
|
|
2700
|
+
holds
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
};
|
|
2704
|
+
//#endregion
|
|
1550
2705
|
//#region src/run.ts
|
|
1551
2706
|
/**
|
|
1552
2707
|
* This function measures and judges the raw records of a cell, with the code of the harness at
|
|
@@ -1575,6 +2730,18 @@ const probeArgs = (cell, lab) => {
|
|
|
1575
2730
|
"--mode",
|
|
1576
2731
|
probe.mode
|
|
1577
2732
|
];
|
|
2733
|
+
if (probe.http !== void 0) {
|
|
2734
|
+
const { http } = probe;
|
|
2735
|
+
const gap = http.gap === void 0 ? 0 : valueIn(http.gap, "s");
|
|
2736
|
+
args.push("--method", http.method);
|
|
2737
|
+
if (http.gap !== void 0) args.push("--gap-ms", String(valueIn(http.gap, "ms")));
|
|
2738
|
+
if (http.hold !== void 0) args.push("--hold-ms", String(valueIn(http.hold, "ms")));
|
|
2739
|
+
if (http.bytes !== void 0) args.push("--bytes", String(http.bytes));
|
|
2740
|
+
for (const [name, value] of Object.entries(http.options ?? {})) args.push(`--${name}`, String(value));
|
|
2741
|
+
args.push("--linger-ms", String(LINGER_MS));
|
|
2742
|
+
args.push("--max-s", String(Math.ceil(longest * 2 + gap + 10)));
|
|
2743
|
+
return args;
|
|
2744
|
+
}
|
|
1578
2745
|
if (probe.request !== void 0) args.push("--request", probe.request);
|
|
1579
2746
|
if (probe.count !== void 0) args.push("--count", String(probe.count));
|
|
1580
2747
|
if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
|
|
@@ -1605,6 +2772,22 @@ const observerError = (cell, partial, within) => new Promise((resolve) => {
|
|
|
1605
2772
|
});
|
|
1606
2773
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1607
2774
|
/**
|
|
2775
|
+
* This time is the pause between the client's first reply and the fault. It is more than Linux's
|
|
2776
|
+
* TCP_DELACK_MAX of 200 ms. Thus the delayed ACK of the reply is out before the fault.
|
|
2777
|
+
*/
|
|
2778
|
+
const QUIET_MS = 250;
|
|
2779
|
+
/**
|
|
2780
|
+
* This time is how long an HTTP client waits after its second request, before it exits. The exit
|
|
2781
|
+
* closes its sockets, thus that close comes after the window of the cell.
|
|
2782
|
+
*/
|
|
2783
|
+
const LINGER_MS = 1e3;
|
|
2784
|
+
/** These are the lines that end the workload of an HTTP client: the end of its second request, its deadline, or its failure. */
|
|
2785
|
+
const HTTP_DONE = [
|
|
2786
|
+
"done",
|
|
2787
|
+
"deadline",
|
|
2788
|
+
"fail"
|
|
2789
|
+
];
|
|
2790
|
+
/**
|
|
1608
2791
|
* This function waits for the request that an in-flight client sends on the signal: its second
|
|
1609
2792
|
* send, after the PING that made it ready. Its stamp is when the request first met the fault.
|
|
1610
2793
|
*/
|
|
@@ -1640,6 +2823,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
1640
2823
|
await lab.forget();
|
|
1641
2824
|
const from = lab.kernel.length;
|
|
1642
2825
|
const middleboxFrom = lab.middlebox.length;
|
|
2826
|
+
const serverFrom = lab.server.length;
|
|
1643
2827
|
const lostBefore = droppedSoFar(lab.kernel);
|
|
1644
2828
|
const probe = await lab.probe(probeArgs(cell, lab));
|
|
1645
2829
|
const server = cell.observer === "server";
|
|
@@ -1681,7 +2865,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
1681
2865
|
await sleep(200);
|
|
1682
2866
|
} else await sleep(250);
|
|
1683
2867
|
const faultAt = cell.faultAt ?? (server ? "client" : "target");
|
|
1684
|
-
const towardServerComesIn = faultAt === "target" ||
|
|
2868
|
+
const towardServerComesIn = faultAt === "target" || isHopPlace(faultAt) && faultAt.toward === "client";
|
|
1685
2869
|
const direction = cell.drops === void 0 ? void 0 : cell.drops === "to-server" === towardServerComesIn ? "in" : "out";
|
|
1686
2870
|
const options = {
|
|
1687
2871
|
...direction === void 0 ? {} : { direction },
|
|
@@ -1702,11 +2886,12 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
1702
2886
|
return null;
|
|
1703
2887
|
case "unreachable": return lab.fault("reject", faultAt, { code: cell.icmp ?? "host" });
|
|
1704
2888
|
case "simplex": return lab.fault("blackhole", faultAt, options);
|
|
1705
|
-
case "mtu-blackhole": return lab.fault("mtu", "client", { above: cell.mtu ?? 1200 });
|
|
2889
|
+
case "mtu-blackhole": return lab.fault("mtu", cell.faultAt ?? "client", { above: cell.mtu ?? 1200 });
|
|
1706
2890
|
case "netem": return netemOn(cell.netem);
|
|
1707
2891
|
case "rst": return lab.fault("rst", faultAt);
|
|
1708
2892
|
case "middlebox-forget":
|
|
1709
2893
|
case "proxy-idle":
|
|
2894
|
+
case "app-stall":
|
|
1710
2895
|
case "none": return null;
|
|
1711
2896
|
case "toxic":
|
|
1712
2897
|
if (cell.toxic === void 0) throw new Error(`${cell.name} names no toxic`);
|
|
@@ -1724,10 +2909,11 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
1724
2909
|
case "middlebox-forget":
|
|
1725
2910
|
case "proxy-idle":
|
|
1726
2911
|
case "server-close":
|
|
2912
|
+
case "app-stall":
|
|
1727
2913
|
case "none": return;
|
|
1728
2914
|
case "unreachable": return lab.lift("reject", faultAt, { code: cell.icmp ?? "host" });
|
|
1729
2915
|
case "simplex": return lab.lift("blackhole", faultAt, options);
|
|
1730
|
-
case "mtu-blackhole": return lab.lift("mtu", "client", { above: cell.mtu ?? 1200 });
|
|
2916
|
+
case "mtu-blackhole": return lab.lift("mtu", cell.faultAt ?? "client", { above: cell.mtu ?? 1200 });
|
|
1731
2917
|
case "netem":
|
|
1732
2918
|
await netemOn(void 0);
|
|
1733
2919
|
return;
|
|
@@ -1741,9 +2927,14 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
1741
2927
|
fault = await apply();
|
|
1742
2928
|
try {
|
|
1743
2929
|
if (cell.idleBefore !== void 0 && cell.probe.mode !== "idle") await sleep(valueIn(cell.idleBefore, "ms"));
|
|
1744
|
-
if (cell.probe.mode
|
|
2930
|
+
if ((cell.probe.mode === "inflight" || cell.probe.mode === "stream") && !replyInFlight) await go();
|
|
1745
2931
|
const { prediction } = cell;
|
|
1746
|
-
|
|
2932
|
+
const http = cell.probe.http !== void 0;
|
|
2933
|
+
if (cell.probe.mode === "gap") {
|
|
2934
|
+
const gap = cell.probe.http?.gap === void 0 ? 0 : valueIn(cell.probe.http.gap, "s");
|
|
2935
|
+
const window = valueIn(prediction.detects ? prediction.high : prediction.window, "s");
|
|
2936
|
+
await probe.until(HTTP_DONE, (gap + window) * 2e3 + 1e4).catch(() => void 0);
|
|
2937
|
+
} else if (cell.fault === "stall") {
|
|
1747
2938
|
if (cell.stall === void 0) throw new Error(`${cell.name} says no stall's length`);
|
|
1748
2939
|
const sent = await requestSent(probe, 5e3);
|
|
1749
2940
|
heal = await lab.heal(faultAt, sent.t + valueIn(cell.stall, "s") * 1e9);
|
|
@@ -1764,10 +2955,12 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
1764
2955
|
} else if (prediction.detects) {
|
|
1765
2956
|
const within = valueIn(prediction.high, "s") * 2e3 + 1e4;
|
|
1766
2957
|
if (cell.observer !== void 0 && cell.observer !== "client") await observerError(cell, partial, within);
|
|
2958
|
+
else if (http && cell.probe.mode !== "idle") await probe.until(HTTP_DONE, within).catch(() => void 0);
|
|
1767
2959
|
else await probe.until([
|
|
1768
2960
|
"error",
|
|
1769
2961
|
"eof",
|
|
1770
|
-
"deadline"
|
|
2962
|
+
"deadline",
|
|
2963
|
+
...http ? ["closed"] : []
|
|
1771
2964
|
], within).catch(() => void 0);
|
|
1772
2965
|
} else await sleep(valueIn(prediction.window, "s") * 1e3);
|
|
1773
2966
|
} finally {
|
|
@@ -1792,7 +2985,8 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
1792
2985
|
targetNetns: lab.netns.target,
|
|
1793
2986
|
lost,
|
|
1794
2987
|
...placed,
|
|
1795
|
-
...lab.topology === null ? {} : { middlebox: lab.middlebox.slice(middleboxFrom) }
|
|
2988
|
+
...lab.topology === null ? {} : { middlebox: lab.middlebox.slice(middleboxFrom) },
|
|
2989
|
+
...cell.target.lines === true ? { server: lab.server.slice(serverFrom) } : {}
|
|
1796
2990
|
};
|
|
1797
2991
|
};
|
|
1798
2992
|
/** This function divides a count of runs across environments, as equally as possible. */
|
|
@@ -1808,6 +3002,7 @@ const line = (cell, run, read) => `${cell.name}: env ${run.environment} ${run.wa
|
|
|
1808
3002
|
* of a cell whose fault ends the environment is a dry run, without the fault.
|
|
1809
3003
|
*/
|
|
1810
3004
|
const inEnvironment = async (cell, environment, count, say) => {
|
|
3005
|
+
if (cell.group !== void 0) return inGroupEnvironment(cell, environment, count, say);
|
|
1811
3006
|
const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
|
|
1812
3007
|
try {
|
|
1813
3008
|
const settings = await lab.configure(cell.targetSettings ?? {});
|
|
@@ -1816,7 +3011,7 @@ const inEnvironment = async (cell, environment, count, say) => {
|
|
|
1816
3011
|
if (cell.fault === "mtu-blackhole" || cell.fault === "netem") {
|
|
1817
3012
|
await lab.offload(false, "target");
|
|
1818
3013
|
await lab.offload(false, "client");
|
|
1819
|
-
if (
|
|
3014
|
+
if (isHopPlace(cell.faultAt)) await lab.offload(false, cell.faultAt);
|
|
1820
3015
|
}
|
|
1821
3016
|
const latency = netemSpec(cell, void 0);
|
|
1822
3017
|
if (latency !== null) await lab.netem(latency, "target");
|
|
@@ -1843,6 +3038,35 @@ const inEnvironment = async (cell, environment, count, say) => {
|
|
|
1843
3038
|
}
|
|
1844
3039
|
};
|
|
1845
3040
|
/**
|
|
3041
|
+
* This function does the part of the runs of a group cell in one environment. The warm-up is a
|
|
3042
|
+
* whole run with its fault, because no fault of a group ends its environment. A server that a
|
|
3043
|
+
* fin stopped starts again, and the group converges before each run.
|
|
3044
|
+
*/
|
|
3045
|
+
const inGroupEnvironment = async (cell, environment, count, say) => {
|
|
3046
|
+
const lab = await openGroupLab(cell.group, { watchSentinels: cell.calibrates === "sentinels" });
|
|
3047
|
+
try {
|
|
3048
|
+
const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
|
|
3049
|
+
const warmup = {
|
|
3050
|
+
...await groupOnce(lab, cell, environment, -1),
|
|
3051
|
+
warmup: true
|
|
3052
|
+
};
|
|
3053
|
+
say(line(cell, warmup, measure(cell, warmup)));
|
|
3054
|
+
const runs = [];
|
|
3055
|
+
for (let index = 0; index < count; index += 1) {
|
|
3056
|
+
const run = await groupOnce(lab, cell, environment, index);
|
|
3057
|
+
runs.push(run);
|
|
3058
|
+
say(line(cell, run, measure(cell, run)));
|
|
3059
|
+
}
|
|
3060
|
+
return {
|
|
3061
|
+
manifest,
|
|
3062
|
+
runs,
|
|
3063
|
+
warmup
|
|
3064
|
+
};
|
|
3065
|
+
} finally {
|
|
3066
|
+
await lab.close();
|
|
3067
|
+
}
|
|
3068
|
+
};
|
|
3069
|
+
/**
|
|
1846
3070
|
* This function does the runs of a cell. They go across new environments, and each environment records
|
|
1847
3071
|
* its manifest. Then the verdict judges the runs. A median across the edge of the band extends
|
|
1848
3072
|
* the cell from 11 runs to 22, and then to 44, each extension in new environments. With a
|
|
@@ -1891,6 +3115,6 @@ const runCell = async (cell, say = () => {}) => {
|
|
|
1891
3115
|
}
|
|
1892
3116
|
};
|
|
1893
3117
|
//#endregion
|
|
1894
|
-
export { LISTING, NOISE, PUMP, RELATIVE, STEPS, crossingsOf, deviceOf, endpointAfter, harnessImage, judge, measure, medianInterval, nodeClientImage, openLab, quantileOf, reorderingOf, routerImage, routesOf, runCell, socketOf, spread, spreadOf, templated, toxicBody, verdictOf };
|
|
3118
|
+
export { CONVERGE_MS, LINGER_MS, LISTING, NOISE, PUMP, QUIET_MS, RELATIVE, STAGES, STEPS, crossingsOf, deadlineOf, deviceOf, endpointAfter, errorsOf, fieldsOf, goHttpImage, groupOnce, groupVerdictOf, harnessImage, infoOf, isHopPlace, judge, lagsOf, measure, measureGroup, medianInterval, memberConfig, nodeClientImage, nodeHttpImage, openGroupLab, openLab, peersOf, quantileOf, reorderingOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketOf, spread, spreadOf, stagesOf, subscriptionsOf, templated, toxicBody, verdictOf, viewOf, writesOf };
|
|
1895
3119
|
|
|
1896
3120
|
//# sourceMappingURL=index.js.map
|