@mark1russell7/harness 0.4.0 → 0.5.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 +56 -3
- package/build/index.d.ts +466 -8
- package/build/index.js +1089 -13
- package/build/index.js.map +1 -1
- 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/package.json +6 -4
package/build/index.js
CHANGED
|
@@ -5,6 +5,10 @@ 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
|
|
9
13
|
/**
|
|
10
14
|
* This amount is the noise of the pipeline itself, δ_abs, as the calibration measured it. It is
|
|
@@ -111,6 +115,94 @@ const middle = (sorted) => {
|
|
|
111
115
|
* which does not exist at this time.
|
|
112
116
|
*/
|
|
113
117
|
const verdictOf = (prediction, runs) => {
|
|
118
|
+
const verdict = verdictOfBand(prediction, runs);
|
|
119
|
+
const counted = runs.filter((run) => run.void === null);
|
|
120
|
+
if (prediction.group === void 0 && !counted.some((run) => run.group !== void 0)) return verdict;
|
|
121
|
+
return {
|
|
122
|
+
...verdict,
|
|
123
|
+
group: groupVerdictOf(prediction, counted)
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
/** This function gives the median of some counts, or null for none. */
|
|
127
|
+
const medianCount = (values) => values.length === 0 ? null : middle([...values].sort((a, b) => a - b));
|
|
128
|
+
/** This function sums what the group of a group cell did in its counted runs, and tests each stage that has a band. */
|
|
129
|
+
const groupVerdictOf = (prediction, counted) => {
|
|
130
|
+
const noise = valueIn(NOISE, "s");
|
|
131
|
+
const stamps = prediction.group?.stamps === void 0 ? 0 : valueIn(prediction.group.stamps, "s");
|
|
132
|
+
const stages = (prediction.group?.stages ?? []).map((band) => {
|
|
133
|
+
const interval = {
|
|
134
|
+
low: s(valueIn(band.low, "s") - noise),
|
|
135
|
+
high: s(valueIn(band.high, "s") + stamps + noise)
|
|
136
|
+
};
|
|
137
|
+
const outside = [];
|
|
138
|
+
const values = [];
|
|
139
|
+
for (const run of counted) {
|
|
140
|
+
const at = run.group?.stages[band.stage];
|
|
141
|
+
if (at === void 0) {
|
|
142
|
+
outside.push({
|
|
143
|
+
run: name(run),
|
|
144
|
+
why: `no Sentinel announced ${band.stage}`
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const value = valueIn(at, "s");
|
|
149
|
+
values.push(value);
|
|
150
|
+
if (value < valueIn(interval.low, "s") || value > valueIn(interval.high, "s")) outside.push({
|
|
151
|
+
run: name(run),
|
|
152
|
+
why: `${band.stage} at ${value} s is outside [${interval.low.value}, ${interval.high.value}] s`
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
values.sort((a, b) => a - b);
|
|
156
|
+
return {
|
|
157
|
+
stage: band.stage,
|
|
158
|
+
band: interval,
|
|
159
|
+
median: values.length === 0 ? null : s(middle(values)),
|
|
160
|
+
outside,
|
|
161
|
+
decision: outside.length === 0 && values.length > 0 ? "holds" : "fails"
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
const medians = {};
|
|
165
|
+
const seen = new Set(counted.flatMap((run) => Object.keys(run.group?.stages ?? {})));
|
|
166
|
+
for (const stage of seen) {
|
|
167
|
+
const values = counted.flatMap((run) => {
|
|
168
|
+
const at = run.group?.stages[stage];
|
|
169
|
+
return at === void 0 ? [] : [valueIn(at, "s")];
|
|
170
|
+
});
|
|
171
|
+
medians[stage] = s(middle(values.sort((a, b) => a - b)));
|
|
172
|
+
}
|
|
173
|
+
const lostCounts = counted.flatMap((run) => run.group?.writes.lost === null || run.group === void 0 ? [] : [run.group.writes.lost.length]);
|
|
174
|
+
const oldCounts = counted.flatMap((run) => run.group === void 0 ? [] : [run.group.writes.old]);
|
|
175
|
+
const unknown = counted.flatMap((run) => run.group?.writes.unknown ?? []);
|
|
176
|
+
const errors = /* @__PURE__ */ new Map();
|
|
177
|
+
for (const run of counted) for (const error of run.group?.errors ?? []) errors.set(error.name, [...errors.get(error.name) ?? [], valueIn(error.first, "s")]);
|
|
178
|
+
const lags = counted.flatMap((run) => run.group?.lags ?? []);
|
|
179
|
+
return {
|
|
180
|
+
stages,
|
|
181
|
+
medians,
|
|
182
|
+
lost: {
|
|
183
|
+
runs: lostCounts.length,
|
|
184
|
+
total: lostCounts.reduce((sum, n) => sum + n, 0),
|
|
185
|
+
median: medianCount(lostCounts),
|
|
186
|
+
max: lostCounts.length === 0 ? null : Math.max(...lostCounts)
|
|
187
|
+
},
|
|
188
|
+
old: {
|
|
189
|
+
median: medianCount(oldCounts),
|
|
190
|
+
max: oldCounts.length === 0 ? null : Math.max(...oldCounts)
|
|
191
|
+
},
|
|
192
|
+
unknown: {
|
|
193
|
+
total: unknown.length,
|
|
194
|
+
held: unknown.filter((each) => each.held === true).length
|
|
195
|
+
},
|
|
196
|
+
errors: [...errors].map(([error, firsts]) => ({
|
|
197
|
+
name: error,
|
|
198
|
+
runs: firsts.length,
|
|
199
|
+
first: s(middle(firsts.sort((a, b) => a - b)))
|
|
200
|
+
})),
|
|
201
|
+
...lags.length === 0 ? {} : { lags: spreadOf(lags) }
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
/** This function gives the verdict of the band of a cell: its containment and the equivalence of its median. */
|
|
205
|
+
const verdictOfBand = (prediction, runs) => {
|
|
114
206
|
const counted = runs.filter((run) => run.void === null);
|
|
115
207
|
const voided = runs.length - counted.length;
|
|
116
208
|
const outside = counted.filter((run) => run.finding !== null).map((run) => ({
|
|
@@ -270,6 +362,252 @@ const verdictOf = (prediction, runs) => {
|
|
|
270
362
|
};
|
|
271
363
|
};
|
|
272
364
|
//#endregion
|
|
365
|
+
//#region src/timeline.ts
|
|
366
|
+
/**
|
|
367
|
+
* This module measures a run of a group cell. It gives the first write that a primary accepted
|
|
368
|
+
* after the fault, and the timeline of the group as its Sentinels announced it. It also gives what
|
|
369
|
+
* became of each write.
|
|
370
|
+
*/
|
|
371
|
+
/** These are the stages of a timeline, in their sequence after a fault. */
|
|
372
|
+
const STAGES = [
|
|
373
|
+
"+sdown",
|
|
374
|
+
"+odown",
|
|
375
|
+
"+try-failover",
|
|
376
|
+
"+elected-leader",
|
|
377
|
+
"+selected-slave",
|
|
378
|
+
"+promoted-slave",
|
|
379
|
+
"+switch-master",
|
|
380
|
+
"+convert-to-slave"
|
|
381
|
+
];
|
|
382
|
+
const between$1 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
|
|
383
|
+
/** This function gives the member whose address and port is `at`, or null. */
|
|
384
|
+
const memberAt = (run, at) => {
|
|
385
|
+
if (typeof at !== "string") return null;
|
|
386
|
+
const ip = at.split(":")[0];
|
|
387
|
+
return Object.entries(run.group?.members ?? {}).find(([, part]) => part.ip === ip)?.[0] ?? null;
|
|
388
|
+
};
|
|
389
|
+
/** This function gives the time of each stage of a timeline from an instant: the first announcement of each, after the instant. */
|
|
390
|
+
const stagesOf = (timeline, from) => {
|
|
391
|
+
const out = {};
|
|
392
|
+
for (const line of timeline) {
|
|
393
|
+
if (line.ev !== "sentinel" || line.t < from) continue;
|
|
394
|
+
const stage = STAGES.find((each) => each === line.event);
|
|
395
|
+
if (stage === void 0 || out[stage] !== void 0) continue;
|
|
396
|
+
if ((stage === "+sdown" || stage === "+odown") && !String(line.detail ?? "").startsWith("master ")) continue;
|
|
397
|
+
out[stage] = between$1(from, line.t);
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
};
|
|
401
|
+
/** This function gives what became of each write of a run, from the lines of the client and the keys that the group kept. */
|
|
402
|
+
const writesOf = (run, after, primary) => {
|
|
403
|
+
const { events } = run;
|
|
404
|
+
const sent = events.filter((event) => event.ev === "sent");
|
|
405
|
+
const done = events.find((event) => event.ev === "done")?.t ?? sent.at(-1)?.t ?? Infinity;
|
|
406
|
+
const acks = /* @__PURE__ */ new Map();
|
|
407
|
+
for (const event of events) if (event.ev === "ack") acks.set(Number(event["n"]), event);
|
|
408
|
+
const holds = run.group?.holds ?? null;
|
|
409
|
+
const kept = holds === null ? null : new Set(holds);
|
|
410
|
+
const old = [...acks.values()].filter((event) => event.t > after && memberAt(run, event["at"]) === primary).length;
|
|
411
|
+
const lost = kept === null ? null : [...acks.keys()].filter((n) => !kept.has(n)).sort((a, b) => a - b);
|
|
412
|
+
const unknown = sent.filter((event) => event.t < done && !acks.has(Number(event["n"]))).map((event) => {
|
|
413
|
+
const n = Number(event["n"]);
|
|
414
|
+
return {
|
|
415
|
+
n,
|
|
416
|
+
held: kept === null ? null : kept.has(n)
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
return {
|
|
420
|
+
sent: sent.length,
|
|
421
|
+
acked: acks.size,
|
|
422
|
+
old,
|
|
423
|
+
lost,
|
|
424
|
+
unknown
|
|
425
|
+
};
|
|
426
|
+
};
|
|
427
|
+
/** This function gives the errors that the client met after an instant: each kind, its first time, and its count. */
|
|
428
|
+
const errorsOf = (events, after) => {
|
|
429
|
+
const done = events.find((event) => event.ev === "done")?.t ?? Infinity;
|
|
430
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const event of events) {
|
|
432
|
+
if (event.t <= after || event.t >= done) continue;
|
|
433
|
+
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;
|
|
434
|
+
if (name === null) continue;
|
|
435
|
+
const seen = kinds.get(name);
|
|
436
|
+
if (seen === void 0) kinds.set(name, {
|
|
437
|
+
first: event.t,
|
|
438
|
+
count: 1
|
|
439
|
+
});
|
|
440
|
+
else seen.count += 1;
|
|
441
|
+
}
|
|
442
|
+
return [...kinds].map(([name, { first, count }]) => ({
|
|
443
|
+
name,
|
|
444
|
+
first: between$1(after, first),
|
|
445
|
+
count
|
|
446
|
+
}));
|
|
447
|
+
};
|
|
448
|
+
/**
|
|
449
|
+
* This function gives the delay of each stamp of the observer after the write of its Sentinel that
|
|
450
|
+
* the observer read. That write is the latest write of the Sentinel's socket toward the observer,
|
|
451
|
+
* at the stamp or before it. The prober records each write (`tx`) on the clock of the stamps.
|
|
452
|
+
*/
|
|
453
|
+
const lagsOf = (run, kernel) => {
|
|
454
|
+
const timeline = run.group?.timeline ?? [];
|
|
455
|
+
const locals = /* @__PURE__ */ new Map();
|
|
456
|
+
for (const line of timeline) if (line.ev === "subscribed") locals.set(String(line.sentinel), String(line["local"]));
|
|
457
|
+
const out = [];
|
|
458
|
+
for (const line of timeline) {
|
|
459
|
+
if (line.ev !== "sentinel") continue;
|
|
460
|
+
const sentinel = String(line.sentinel);
|
|
461
|
+
const local = locals.get(sentinel);
|
|
462
|
+
if (local === void 0) continue;
|
|
463
|
+
const write = kernel.filter((record) => record.ev === "tx" && record.src === sentinel && record.dst === local && record.mono <= line.t).at(-1);
|
|
464
|
+
if (write !== void 0) out.push(between$1(write.mono, line.t));
|
|
465
|
+
}
|
|
466
|
+
return out;
|
|
467
|
+
};
|
|
468
|
+
/**
|
|
469
|
+
* This function reads one run of a group cell.
|
|
470
|
+
* - The reference is the start of the commit of the fault, as the injector stamped it. For a
|
|
471
|
+
* stall, it is the first write into the fault, as for the stall of a direct pair.
|
|
472
|
+
* - The outcome is the first write that a primary accepted after the fault. The prediction says
|
|
473
|
+
* which primary: a new one, or the same one after the lift.
|
|
474
|
+
* - Its time is the last segment with data that the client's socket to that primary received
|
|
475
|
+
* before the client saw the acceptance. The stamp of the client is the fallback.
|
|
476
|
+
* - A run is void when the prober lost records, or when the client did not become ready. It is
|
|
477
|
+
* also void when a Sentinel entered its TILT mode, or when the election split its votes. A band
|
|
478
|
+
* from the fault whose commit took longer than δ_abs is void too. So is a band whose hold lifted
|
|
479
|
+
* more than δ_abs late.
|
|
480
|
+
*/
|
|
481
|
+
const measureGroup = (cell, run) => {
|
|
482
|
+
const { events, kernel } = run;
|
|
483
|
+
const { prediction } = cell;
|
|
484
|
+
const expected = prediction.group;
|
|
485
|
+
const primary = run.group?.primary ?? "";
|
|
486
|
+
const ready = events.find((event) => event.ev === "ready");
|
|
487
|
+
const t0 = run.fault?.t0;
|
|
488
|
+
const empty = {
|
|
489
|
+
from: primary,
|
|
490
|
+
to: null,
|
|
491
|
+
stages: {},
|
|
492
|
+
writes: {
|
|
493
|
+
sent: 0,
|
|
494
|
+
acked: 0,
|
|
495
|
+
old: 0,
|
|
496
|
+
lost: null,
|
|
497
|
+
unknown: []
|
|
498
|
+
},
|
|
499
|
+
errors: []
|
|
500
|
+
};
|
|
501
|
+
const silent = {
|
|
502
|
+
outcome: { kind: "silent" },
|
|
503
|
+
elapsed: null,
|
|
504
|
+
socket: null,
|
|
505
|
+
noticed: null,
|
|
506
|
+
finding: null
|
|
507
|
+
};
|
|
508
|
+
if (ready === void 0) return {
|
|
509
|
+
...silent,
|
|
510
|
+
void: "the client never became ready",
|
|
511
|
+
group: empty
|
|
512
|
+
};
|
|
513
|
+
if (t0 === void 0 || run.fault === null) return {
|
|
514
|
+
...silent,
|
|
515
|
+
void: "the fault never committed",
|
|
516
|
+
group: empty
|
|
517
|
+
};
|
|
518
|
+
if (run.lost === null) return {
|
|
519
|
+
...silent,
|
|
520
|
+
void: "the prober wrote no clock line after the run, so nothing says what it dropped",
|
|
521
|
+
group: empty
|
|
522
|
+
};
|
|
523
|
+
if (run.lost > 0) return {
|
|
524
|
+
...silent,
|
|
525
|
+
void: `the prober dropped ${run.lost} records`,
|
|
526
|
+
group: empty
|
|
527
|
+
};
|
|
528
|
+
const timeline = run.group?.timeline ?? [];
|
|
529
|
+
const stages = stagesOf(timeline, t0);
|
|
530
|
+
const heal = run.heal ?? null;
|
|
531
|
+
const lands = expected?.lands ?? "new";
|
|
532
|
+
const after = lands === "same" && heal !== null ? heal.t0 : t0;
|
|
533
|
+
const accepted = events.find((event) => {
|
|
534
|
+
if (event.ev !== "ack" || event.t <= after) return false;
|
|
535
|
+
const member = memberAt(run, event["at"]);
|
|
536
|
+
return member !== null && (lands === "new" ? member !== primary : true);
|
|
537
|
+
});
|
|
538
|
+
const to = accepted === void 0 ? null : memberAt(run, accepted["at"]);
|
|
539
|
+
const group = {
|
|
540
|
+
from: primary,
|
|
541
|
+
to,
|
|
542
|
+
stages,
|
|
543
|
+
writes: writesOf(run, t0, primary),
|
|
544
|
+
errors: errorsOf(events, t0),
|
|
545
|
+
...cell.calibrates === "sentinels" ? { lags: lagsOf(run, kernel) } : {}
|
|
546
|
+
};
|
|
547
|
+
let voided = null;
|
|
548
|
+
const tilt = timeline.find((line) => line.ev === "sentinel" && line.event === "+tilt" && line.t >= t0);
|
|
549
|
+
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`;
|
|
550
|
+
const split = timeline.find((line) => line.ev === "sentinel" && line.event === "-failover-abort-not-elected" && line.t >= t0);
|
|
551
|
+
const elected = timeline.find((line) => line.ev === "sentinel" && line.event === "+elected-leader" && line.t >= t0);
|
|
552
|
+
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";
|
|
553
|
+
const odown = stages["+odown"];
|
|
554
|
+
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";
|
|
555
|
+
if (voided === null && cell.hold !== void 0 && prediction.detects !== null) {
|
|
556
|
+
if (heal === null) voided = "the fault never lifted";
|
|
557
|
+
else {
|
|
558
|
+
const late = (heal.t0 - t0) / 1e9 - valueIn(cell.hold, "s");
|
|
559
|
+
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`;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const assumed = prediction.detects === null ? void 0 : prediction.rtt;
|
|
563
|
+
const first = kernel.find((record) => record.netns === run.netns && record.dst === `${run.group?.members[primary]?.ip}:${cell.group?.port}` && record.tcp?.srtt !== void 0);
|
|
564
|
+
const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
|
|
565
|
+
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`;
|
|
566
|
+
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`;
|
|
567
|
+
let reference = t0;
|
|
568
|
+
if (prediction.detects === false && prediction.recovers !== void 0) {
|
|
569
|
+
const into = events.find((event) => event.ev === "sent" && event.t > run.fault.t1);
|
|
570
|
+
if (into === void 0 || heal !== null && into.t > heal.t0) voided ??= "the write in flight went out before the fault held";
|
|
571
|
+
else {
|
|
572
|
+
reference = into.t;
|
|
573
|
+
const { heal: bounds } = prediction.recovers;
|
|
574
|
+
if (heal === null) voided ??= "the stall never ended";
|
|
575
|
+
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`;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const findings = [];
|
|
579
|
+
const switched = stages["+switch-master"];
|
|
580
|
+
if (expected?.failover === false && switched !== void 0) findings.push(`the group failed over ${switched.value} s after the fault, where it should not`);
|
|
581
|
+
if (accepted !== void 0 && lands === "same" && to !== primary) findings.push(`the client wrote to ${to ?? String(accepted["at"])}, where it should stay on ${primary}`);
|
|
582
|
+
for (const error of expected?.errors ?? []) {
|
|
583
|
+
const met = group.errors.find((each) => each.name === error.name);
|
|
584
|
+
const noise = valueIn(NOISE, "s");
|
|
585
|
+
if (met === void 0) findings.push(`the client never met ${error.name}`);
|
|
586
|
+
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`);
|
|
587
|
+
}
|
|
588
|
+
if (accepted === void 0) {
|
|
589
|
+
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");
|
|
590
|
+
return {
|
|
591
|
+
...silent,
|
|
592
|
+
void: voided,
|
|
593
|
+
finding: findings[0] ?? null,
|
|
594
|
+
group
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const address = String(accepted["at"]);
|
|
598
|
+
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);
|
|
599
|
+
const at = segment?.mono ?? accepted.t;
|
|
600
|
+
return {
|
|
601
|
+
outcome: { kind: "answered" },
|
|
602
|
+
elapsed: between$1(reference, at),
|
|
603
|
+
socket: between$1(reference, accepted.t),
|
|
604
|
+
noticed: segment === void 0 ? null : between$1(segment.mono, accepted.t),
|
|
605
|
+
void: voided,
|
|
606
|
+
finding: findings[0] ?? null,
|
|
607
|
+
group
|
|
608
|
+
};
|
|
609
|
+
};
|
|
610
|
+
//#endregion
|
|
273
611
|
//#region src/observe.ts
|
|
274
612
|
/** These are the errno numbers that the kernel reports to a socket, by the names that predictions use. */
|
|
275
613
|
const ERRNO = {
|
|
@@ -425,6 +763,24 @@ const crossingsOf = (run, hop, toClient, toTarget) => {
|
|
|
425
763
|
* prober lost is also void.
|
|
426
764
|
*/
|
|
427
765
|
const measure = (cell, run) => {
|
|
766
|
+
if (cell.group !== void 0) {
|
|
767
|
+
const read = measureGroup(cell, run);
|
|
768
|
+
return {
|
|
769
|
+
environment: run.environment,
|
|
770
|
+
index: run.index,
|
|
771
|
+
outcome: read.outcome,
|
|
772
|
+
elapsed: read.elapsed,
|
|
773
|
+
socket: read.socket,
|
|
774
|
+
noticed: read.noticed,
|
|
775
|
+
steps: [],
|
|
776
|
+
probes: 0,
|
|
777
|
+
retransmissions: run.kernel.filter((record) => record.netns === run.netns && record.ev === "retrans").length,
|
|
778
|
+
acks: read.group.writes.acked,
|
|
779
|
+
void: read.void,
|
|
780
|
+
finding: read.finding,
|
|
781
|
+
group: read.group
|
|
782
|
+
};
|
|
783
|
+
}
|
|
428
784
|
const { events, kernel } = run;
|
|
429
785
|
const ready = events.find((event) => event.ev === "ready");
|
|
430
786
|
const local = events.find((event) => event.ev === "connected")?.["local"];
|
|
@@ -651,13 +1007,25 @@ const measure = (cell, run) => {
|
|
|
651
1007
|
if (!prediction.detects) {
|
|
652
1008
|
const window = valueIn(prediction.window, "s") * 1e9;
|
|
653
1009
|
if (ended === void 0 || ended - request.t > window) {
|
|
654
|
-
const
|
|
1010
|
+
const forgetIn = forgotten !== void 0 && forgotten.t - request.t <= window ? forgotten : void 0;
|
|
1011
|
+
const expected = prediction.middlebox?.forgets;
|
|
1012
|
+
const after = forgetIn === void 0 ? null : between(lastRx, forgetIn.t);
|
|
1013
|
+
let finding = null;
|
|
1014
|
+
if (expected === void 0) {
|
|
1015
|
+
if (after !== null) finding = `a router forgot the flow ${after.value} s after the last segment, where it should keep it`;
|
|
1016
|
+
} else if (after === null) finding = "no router forgot the flow in the window, where it should forget it";
|
|
1017
|
+
else {
|
|
1018
|
+
const noise = valueIn(NOISE, "s");
|
|
1019
|
+
const low = valueIn(expected.low, "s") - noise;
|
|
1020
|
+
const high = valueIn(expected.high, "s") + noise;
|
|
1021
|
+
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`;
|
|
1022
|
+
}
|
|
655
1023
|
return {
|
|
656
1024
|
...base,
|
|
657
1025
|
...nothing,
|
|
658
1026
|
...chain,
|
|
659
1027
|
void: voided,
|
|
660
|
-
finding
|
|
1028
|
+
finding
|
|
661
1029
|
};
|
|
662
1030
|
}
|
|
663
1031
|
return {
|
|
@@ -888,6 +1256,11 @@ const lines = (also) => {
|
|
|
888
1256
|
}
|
|
889
1257
|
};
|
|
890
1258
|
};
|
|
1259
|
+
/** This function makes a container share the process namespace of another container, for example `container:<id>`. */
|
|
1260
|
+
const withPid = (container, mode) => {
|
|
1261
|
+
container.hostConfig.PidMode = mode;
|
|
1262
|
+
return container;
|
|
1263
|
+
};
|
|
891
1264
|
/** This container class can share the process namespace of another container. */
|
|
892
1265
|
const shared = (Base) => class extends Base {
|
|
893
1266
|
withPidMode(mode) {
|
|
@@ -1207,17 +1580,22 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1207
1580
|
* on its sockets. A router forwards past INPUT and OUTPUT, thus its rules are FORWARD rules on
|
|
1208
1581
|
* its device toward the side (`--via`).
|
|
1209
1582
|
*/
|
|
1210
|
-
const
|
|
1211
|
-
if (at
|
|
1583
|
+
const hopPlace = (at) => {
|
|
1584
|
+
if (!isHopPlace(at)) throw new Error(`a lab of a topology has no part ${JSON.stringify(at)}: a group cell opens a group lab`);
|
|
1585
|
+
return at;
|
|
1586
|
+
};
|
|
1587
|
+
const placeOf = (place) => {
|
|
1588
|
+
if (place === "client") return [
|
|
1212
1589
|
admin,
|
|
1213
1590
|
targetIp,
|
|
1214
1591
|
[]
|
|
1215
1592
|
];
|
|
1216
|
-
if (
|
|
1593
|
+
if (place === "target") return [
|
|
1217
1594
|
injector,
|
|
1218
1595
|
targetPeer,
|
|
1219
1596
|
[]
|
|
1220
1597
|
];
|
|
1598
|
+
const at = hopPlace(place);
|
|
1221
1599
|
const hop = hopNamed(at.hop);
|
|
1222
1600
|
const peer = at.toward === "client" ? hop.peer.client : hop.peer.target.split(":")[0];
|
|
1223
1601
|
const via = hop.hop.kind === "router" ? ["--via", hop.devices[at.toward]] : [];
|
|
@@ -1228,9 +1606,10 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1228
1606
|
];
|
|
1229
1607
|
};
|
|
1230
1608
|
/** 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 (
|
|
1609
|
+
const deviceAt = (place) => {
|
|
1610
|
+
if (place === "client") return [admin, "eth0"];
|
|
1611
|
+
if (place === "target") return [injector, "eth0"];
|
|
1612
|
+
const at = hopPlace(place);
|
|
1234
1613
|
const hop = hopNamed(at.hop);
|
|
1235
1614
|
return [hop.injector, hop.devices[at.toward]];
|
|
1236
1615
|
};
|
|
@@ -1340,7 +1719,7 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1340
1719
|
return stamped(where, args);
|
|
1341
1720
|
},
|
|
1342
1721
|
async offload(on, at = "target") {
|
|
1343
|
-
const devices =
|
|
1722
|
+
const devices = isHopPlace(at) ? Object.values(hopNamed(at.hop).devices) : [deviceAt(at)[1]];
|
|
1344
1723
|
const [where] = deviceAt(at);
|
|
1345
1724
|
for (const device of devices) await output(where, [
|
|
1346
1725
|
"/opt/harness/inject",
|
|
@@ -1547,6 +1926,673 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1547
1926
|
}
|
|
1548
1927
|
};
|
|
1549
1928
|
//#endregion
|
|
1929
|
+
//#region src/group.ts
|
|
1930
|
+
/** This function gives the configuration file of a member: its port, its settings, and the primary that a replica follows. */
|
|
1931
|
+
const memberConfig = (group, primary) => [
|
|
1932
|
+
`port ${group.port}`,
|
|
1933
|
+
...Object.entries(group.memberSettings).map(([key, value]) => `${key} ${value}`),
|
|
1934
|
+
...primary === null ? [] : [`replicaof ${primary} ${group.port}`],
|
|
1935
|
+
""
|
|
1936
|
+
].join("\n");
|
|
1937
|
+
/** This function gives the configuration file of a Sentinel: its port, the group that it monitors, and the settings of the group. */
|
|
1938
|
+
const sentinelConfig = (group, primary) => [
|
|
1939
|
+
`port ${group.sentinelPort}`,
|
|
1940
|
+
`sentinel monitor ${group.name} ${primary} ${group.port} ${group.quorum}`,
|
|
1941
|
+
...Object.entries(group.sentinelSettings).map(([key, value]) => `sentinel ${key} ${group.name} ${value}`),
|
|
1942
|
+
""
|
|
1943
|
+
].join("\n");
|
|
1944
|
+
/**
|
|
1945
|
+
* This function gives the peers that a fault on a part cuts it from, by their addresses. `all` is
|
|
1946
|
+
* each other part and the client, `group` is the other parts, and `clients` is the client. The
|
|
1947
|
+
* observer is not a peer, thus the timeline of the group stays whole.
|
|
1948
|
+
*/
|
|
1949
|
+
const peersOf = (parts, client, part, cuts) => {
|
|
1950
|
+
const others = [...Object.entries(parts.members), ...Object.entries(parts.sentinels)].filter(([name]) => name !== part).map(([, at]) => at.ip);
|
|
1951
|
+
if (cuts === "clients") return [client];
|
|
1952
|
+
if (cuts === "group") return others;
|
|
1953
|
+
return [...others, client];
|
|
1954
|
+
};
|
|
1955
|
+
/** This function reads the flat reply of a command line client into its fields: name, value, name, value. */
|
|
1956
|
+
const fieldsOf = (text) => {
|
|
1957
|
+
const out = [];
|
|
1958
|
+
const words = text.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
1959
|
+
let current = null;
|
|
1960
|
+
for (let at = 0; at + 1 < words.length; at += 2) {
|
|
1961
|
+
const [key, value] = [words[at], words[at + 1]];
|
|
1962
|
+
if (key === "name" || current === null) {
|
|
1963
|
+
current = {};
|
|
1964
|
+
out.push(current);
|
|
1965
|
+
}
|
|
1966
|
+
current[key] = value;
|
|
1967
|
+
}
|
|
1968
|
+
return out;
|
|
1969
|
+
};
|
|
1970
|
+
/** This function reads the INFO of a server into its fields. */
|
|
1971
|
+
const infoOf = (text) => {
|
|
1972
|
+
const out = {};
|
|
1973
|
+
for (const line of text.split("\n")) {
|
|
1974
|
+
const at = line.indexOf(":");
|
|
1975
|
+
if (line.startsWith("#") || at === -1) continue;
|
|
1976
|
+
out[line.slice(0, at)] = line.slice(at + 1).trim();
|
|
1977
|
+
}
|
|
1978
|
+
return out;
|
|
1979
|
+
};
|
|
1980
|
+
/** This function judges the views of the Sentinels and of the members: the primary that they agree on, if the group is healthy. */
|
|
1981
|
+
const viewOf = (group, ips, sentinels, members) => {
|
|
1982
|
+
const others = String(group.sentinels.length - 1);
|
|
1983
|
+
const replicas = group.members.length - 1;
|
|
1984
|
+
const addresses = new Set(sentinels.map((each) => each.master["ip"]));
|
|
1985
|
+
if (addresses.size !== 1) return {
|
|
1986
|
+
primary: null,
|
|
1987
|
+
healthy: false,
|
|
1988
|
+
why: `the Sentinels name ${addresses.size} primaries`
|
|
1989
|
+
};
|
|
1990
|
+
const ip = [...addresses][0];
|
|
1991
|
+
const primary = Object.entries(ips).find(([, at]) => at === ip)?.[0] ?? null;
|
|
1992
|
+
if (primary === null) return {
|
|
1993
|
+
primary: null,
|
|
1994
|
+
healthy: false,
|
|
1995
|
+
why: `no member has the address ${ip}`
|
|
1996
|
+
};
|
|
1997
|
+
for (const [at, each] of sentinels.entries()) {
|
|
1998
|
+
if (each.master["flags"] !== "master") return {
|
|
1999
|
+
primary,
|
|
2000
|
+
healthy: false,
|
|
2001
|
+
why: `Sentinel ${at} flags the primary ${each.master["flags"]}`
|
|
2002
|
+
};
|
|
2003
|
+
if (each.master["num-other-sentinels"] !== others) return {
|
|
2004
|
+
primary,
|
|
2005
|
+
healthy: false,
|
|
2006
|
+
why: `Sentinel ${at} knows ${each.master["num-other-sentinels"]} other Sentinels`
|
|
2007
|
+
};
|
|
2008
|
+
const good = each.replicas.filter((replica) => replica["flags"] === "slave" && replica["master-link-status"] === "ok" && replica["master-host"] === ip);
|
|
2009
|
+
if (good.length !== replicas || each.replicas.length !== replicas) return {
|
|
2010
|
+
primary,
|
|
2011
|
+
healthy: false,
|
|
2012
|
+
why: `Sentinel ${at} has ${good.length} healthy replicas of ${each.replicas.length}`
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
const info = members[primary] ?? {};
|
|
2016
|
+
if (info["role"] !== "master") return {
|
|
2017
|
+
primary,
|
|
2018
|
+
healthy: false,
|
|
2019
|
+
why: `${primary} reports the role ${info["role"]}`
|
|
2020
|
+
};
|
|
2021
|
+
if (info["master_failover_state"] !== void 0 && info["master_failover_state"] !== "no-failover") return {
|
|
2022
|
+
primary,
|
|
2023
|
+
healthy: false,
|
|
2024
|
+
why: `${primary} is in the failover state ${info["master_failover_state"]}`
|
|
2025
|
+
};
|
|
2026
|
+
const online = Object.entries(info).filter(([key, value]) => /^slave\d+$/.test(key) && value.includes("state=online")).length;
|
|
2027
|
+
if (online !== replicas) return {
|
|
2028
|
+
primary,
|
|
2029
|
+
healthy: false,
|
|
2030
|
+
why: `${primary} has ${online} replicas online`
|
|
2031
|
+
};
|
|
2032
|
+
for (const name of group.members) {
|
|
2033
|
+
if (name === primary) continue;
|
|
2034
|
+
const each = members[name] ?? {};
|
|
2035
|
+
if (each["role"] !== "slave" || each["master_host"] !== ip || each["master_link_status"] !== "up") return {
|
|
2036
|
+
primary,
|
|
2037
|
+
healthy: false,
|
|
2038
|
+
why: `${name} is not a replica of ${primary} with its link up`
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
return {
|
|
2042
|
+
primary,
|
|
2043
|
+
healthy: true,
|
|
2044
|
+
why: "healthy"
|
|
2045
|
+
};
|
|
2046
|
+
};
|
|
2047
|
+
const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2048
|
+
/** This time is the longest that a group takes to converge before a run, or after it. */
|
|
2049
|
+
const CONVERGE_MS = 24e4;
|
|
2050
|
+
/**
|
|
2051
|
+
* This function opens one environment of a group cell:
|
|
2052
|
+
* - one new network, with each member, each Sentinel, the client and the observer on it.
|
|
2053
|
+
* - each member: the stock image, its server as the child of a shell, thus a fin can stop the
|
|
2054
|
+
* server and the container stays up. The first member is the primary, and the others follow it.
|
|
2055
|
+
* - each Sentinel: the stock image, its Sentinel as the child of a shell, which monitors the first member.
|
|
2056
|
+
* - an injector sidecar for each part, in its network and process namespaces.
|
|
2057
|
+
* - a client that holds its own network namespace, with a privileged sidecar for its sysctls.
|
|
2058
|
+
* - the observer of the Sentinels, in a namespace of its own.
|
|
2059
|
+
* - the prober, which watches the namespace of the client, and of each Sentinel for a calibration.
|
|
2060
|
+
*/
|
|
2061
|
+
const openGroupLab = async (group, options = {}) => {
|
|
2062
|
+
const image = await harnessImage();
|
|
2063
|
+
const node = await nodeClientImage();
|
|
2064
|
+
const { GenericContainer, Network, Wait, getContainerRuntimeClient } = await containers();
|
|
2065
|
+
const runtime = await getContainerRuntimeClient();
|
|
2066
|
+
const opened = [];
|
|
2067
|
+
let network;
|
|
2068
|
+
const close = async () => {
|
|
2069
|
+
for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
|
|
2070
|
+
await network?.stop().catch(() => void 0);
|
|
2071
|
+
};
|
|
2072
|
+
const ipOf = async (container) => {
|
|
2073
|
+
const ip = (await runtime.container.inspect(runtime.container.getById(container.getId()))).NetworkSettings.Networks[network.getName()]?.IPAddress;
|
|
2074
|
+
if (ip === void 0 || ip === "") throw new Error(`${container.getName()} has no address on ${network.getName()}`);
|
|
2075
|
+
return ip;
|
|
2076
|
+
};
|
|
2077
|
+
try {
|
|
2078
|
+
network = await new Network().start();
|
|
2079
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
2080
|
+
const injectors = /* @__PURE__ */ new Map();
|
|
2081
|
+
const parts = {
|
|
2082
|
+
members: {},
|
|
2083
|
+
sentinels: {}
|
|
2084
|
+
};
|
|
2085
|
+
/** This function starts one part: the stock image with its configuration, its program as the child of a shell, and its injector. */
|
|
2086
|
+
const start = async (name, program, file, content, ready) => {
|
|
2087
|
+
const box = await new GenericContainer(group.image).withNetwork(network).withNetworkAliases(name).withCopyContentToContainer([{
|
|
2088
|
+
content,
|
|
2089
|
+
target: file
|
|
2090
|
+
}]).withCommand([
|
|
2091
|
+
"sh",
|
|
2092
|
+
"-c",
|
|
2093
|
+
`${program} ${file} & while :; do sleep 3600; done`
|
|
2094
|
+
]).withWaitStrategy(Wait.forLogMessage(ready)).start();
|
|
2095
|
+
opened.push(box);
|
|
2096
|
+
boxes.set(name, box);
|
|
2097
|
+
const injector = await withPid(new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${box.getId()}`).start();
|
|
2098
|
+
opened.push(injector);
|
|
2099
|
+
injectors.set(name, injector);
|
|
2100
|
+
return {
|
|
2101
|
+
ip: await ipOf(box),
|
|
2102
|
+
netns: await netnsOf(injector)
|
|
2103
|
+
};
|
|
2104
|
+
};
|
|
2105
|
+
const [first, ...rest] = group.members;
|
|
2106
|
+
if (first === void 0) throw new Error("a group has no member");
|
|
2107
|
+
parts.members[first] = await start(first, group.programs.server, "/data/server.conf", memberConfig(group, null), group.ready.member);
|
|
2108
|
+
const primaryIp = parts.members[first].ip;
|
|
2109
|
+
for (const name of rest) parts.members[name] = await start(name, group.programs.server, "/data/server.conf", memberConfig(group, primaryIp), group.ready.member);
|
|
2110
|
+
for (const name of group.sentinels) parts.sentinels[name] = await start(name, group.programs.sentinel, "/data/sentinel.conf", sentinelConfig(group, primaryIp), group.ready.sentinel);
|
|
2111
|
+
const holder = await new GenericContainer(image).withNetwork(network).start();
|
|
2112
|
+
opened.push(holder);
|
|
2113
|
+
const admin = await new GenericContainer(image).withNetworkMode(`container:${holder.getId()}`).withPrivilegedMode().start();
|
|
2114
|
+
opened.push(admin);
|
|
2115
|
+
const clientIp = await ipOf(holder);
|
|
2116
|
+
const sentinelAddresses = group.sentinels.map((name) => `${parts.sentinels[name].ip}:${group.sentinelPort}`);
|
|
2117
|
+
const timeline = lines();
|
|
2118
|
+
const observer = await new GenericContainer(node).withNetwork(network).withCommand([
|
|
2119
|
+
"node",
|
|
2120
|
+
"/opt/harness/node/observer.mjs",
|
|
2121
|
+
sentinelAddresses.join(",")
|
|
2122
|
+
]).withLogConsumer(timeline.consume).start();
|
|
2123
|
+
opened.push(observer);
|
|
2124
|
+
const observerIp = await ipOf(observer);
|
|
2125
|
+
for (const address of sentinelAddresses) await new Promise((resolve, reject) => {
|
|
2126
|
+
const started = Date.now();
|
|
2127
|
+
const look = () => {
|
|
2128
|
+
if (timeline.seen.some((line) => line.ev === "subscribed" && line["sentinel"] === address)) resolve();
|
|
2129
|
+
else if (Date.now() - started > 3e4) reject(/* @__PURE__ */ new Error(`the observer never subscribed to ${address}`));
|
|
2130
|
+
else setTimeout(look, 50);
|
|
2131
|
+
};
|
|
2132
|
+
look();
|
|
2133
|
+
});
|
|
2134
|
+
const clientNetns = await netnsOf(admin);
|
|
2135
|
+
const watched = [clientNetns, ...options.watchSentinels === true ? group.sentinels.map((name) => parts.sentinels[name].netns) : []];
|
|
2136
|
+
const records = lines();
|
|
2137
|
+
const prober = await new GenericContainer(image).withPrivilegedMode().withNetworkMode("none").withCommand(["/opt/harness/prober", ...watched.map(String)]).withLogConsumer(records.consume).start();
|
|
2138
|
+
opened.push(prober);
|
|
2139
|
+
const begun = await records.until(["begin", "fail"], 3e4, "prober");
|
|
2140
|
+
if (begun.ev === "fail") throw new Error(`the prober failed: ${String(begun["why"])}`);
|
|
2141
|
+
const box = (name) => {
|
|
2142
|
+
const found = boxes.get(name);
|
|
2143
|
+
if (found === void 0) throw new Error(`the group has no part ${name}`);
|
|
2144
|
+
return found;
|
|
2145
|
+
};
|
|
2146
|
+
const injectorOf = (name) => {
|
|
2147
|
+
const found = injectors.get(name);
|
|
2148
|
+
if (found === void 0) throw new Error(`the group has no part ${name}`);
|
|
2149
|
+
return found;
|
|
2150
|
+
};
|
|
2151
|
+
const cli = (name, args) => {
|
|
2152
|
+
const port = group.sentinels.includes(name) ? group.sentinelPort : group.port;
|
|
2153
|
+
return output(box(name), [
|
|
2154
|
+
group.programs.cli,
|
|
2155
|
+
"-p",
|
|
2156
|
+
String(port),
|
|
2157
|
+
...args
|
|
2158
|
+
]);
|
|
2159
|
+
};
|
|
2160
|
+
const stamped = async (where, argv) => {
|
|
2161
|
+
const stamp = JSON.parse((await output(where, argv)).split("\n").at(-1));
|
|
2162
|
+
return {
|
|
2163
|
+
t0: stamp.t0,
|
|
2164
|
+
t1: stamp.t1
|
|
2165
|
+
};
|
|
2166
|
+
};
|
|
2167
|
+
const ips = Object.fromEntries(Object.entries(parts.members).map(([name, at]) => [name, at.ip]));
|
|
2168
|
+
const view = async () => {
|
|
2169
|
+
const sentinels = [];
|
|
2170
|
+
for (const name of group.sentinels) {
|
|
2171
|
+
const master = fieldsOf(await cli(name, [
|
|
2172
|
+
"SENTINEL",
|
|
2173
|
+
"MASTER",
|
|
2174
|
+
group.name
|
|
2175
|
+
]))[0] ?? {};
|
|
2176
|
+
const replicas = fieldsOf(await cli(name, [
|
|
2177
|
+
"SENTINEL",
|
|
2178
|
+
"REPLICAS",
|
|
2179
|
+
group.name
|
|
2180
|
+
]));
|
|
2181
|
+
sentinels.push({
|
|
2182
|
+
master,
|
|
2183
|
+
replicas
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
const members = {};
|
|
2187
|
+
for (const name of group.members) members[name] = infoOf(await cli(name, ["INFO", "replication"]));
|
|
2188
|
+
return viewOf(group, ips, sentinels, members);
|
|
2189
|
+
};
|
|
2190
|
+
const after = (at) => at === void 0 ? [] : ["--at", String(Math.round(at))];
|
|
2191
|
+
const roles = (role, primary) => {
|
|
2192
|
+
if (role === "primary") return primary;
|
|
2193
|
+
if (role === "sentinel") return group.sentinels[0];
|
|
2194
|
+
const replica = group.members.find((name) => name !== primary);
|
|
2195
|
+
if (replica === void 0) throw new Error("the group has no replica");
|
|
2196
|
+
return replica;
|
|
2197
|
+
};
|
|
2198
|
+
return {
|
|
2199
|
+
group,
|
|
2200
|
+
clientIp,
|
|
2201
|
+
observerIp,
|
|
2202
|
+
members: parts.members,
|
|
2203
|
+
sentinels: parts.sentinels,
|
|
2204
|
+
netns: {
|
|
2205
|
+
client: clientNetns,
|
|
2206
|
+
watched
|
|
2207
|
+
},
|
|
2208
|
+
kernel: records.seen,
|
|
2209
|
+
timeline: timeline.seen,
|
|
2210
|
+
async sysctl(values) {
|
|
2211
|
+
const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
|
|
2212
|
+
if (settings.length > 0) await output(admin, [
|
|
2213
|
+
"sysctl",
|
|
2214
|
+
"-q",
|
|
2215
|
+
"-w",
|
|
2216
|
+
...settings
|
|
2217
|
+
]);
|
|
2218
|
+
const read = {};
|
|
2219
|
+
for (const key of Object.keys(values)) read[key] = await output(admin, [
|
|
2220
|
+
"sysctl",
|
|
2221
|
+
"-n",
|
|
2222
|
+
key
|
|
2223
|
+
]);
|
|
2224
|
+
return read;
|
|
2225
|
+
},
|
|
2226
|
+
async converge(within = CONVERGE_MS) {
|
|
2227
|
+
const started = Date.now();
|
|
2228
|
+
let calm = 0;
|
|
2229
|
+
let last = {
|
|
2230
|
+
primary: null,
|
|
2231
|
+
healthy: false,
|
|
2232
|
+
why: "no view yet"
|
|
2233
|
+
};
|
|
2234
|
+
while (Date.now() - started < within) {
|
|
2235
|
+
last = await view().catch((error) => ({
|
|
2236
|
+
primary: null,
|
|
2237
|
+
healthy: false,
|
|
2238
|
+
why: error.message
|
|
2239
|
+
}));
|
|
2240
|
+
calm = last.healthy ? calm + 1 : 0;
|
|
2241
|
+
if (calm >= 2 && last.primary !== null) return last.primary;
|
|
2242
|
+
await sleep$1(1e3);
|
|
2243
|
+
}
|
|
2244
|
+
throw new Error(`the group did not converge in ${within} ms: ${last.why}`);
|
|
2245
|
+
},
|
|
2246
|
+
async flush(primary) {
|
|
2247
|
+
await cli(primary, ["FLUSHALL"]);
|
|
2248
|
+
const acked = Number(await cli(primary, [
|
|
2249
|
+
"WAIT",
|
|
2250
|
+
String(group.members.length - 1),
|
|
2251
|
+
"5000"
|
|
2252
|
+
]));
|
|
2253
|
+
if (acked !== group.members.length - 1) throw new Error(`${acked} replicas acknowledged the flush`);
|
|
2254
|
+
},
|
|
2255
|
+
async reset() {
|
|
2256
|
+
for (const name of group.sentinels) await cli(name, [
|
|
2257
|
+
"SENTINEL",
|
|
2258
|
+
"RESET",
|
|
2259
|
+
group.name
|
|
2260
|
+
]);
|
|
2261
|
+
},
|
|
2262
|
+
async holds(primary) {
|
|
2263
|
+
return (await cli(primary, [
|
|
2264
|
+
"--scan",
|
|
2265
|
+
"--pattern",
|
|
2266
|
+
"w:*"
|
|
2267
|
+
])).split("\n").map((key) => Number(key.trim().slice(2))).filter((n) => Number.isInteger(n) && n > 0).sort((a, b) => a - b);
|
|
2268
|
+
},
|
|
2269
|
+
partOf: roles,
|
|
2270
|
+
async client(args) {
|
|
2271
|
+
const events = lines();
|
|
2272
|
+
const container = await new GenericContainer(node).withNetworkMode(`container:${holder.getId()}`).withCommand([
|
|
2273
|
+
"node",
|
|
2274
|
+
"/opt/harness/node/client-sentinel.mjs",
|
|
2275
|
+
...args
|
|
2276
|
+
]).withLogConsumer(events.consume).start();
|
|
2277
|
+
return {
|
|
2278
|
+
events: events.seen,
|
|
2279
|
+
until: (kinds, within) => events.until(kinds, within, "client"),
|
|
2280
|
+
stop: () => container.stop({ timeout: 2 }).then(() => void 0).catch(() => void 0)
|
|
2281
|
+
};
|
|
2282
|
+
},
|
|
2283
|
+
fault(kind, part, cuts, at) {
|
|
2284
|
+
const peers = peersOf(parts, clientIp, part, cuts).join(",");
|
|
2285
|
+
return stamped(injectorOf(part), [
|
|
2286
|
+
"/opt/harness/inject",
|
|
2287
|
+
kind,
|
|
2288
|
+
peers,
|
|
2289
|
+
...after(at)
|
|
2290
|
+
]);
|
|
2291
|
+
},
|
|
2292
|
+
lift(kind, part, cuts, at) {
|
|
2293
|
+
const peers = peersOf(parts, clientIp, part, cuts).join(",");
|
|
2294
|
+
return stamped(injectorOf(part), [
|
|
2295
|
+
"/opt/harness/inject",
|
|
2296
|
+
kind === "rst" ? "clear-rst" : "clear",
|
|
2297
|
+
peers,
|
|
2298
|
+
...after(at)
|
|
2299
|
+
]);
|
|
2300
|
+
},
|
|
2301
|
+
signal(part, signal, at) {
|
|
2302
|
+
const program = group.sentinels.includes(part) ? group.programs.sentinel : group.programs.server;
|
|
2303
|
+
return stamped(injectorOf(part), [
|
|
2304
|
+
"/opt/harness/inject",
|
|
2305
|
+
"signal",
|
|
2306
|
+
program,
|
|
2307
|
+
signal,
|
|
2308
|
+
...after(at)
|
|
2309
|
+
]);
|
|
2310
|
+
},
|
|
2311
|
+
async restart(member) {
|
|
2312
|
+
await output(box(member), [
|
|
2313
|
+
"sh",
|
|
2314
|
+
"-c",
|
|
2315
|
+
`${group.programs.server} /data/server.conf --daemonize yes`
|
|
2316
|
+
]);
|
|
2317
|
+
const started = Date.now();
|
|
2318
|
+
while (await cli(member, ["PING"]).catch(() => "") !== "PONG") {
|
|
2319
|
+
if (Date.now() - started > 3e4) throw new Error(`${member} did not start again`);
|
|
2320
|
+
await sleep$1(200);
|
|
2321
|
+
}
|
|
2322
|
+
},
|
|
2323
|
+
failover(coordinated) {
|
|
2324
|
+
const sentinel = group.sentinels[0];
|
|
2325
|
+
return stamped(injectorOf(sentinel), [
|
|
2326
|
+
"/opt/harness/inject",
|
|
2327
|
+
"resp",
|
|
2328
|
+
`127.0.0.1:${group.sentinelPort}`,
|
|
2329
|
+
"SENTINEL",
|
|
2330
|
+
"FAILOVER",
|
|
2331
|
+
group.name,
|
|
2332
|
+
...coordinated ? ["COORDINATED"] : []
|
|
2333
|
+
]);
|
|
2334
|
+
},
|
|
2335
|
+
async forget() {
|
|
2336
|
+
await output(admin, [
|
|
2337
|
+
"ip",
|
|
2338
|
+
"tcp_metrics",
|
|
2339
|
+
"flush",
|
|
2340
|
+
"all"
|
|
2341
|
+
]);
|
|
2342
|
+
},
|
|
2343
|
+
async settle() {
|
|
2344
|
+
const from = records.seen.length;
|
|
2345
|
+
const clock = await new Promise((resolve) => {
|
|
2346
|
+
const started = Date.now();
|
|
2347
|
+
const look = () => {
|
|
2348
|
+
const line = records.seen.slice(from).find((record) => record.ev === "clock");
|
|
2349
|
+
if (line !== void 0) resolve(line);
|
|
2350
|
+
else if (Date.now() - started > 5e3) resolve(null);
|
|
2351
|
+
else setTimeout(look, 50);
|
|
2352
|
+
};
|
|
2353
|
+
look();
|
|
2354
|
+
});
|
|
2355
|
+
return clock === null ? null : Number(clock["lost"] ?? 0);
|
|
2356
|
+
},
|
|
2357
|
+
async manifest(sysctls) {
|
|
2358
|
+
const config = await output(admin, ["zcat", "/proc/config.gz"]).catch(() => "");
|
|
2359
|
+
const inspect = async (container) => runtime.container.inspect(runtime.container.getById(container.getId()));
|
|
2360
|
+
const memberSettings = async (name) => {
|
|
2361
|
+
const read = {};
|
|
2362
|
+
for (const key of Object.keys(group.memberSettings)) read[key] = (await cli(name, [
|
|
2363
|
+
"CONFIG",
|
|
2364
|
+
"GET",
|
|
2365
|
+
key
|
|
2366
|
+
])).split("\n")[1] ?? "";
|
|
2367
|
+
return read;
|
|
2368
|
+
};
|
|
2369
|
+
const sentinelSettings = async (name) => {
|
|
2370
|
+
const master = fieldsOf(await cli(name, [
|
|
2371
|
+
"SENTINEL",
|
|
2372
|
+
"MASTER",
|
|
2373
|
+
group.name
|
|
2374
|
+
]))[0] ?? {};
|
|
2375
|
+
const read = { quorum: master["quorum"] ?? "" };
|
|
2376
|
+
for (const key of Object.keys(group.sentinelSettings)) read[key] = master[key] ?? "";
|
|
2377
|
+
return read;
|
|
2378
|
+
};
|
|
2379
|
+
const members = [];
|
|
2380
|
+
for (const name of group.members) members.push({
|
|
2381
|
+
name,
|
|
2382
|
+
ip: parts.members[name].ip,
|
|
2383
|
+
id: (await inspect(box(name))).Image,
|
|
2384
|
+
settings: await memberSettings(name)
|
|
2385
|
+
});
|
|
2386
|
+
const sentinels = [];
|
|
2387
|
+
for (const name of group.sentinels) sentinels.push({
|
|
2388
|
+
name,
|
|
2389
|
+
ip: parts.sentinels[name].ip,
|
|
2390
|
+
id: (await inspect(box(name))).Image,
|
|
2391
|
+
settings: await sentinelSettings(name)
|
|
2392
|
+
});
|
|
2393
|
+
return {
|
|
2394
|
+
kernel: await output(holder, ["uname", "-r"]),
|
|
2395
|
+
architecture: await output(holder, ["uname", "-m"]),
|
|
2396
|
+
hz: /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1] ?? null,
|
|
2397
|
+
boot_id: await output(holder, ["cat", "/proc/sys/kernel/random/boot_id"]),
|
|
2398
|
+
docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
|
|
2399
|
+
target: {
|
|
2400
|
+
image: group.image,
|
|
2401
|
+
id: members[0].id,
|
|
2402
|
+
settings: members[0].settings,
|
|
2403
|
+
sysctls: {}
|
|
2404
|
+
},
|
|
2405
|
+
harness: {
|
|
2406
|
+
image,
|
|
2407
|
+
id: (await inspect(holder)).Image,
|
|
2408
|
+
packages: (await output(holder, [
|
|
2409
|
+
"apk",
|
|
2410
|
+
"info",
|
|
2411
|
+
"-v"
|
|
2412
|
+
])).split("\n").sort()
|
|
2413
|
+
},
|
|
2414
|
+
client: {
|
|
2415
|
+
kind: "ioredis",
|
|
2416
|
+
image: node
|
|
2417
|
+
},
|
|
2418
|
+
netns: {
|
|
2419
|
+
client: clientNetns,
|
|
2420
|
+
target: parts.members[first].netns
|
|
2421
|
+
},
|
|
2422
|
+
sysctls,
|
|
2423
|
+
group: {
|
|
2424
|
+
members,
|
|
2425
|
+
sentinels,
|
|
2426
|
+
observer: { image: node }
|
|
2427
|
+
}
|
|
2428
|
+
};
|
|
2429
|
+
},
|
|
2430
|
+
close
|
|
2431
|
+
};
|
|
2432
|
+
} catch (error) {
|
|
2433
|
+
await close();
|
|
2434
|
+
throw error;
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
/** This count is the number of accepted writes that a run waits for before its fault: the stream is steady then. */
|
|
2438
|
+
const STEADY = 20;
|
|
2439
|
+
/** This time is how long a run continues after its outcome, so that the record shows what came next. */
|
|
2440
|
+
const TAIL_MS = 2e3;
|
|
2441
|
+
/** This function gives the address of a member, with the port of the group. */
|
|
2442
|
+
const addressOf = (lab, member) => `${lab.members[member].ip}:${lab.group.port}`;
|
|
2443
|
+
/** This function gives the latest time that a group cell waits for its outcome, in ms from the fault. */
|
|
2444
|
+
const deadlineOf = (cell) => {
|
|
2445
|
+
const { prediction } = cell;
|
|
2446
|
+
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;
|
|
2447
|
+
};
|
|
2448
|
+
/** This function gives the arguments of the client of a group cell. */
|
|
2449
|
+
const clientArgs = (cell, lab) => {
|
|
2450
|
+
return [
|
|
2451
|
+
"--sentinels",
|
|
2452
|
+
cell.group.sentinels.map((name) => `${lab.sentinels[name].ip}:${cell.group.sentinelPort}`).join(","),
|
|
2453
|
+
"--name",
|
|
2454
|
+
cell.group.name,
|
|
2455
|
+
"--interval-ms",
|
|
2456
|
+
String(cell.probe.interval === void 0 ? 50 : valueIn(cell.probe.interval, "ms")),
|
|
2457
|
+
"--max-s",
|
|
2458
|
+
String(Math.ceil(deadlineOf(cell) / 1e3 + 120)),
|
|
2459
|
+
...cell.driverOptions === void 0 ? [] : ["--options", JSON.stringify(cell.driverOptions)]
|
|
2460
|
+
];
|
|
2461
|
+
};
|
|
2462
|
+
/** This function waits until a condition on the lines of the client holds, or for `within` ms. It gives the first line that matches. */
|
|
2463
|
+
const waitFor = (client, match, within) => new Promise((resolve) => {
|
|
2464
|
+
const started = Date.now();
|
|
2465
|
+
const look = () => {
|
|
2466
|
+
const found = client.events.find(match);
|
|
2467
|
+
if (found !== void 0 || Date.now() - started > within) resolve(found);
|
|
2468
|
+
else setTimeout(look, 20);
|
|
2469
|
+
};
|
|
2470
|
+
look();
|
|
2471
|
+
});
|
|
2472
|
+
/** This function gives the latest subscription of the observer to each Sentinel, in some lines of the observer. */
|
|
2473
|
+
const subscriptionsOf = (timeline) => {
|
|
2474
|
+
const latest = /* @__PURE__ */ new Map();
|
|
2475
|
+
for (const line of timeline) if (line.ev === "subscribed") latest.set(String(line.sentinel), line);
|
|
2476
|
+
return [...latest.values()];
|
|
2477
|
+
};
|
|
2478
|
+
/** This function gives the count of lost records in the latest clock line of the prober. */
|
|
2479
|
+
const droppedSoFar$1 = (records) => {
|
|
2480
|
+
for (let at = records.length - 1; at >= 0; at -= 1) if (records[at].ev === "clock") return Number(records[at]["lost"] ?? 0);
|
|
2481
|
+
return 0;
|
|
2482
|
+
};
|
|
2483
|
+
/**
|
|
2484
|
+
* This function does one run of a group cell:
|
|
2485
|
+
* - The group converges, and its keys go. The client connects through the Sentinels, and its
|
|
2486
|
+
* stream of writes gets steady.
|
|
2487
|
+
* - The fault occurs on its part, as the primary of this run makes the roles. A fault with a
|
|
2488
|
+
* hold lifts at its instant in the VM. A stall lifts at its length after the first write into it.
|
|
2489
|
+
* - The run waits for its outcome: the first write that a new primary accepts, or the first one
|
|
2490
|
+
* after the lift on the same primary. Then it continues for a moment.
|
|
2491
|
+
* - Each fault goes, the client stops, and the group converges again. The keys that it then holds
|
|
2492
|
+
* tell which writes it kept.
|
|
2493
|
+
*/
|
|
2494
|
+
const groupOnce = async (lab, cell, environment, index) => {
|
|
2495
|
+
const group = cell.group;
|
|
2496
|
+
const primary = await lab.converge();
|
|
2497
|
+
await lab.flush(primary);
|
|
2498
|
+
await lab.forget();
|
|
2499
|
+
const from = lab.kernel.length;
|
|
2500
|
+
const timelineFrom = lab.timeline.length;
|
|
2501
|
+
const lostBefore = droppedSoFar$1(lab.kernel);
|
|
2502
|
+
const client = await lab.client(clientArgs(cell, lab));
|
|
2503
|
+
const lands = cell.prediction.group?.lands ?? "new";
|
|
2504
|
+
const place = cell.faultAt ?? { role: "primary" };
|
|
2505
|
+
let fault = null;
|
|
2506
|
+
let heal = null;
|
|
2507
|
+
let at = null;
|
|
2508
|
+
const undo = [];
|
|
2509
|
+
try {
|
|
2510
|
+
await client.until(["ready"], 6e4);
|
|
2511
|
+
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`);
|
|
2512
|
+
let healing = null;
|
|
2513
|
+
switch (cell.fault) {
|
|
2514
|
+
case "blackhole":
|
|
2515
|
+
case "partition":
|
|
2516
|
+
case "rst": {
|
|
2517
|
+
const kind = cell.fault === "rst" ? "rst" : "blackhole";
|
|
2518
|
+
const cuts = place.cuts ?? (cell.fault === "partition" ? "group" : "all");
|
|
2519
|
+
const part = lab.partOf(place.role, primary);
|
|
2520
|
+
at = part;
|
|
2521
|
+
fault = await lab.fault(kind, part, cuts);
|
|
2522
|
+
undo.push(() => lab.lift(kind, part, cuts));
|
|
2523
|
+
if (cell.stall !== void 0) {
|
|
2524
|
+
const t1 = fault.t1;
|
|
2525
|
+
const sent = await waitFor(client, (event) => event.ev === "sent" && event.t > t1, 5e3);
|
|
2526
|
+
if (sent !== void 0) healing = lab.lift(kind, part, cuts, sent.t + valueIn(cell.stall, "s") * 1e9);
|
|
2527
|
+
} else if (cell.hold !== void 0) healing = lab.lift(kind, part, cuts, fault.t0 + valueIn(cell.hold, "s") * 1e9);
|
|
2528
|
+
break;
|
|
2529
|
+
}
|
|
2530
|
+
case "freeze-process": {
|
|
2531
|
+
const part = lab.partOf(place.role, primary);
|
|
2532
|
+
at = part;
|
|
2533
|
+
fault = await lab.signal(part, "STOP");
|
|
2534
|
+
undo.push(() => lab.signal(part, "CONT"));
|
|
2535
|
+
if (cell.hold !== void 0) healing = lab.signal(part, "CONT", fault.t0 + valueIn(cell.hold, "s") * 1e9);
|
|
2536
|
+
break;
|
|
2537
|
+
}
|
|
2538
|
+
case "fin": {
|
|
2539
|
+
const part = lab.partOf(place.role, primary);
|
|
2540
|
+
at = part;
|
|
2541
|
+
fault = await lab.signal(part, "KILL");
|
|
2542
|
+
if (group.members.includes(part)) undo.push(() => lab.restart(part));
|
|
2543
|
+
break;
|
|
2544
|
+
}
|
|
2545
|
+
case "planned-failover":
|
|
2546
|
+
fault = await lab.failover(cell.coordinated === true);
|
|
2547
|
+
break;
|
|
2548
|
+
default: throw new Error(`a group cell has no fault ${cell.fault}`);
|
|
2549
|
+
}
|
|
2550
|
+
const healed = healing === null ? Promise.resolve(null) : healing.then((stamp) => {
|
|
2551
|
+
heal = stamp;
|
|
2552
|
+
undo.length = 0;
|
|
2553
|
+
return stamp;
|
|
2554
|
+
});
|
|
2555
|
+
healed.catch(() => void 0);
|
|
2556
|
+
const t0 = fault.t0;
|
|
2557
|
+
const old = addressOf(lab, primary);
|
|
2558
|
+
const deadline = deadlineOf(cell);
|
|
2559
|
+
if ((lands === "new" ? await waitFor(client, (event) => event.ev === "ack" && event.t > t0 && typeof event["at"] === "string" && event["at"] !== old, deadline) : await (async () => {
|
|
2560
|
+
await healed;
|
|
2561
|
+
const after = heal?.t0 ?? t0;
|
|
2562
|
+
return waitFor(client, (event) => event.ev === "ack" && event.t > after, deadline);
|
|
2563
|
+
})()) !== void 0) await sleep$1(TAIL_MS);
|
|
2564
|
+
await healed;
|
|
2565
|
+
} finally {
|
|
2566
|
+
for (const step of undo.reverse()) await step().catch(() => void 0);
|
|
2567
|
+
await client.stop();
|
|
2568
|
+
}
|
|
2569
|
+
const settled = await lab.settle();
|
|
2570
|
+
const lost = settled === null ? null : settled - lostBefore;
|
|
2571
|
+
const after = await lab.converge().catch(() => null);
|
|
2572
|
+
const holds = after === null ? null : await lab.holds(after);
|
|
2573
|
+
if (lab.timeline.slice(timelineFrom).some((line) => line.ev === "sentinel" && String(line.event).startsWith("-failover-abort"))) await lab.reset();
|
|
2574
|
+
return {
|
|
2575
|
+
environment,
|
|
2576
|
+
index,
|
|
2577
|
+
events: [...client.events],
|
|
2578
|
+
fault,
|
|
2579
|
+
...heal === null ? {} : { heal },
|
|
2580
|
+
kernel: lab.kernel.slice(from),
|
|
2581
|
+
netns: lab.netns.client,
|
|
2582
|
+
targetNetns: lab.members[primary].netns,
|
|
2583
|
+
lost,
|
|
2584
|
+
group: {
|
|
2585
|
+
members: lab.members,
|
|
2586
|
+
sentinels: lab.sentinels,
|
|
2587
|
+
primary,
|
|
2588
|
+
at,
|
|
2589
|
+
timeline: [...subscriptionsOf(lab.timeline.slice(0, timelineFrom)), ...lab.timeline.slice(timelineFrom)],
|
|
2590
|
+
after,
|
|
2591
|
+
holds
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
};
|
|
2595
|
+
//#endregion
|
|
1550
2596
|
//#region src/run.ts
|
|
1551
2597
|
/**
|
|
1552
2598
|
* This function measures and judges the raw records of a cell, with the code of the harness at
|
|
@@ -1679,9 +2725,9 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
1679
2725
|
if (replyInFlight) {
|
|
1680
2726
|
await go();
|
|
1681
2727
|
await sleep(200);
|
|
1682
|
-
}
|
|
2728
|
+
} else await sleep(250);
|
|
1683
2729
|
const faultAt = cell.faultAt ?? (server ? "client" : "target");
|
|
1684
|
-
const towardServerComesIn = faultAt === "target" ||
|
|
2730
|
+
const towardServerComesIn = faultAt === "target" || isHopPlace(faultAt) && faultAt.toward === "client";
|
|
1685
2731
|
const direction = cell.drops === void 0 ? void 0 : cell.drops === "to-server" === towardServerComesIn ? "in" : "out";
|
|
1686
2732
|
const options = {
|
|
1687
2733
|
...direction === void 0 ? {} : { direction },
|
|
@@ -1808,6 +2854,7 @@ const line = (cell, run, read) => `${cell.name}: env ${run.environment} ${run.wa
|
|
|
1808
2854
|
* of a cell whose fault ends the environment is a dry run, without the fault.
|
|
1809
2855
|
*/
|
|
1810
2856
|
const inEnvironment = async (cell, environment, count, say) => {
|
|
2857
|
+
if (cell.group !== void 0) return inGroupEnvironment(cell, environment, count, say);
|
|
1811
2858
|
const lab = await openLab(cell.target, cell.client ?? cell.probe.driver?.name ?? "tcp", cell.topology);
|
|
1812
2859
|
try {
|
|
1813
2860
|
const settings = await lab.configure(cell.targetSettings ?? {});
|
|
@@ -1816,7 +2863,7 @@ const inEnvironment = async (cell, environment, count, say) => {
|
|
|
1816
2863
|
if (cell.fault === "mtu-blackhole" || cell.fault === "netem") {
|
|
1817
2864
|
await lab.offload(false, "target");
|
|
1818
2865
|
await lab.offload(false, "client");
|
|
1819
|
-
if (
|
|
2866
|
+
if (isHopPlace(cell.faultAt)) await lab.offload(false, cell.faultAt);
|
|
1820
2867
|
}
|
|
1821
2868
|
const latency = netemSpec(cell, void 0);
|
|
1822
2869
|
if (latency !== null) await lab.netem(latency, "target");
|
|
@@ -1843,6 +2890,35 @@ const inEnvironment = async (cell, environment, count, say) => {
|
|
|
1843
2890
|
}
|
|
1844
2891
|
};
|
|
1845
2892
|
/**
|
|
2893
|
+
* This function does the part of the runs of a group cell in one environment. The warm-up is a
|
|
2894
|
+
* whole run with its fault, because no fault of a group ends its environment. A server that a
|
|
2895
|
+
* fin stopped starts again, and the group converges before each run.
|
|
2896
|
+
*/
|
|
2897
|
+
const inGroupEnvironment = async (cell, environment, count, say) => {
|
|
2898
|
+
const lab = await openGroupLab(cell.group, { watchSentinels: cell.calibrates === "sentinels" });
|
|
2899
|
+
try {
|
|
2900
|
+
const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
|
|
2901
|
+
const warmup = {
|
|
2902
|
+
...await groupOnce(lab, cell, environment, -1),
|
|
2903
|
+
warmup: true
|
|
2904
|
+
};
|
|
2905
|
+
say(line(cell, warmup, measure(cell, warmup)));
|
|
2906
|
+
const runs = [];
|
|
2907
|
+
for (let index = 0; index < count; index += 1) {
|
|
2908
|
+
const run = await groupOnce(lab, cell, environment, index);
|
|
2909
|
+
runs.push(run);
|
|
2910
|
+
say(line(cell, run, measure(cell, run)));
|
|
2911
|
+
}
|
|
2912
|
+
return {
|
|
2913
|
+
manifest,
|
|
2914
|
+
runs,
|
|
2915
|
+
warmup
|
|
2916
|
+
};
|
|
2917
|
+
} finally {
|
|
2918
|
+
await lab.close();
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
/**
|
|
1846
2922
|
* This function does the runs of a cell. They go across new environments, and each environment records
|
|
1847
2923
|
* its manifest. Then the verdict judges the runs. A median across the edge of the band extends
|
|
1848
2924
|
* the cell from 11 runs to 22, and then to 44, each extension in new environments. With a
|
|
@@ -1891,6 +2967,6 @@ const runCell = async (cell, say = () => {}) => {
|
|
|
1891
2967
|
}
|
|
1892
2968
|
};
|
|
1893
2969
|
//#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 };
|
|
2970
|
+
export { CONVERGE_MS, LISTING, NOISE, PUMP, RELATIVE, STAGES, STEPS, crossingsOf, deadlineOf, deviceOf, endpointAfter, errorsOf, fieldsOf, groupOnce, groupVerdictOf, harnessImage, infoOf, isHopPlace, judge, lagsOf, measure, measureGroup, medianInterval, memberConfig, nodeClientImage, openGroupLab, openLab, peersOf, quantileOf, reorderingOf, routerImage, routesOf, runCell, sentinelConfig, socketOf, spread, spreadOf, stagesOf, subscriptionsOf, templated, toxicBody, verdictOf, viewOf, writesOf };
|
|
1895
2971
|
|
|
1896
2972
|
//# sourceMappingURL=index.js.map
|