@mark1russell7/harness 0.7.0 → 0.9.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 +37 -3
- package/build/index.d.ts +256 -8
- package/build/index.js +398 -51
- package/build/index.js.map +1 -1
- package/image/procedures/Dockerfile +13 -0
- package/image/procedures/client-ws.mjs +234 -0
- package/image/procedures/lines.mjs +68 -0
- package/image/procedures/npm-lock.json +125 -0
- package/image/procedures/package.json +9 -0
- package/image/procedures/server-ws.mjs +94 -0
- package/package.json +8 -2
package/build/index.js
CHANGED
|
@@ -20,6 +20,25 @@ const requestWords = (outcome) => `${outcome.connection}${outcome.retried ? ", a
|
|
|
20
20
|
* the harness, the kernel or Docker.
|
|
21
21
|
*/
|
|
22
22
|
const NOISE = quantity(4, "ms");
|
|
23
|
+
/**
|
|
24
|
+
* This time is how late the injector can commit or lift a fault before the run is void. Stage 0
|
|
25
|
+
* measures the commit apart from δ_abs, and the commit is longer. On a hosted runner it took up to
|
|
26
|
+
* 2.75 ms at 4 environments, where δ_abs is 1 ms. Thus a cell's δ_abs does not change it.
|
|
27
|
+
*/
|
|
28
|
+
const INJECT = NOISE;
|
|
29
|
+
/** This function gives the δ_abs of a cell: its own, or `NOISE` for a cell that gives none. */
|
|
30
|
+
const noiseOf = (cell) => cell.noise ?? NOISE;
|
|
31
|
+
/**
|
|
32
|
+
* This function tells if a timer firing came late enough to void a run. A firing two jiffies or
|
|
33
|
+
* more behind its wheel slot came late because the host stopped a vCPU. With the HZ of the run,
|
|
34
|
+
* the rule is in time: the delay must also reach δ_abs, because each band already includes that
|
|
35
|
+
* noise. At HZ 250 two jiffies are 8 ms, more than a δ_abs of 4 ms. At HZ 1000 they are 2 ms.
|
|
36
|
+
*/
|
|
37
|
+
const delayVoids = (behind, hz, noise) => {
|
|
38
|
+
if (behind < 2) return false;
|
|
39
|
+
if (hz === void 0 || !(hz > 0)) return true;
|
|
40
|
+
return behind / hz >= Math.max(2 / hz, valueIn(noise, "s"));
|
|
41
|
+
};
|
|
23
42
|
/** This fraction, δ_rel, is how far the median of a cell can be outside the prediction and stay equivalent. */
|
|
24
43
|
const RELATIVE = .05;
|
|
25
44
|
/** These are the run counts of a cell: 11, then 22 and 44 while its median crosses the edge of the band. */
|
|
@@ -116,14 +135,52 @@ const middle = (sorted) => {
|
|
|
116
135
|
* finding. The distribution test (a DKW band) needs a predicted distribution from a simulator,
|
|
117
136
|
* which does not exist at this time.
|
|
118
137
|
*/
|
|
119
|
-
const verdictOf = (prediction, runs) => {
|
|
120
|
-
const band = verdictOfBand(prediction, runs);
|
|
138
|
+
const verdictOf = (prediction, runs, noise = NOISE) => {
|
|
139
|
+
const band = verdictOfBand(prediction, runs, noise);
|
|
121
140
|
const counted = runs.filter((run) => run.void === null);
|
|
122
|
-
const
|
|
141
|
+
const asked = counted.some((run) => run.request !== void 0) ? withRequests(band, counted) : band;
|
|
142
|
+
const verdict = withProgram(asked, counted);
|
|
123
143
|
if (prediction.group === void 0 && !counted.some((run) => run.group !== void 0)) return verdict;
|
|
124
144
|
return {
|
|
125
145
|
...verdict,
|
|
126
|
-
group: groupVerdictOf(prediction, counted)
|
|
146
|
+
group: groupVerdictOf(prediction, counted, noise)
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* This function adds the reconnect of the client and the outcomes of its call in flight to a
|
|
151
|
+
* verdict. Its reason gets a sentence for each. A verdict whose runs have neither stays as it is.
|
|
152
|
+
*/
|
|
153
|
+
const withProgram = (verdict, counted) => {
|
|
154
|
+
const reconnects = counted.flatMap((run) => run.reconnect === void 0 ? [] : [run.reconnect]);
|
|
155
|
+
const calls = counted.flatMap((run) => run.call === void 0 ? [] : [run.call]);
|
|
156
|
+
const lefts = counted.flatMap((run) => run.left === void 0 ? [] : [run.left]);
|
|
157
|
+
if (reconnects.length === 0 && calls.length === 0 && lefts.length === 0) return verdict;
|
|
158
|
+
const ms = (spread) => spread === null ? "none" : `${Math.round(valueIn(spread.median, "s") * 1e6) / 1e3} ms`;
|
|
159
|
+
const tries = Math.max(0, ...reconnects.map((each) => each.waits.length));
|
|
160
|
+
const reconnect = reconnects.length === 0 ? void 0 : {
|
|
161
|
+
waits: Array.from({ length: tries }, (_, at) => spreadOf(reconnects.flatMap((each) => each.waits[at] === void 0 ? [] : [each.waits[at]]))),
|
|
162
|
+
syn: spreadOf(reconnects.flatMap((each) => each.syn === null ? [] : [each.syn])),
|
|
163
|
+
established: spreadOf(reconnects.flatMap((each) => each.established === null ? [] : [each.established])),
|
|
164
|
+
connect: spreadOf(reconnects.flatMap((each) => each.connects))
|
|
165
|
+
};
|
|
166
|
+
const outcomes = {};
|
|
167
|
+
for (const call of calls) outcomes[call.outcome] = (outcomes[call.outcome] ?? 0) + 1;
|
|
168
|
+
const gone = lefts.flatMap((each) => each === null ? [] : [each]);
|
|
169
|
+
const left = lefts.length === 0 ? void 0 : {
|
|
170
|
+
spread: spreadOf(gone),
|
|
171
|
+
stayed: lefts.length - gone.length
|
|
172
|
+
};
|
|
173
|
+
const said = [
|
|
174
|
+
...left === void 0 ? [] : [`After the drop, the program closed its socket in ${gone.length} runs, median ${ms(left.spread)} after its drop line${left.stayed === 0 ? "" : `, and it did not close it in ${left.stayed}`}.`],
|
|
175
|
+
...reconnect === void 0 ? [] : [`Reconnect: the waits before each try, median ${reconnect.waits.map(ms).join(", ")}; the first SYN median ${ms(reconnect.syn)} and ESTABLISHED median ${ms(reconnect.established)} after the detection.`],
|
|
176
|
+
...calls.length === 0 ? [] : [`Calls in flight: ${Object.entries(outcomes).map(([outcome, n]) => `${n} ${outcome}`).join("; ")}.`]
|
|
177
|
+
];
|
|
178
|
+
return {
|
|
179
|
+
...verdict,
|
|
180
|
+
...reconnect === void 0 ? {} : { reconnect },
|
|
181
|
+
...calls.length === 0 ? {} : { calls: outcomes },
|
|
182
|
+
...left === void 0 ? {} : { left },
|
|
183
|
+
why: `${verdict.why.endsWith(".") ? verdict.why : `${verdict.why}.`} ${said.join(" ")}`
|
|
127
184
|
};
|
|
128
185
|
};
|
|
129
186
|
/**
|
|
@@ -146,8 +203,8 @@ const withRequests = (verdict, counted) => {
|
|
|
146
203
|
/** This function gives the median of some counts, or null for none. */
|
|
147
204
|
const medianCount = (values) => values.length === 0 ? null : middle([...values].sort((a, b) => a - b));
|
|
148
205
|
/** 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(
|
|
206
|
+
const groupVerdictOf = (prediction, counted, delta = NOISE) => {
|
|
207
|
+
const noise = valueIn(delta, "s");
|
|
151
208
|
const stamps = prediction.group?.stamps === void 0 ? 0 : valueIn(prediction.group.stamps, "s");
|
|
152
209
|
const stages = (prediction.group?.stages ?? []).map((band) => {
|
|
153
210
|
const interval = {
|
|
@@ -223,7 +280,7 @@ const groupVerdictOf = (prediction, counted) => {
|
|
|
223
280
|
};
|
|
224
281
|
};
|
|
225
282
|
/** This function gives the verdict of the band of a cell: its containment and the equivalence of its median. */
|
|
226
|
-
const verdictOfBand = (prediction, runs) => {
|
|
283
|
+
const verdictOfBand = (prediction, runs, delta = NOISE) => {
|
|
227
284
|
const counted = runs.filter((run) => run.void === null);
|
|
228
285
|
const voided = runs.length - counted.length;
|
|
229
286
|
const outside = counted.filter((run) => run.finding !== null).map((run) => ({
|
|
@@ -317,7 +374,7 @@ const verdictOfBand = (prediction, runs) => {
|
|
|
317
374
|
};
|
|
318
375
|
}
|
|
319
376
|
const timed = prediction.detects ? prediction : prediction.recovers ?? prediction.replies;
|
|
320
|
-
const noise = valueIn(
|
|
377
|
+
const noise = valueIn(delta, "s");
|
|
321
378
|
const low = valueIn(timed.low, "s");
|
|
322
379
|
const high = valueIn(timed.high, "s");
|
|
323
380
|
const band = {
|
|
@@ -383,6 +440,150 @@ const verdictOfBand = (prediction, runs) => {
|
|
|
383
440
|
};
|
|
384
441
|
};
|
|
385
442
|
//#endregion
|
|
443
|
+
//#region src/application.ts
|
|
444
|
+
/**
|
|
445
|
+
* This module reads the acts of the program of the observer: its heartbeat, its drop of the
|
|
446
|
+
* connection, its reconnect and its call in flight. The kernel has no report of a detection by
|
|
447
|
+
* a program. Thus the program writes a line before each of its own acts, on CLOCK_MONOTONIC:
|
|
448
|
+
* - `ping` before each ping of its heartbeat, and `drop` before it closes the connection by itself.
|
|
449
|
+
* - `reconnecting` when it starts its reconnect timer, and `dial` before each new connection.
|
|
450
|
+
*
|
|
451
|
+
* The kernel then stamps each write of the program on the socket (`tx`) and each change of the
|
|
452
|
+
* state of the socket. A line of the client names its socket by its local address (`local`), and
|
|
453
|
+
* a line of a server names it by its peer (`peer`).
|
|
454
|
+
*/
|
|
455
|
+
/** This function gives the interval between two instants on the one clock, in seconds to the microsecond. */
|
|
456
|
+
const between$3 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
|
|
457
|
+
/** This function gives the state of a state record after the change, and before it. TCP_ESTABLISHED is 1, TCP_SYN_SENT 2. */
|
|
458
|
+
const newState$2 = (record) => record.new;
|
|
459
|
+
const oldState$2 = (record) => record.old;
|
|
460
|
+
/** This function gives the lines of the program of the observer: the client, or the target for the server. A proxy has no lines. */
|
|
461
|
+
const programLines = (cell, run) => {
|
|
462
|
+
const observer = cell.observer ?? "client";
|
|
463
|
+
if (observer === "client") return run.events;
|
|
464
|
+
if (observer === "server") return run.server ?? [];
|
|
465
|
+
return [];
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* This function tells if a line of the program is about the connection of the run. The client
|
|
469
|
+
* names it by its local address, and a server by its peer. A line with neither is about the one
|
|
470
|
+
* connection of its program. A line whose address is null is about a socket that did not open:
|
|
471
|
+
* a try that failed.
|
|
472
|
+
*/
|
|
473
|
+
const onConnection = (line, local) => {
|
|
474
|
+
if (!("local" in line) && !("peer" in line)) return true;
|
|
475
|
+
return (line["local"] ?? line["peer"]) === local;
|
|
476
|
+
};
|
|
477
|
+
/** This function gives the first record of a socket that matches, at the stamp of a line or in δ_abs after it. */
|
|
478
|
+
const kernelAt = (socket, line, matches, delta) => {
|
|
479
|
+
const noise = valueIn(delta, "s") * 1e9;
|
|
480
|
+
return socket.find((record) => record.mono >= line.t && record.mono <= line.t + noise && matches(record));
|
|
481
|
+
};
|
|
482
|
+
const eventOf = (socket, line, matches, delta = NOISE) => {
|
|
483
|
+
const record = kernelAt(socket, line, matches, delta);
|
|
484
|
+
return {
|
|
485
|
+
line,
|
|
486
|
+
at: record?.mono ?? line.t,
|
|
487
|
+
kernel: record !== void 0,
|
|
488
|
+
record: record === void 0 ? null : record.ev === "tx" ? "write" : "state"
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
/**
|
|
492
|
+
* This function gives the time from a line of the program to the close of the socket by its own
|
|
493
|
+
* side. That close is its change from ESTABLISHED to FIN_WAIT1, or to CLOSE (an abort). A FIN of
|
|
494
|
+
* the peer (CLOSE_WAIT) is not such a close. It is null when the socket did not close itself.
|
|
495
|
+
*/
|
|
496
|
+
const leftAfter = (socket, line) => {
|
|
497
|
+
const left = socket.find((record) => record.ev === "state" && oldState$2(record) === 1 && (newState$2(record) === 4 || newState$2(record) === 7) && record.mono >= line.t);
|
|
498
|
+
return left === void 0 ? null : between$3(line.t, left.mono);
|
|
499
|
+
};
|
|
500
|
+
/**
|
|
501
|
+
* This function tells how a drop differs from the way that its prediction gives, or gives null.
|
|
502
|
+
* A close closes the socket in δ_abs. A close frame is a write, and the program does not close
|
|
503
|
+
* the socket at the drop. A peer that answers the close frame can close it, and that is no finding.
|
|
504
|
+
*/
|
|
505
|
+
const dropFinding = (expected, drop, left, delta = NOISE) => {
|
|
506
|
+
const noise = valueIn(delta, "s");
|
|
507
|
+
const soon = left !== null && valueIn(left, "s") <= noise;
|
|
508
|
+
if (expected === "close") {
|
|
509
|
+
if (left === null) return "the program did not close its socket after the drop, where a close is at once";
|
|
510
|
+
return soon ? null : `the program closed its socket ${left.value} s after the drop, not at once`;
|
|
511
|
+
}
|
|
512
|
+
if (drop.record !== "write") return "the drop wrote no close frame on its socket";
|
|
513
|
+
return soon ? "the program closed its socket at the drop, where a close frame leaves it open" : null;
|
|
514
|
+
};
|
|
515
|
+
const isWrite = (record) => record.ev === "tx";
|
|
516
|
+
/** This function gives each ping of the program on the connection of the run, with the kernel's stamp of its write. */
|
|
517
|
+
const pingsOf = (lines, socket, local, delta = NOISE) => lines.filter((line) => line.ev === "ping" && onConnection(line, local)).map((line) => eventOf(socket, line, isWrite, delta));
|
|
518
|
+
/**
|
|
519
|
+
* This function gives the first ping that got no answer: no segment with data reached the socket
|
|
520
|
+
* after its write, up to `until`. Its write starts the wait of the heartbeat that the fault met.
|
|
521
|
+
*/
|
|
522
|
+
const firstUnanswered = (pings, socket, until = Number.POSITIVE_INFINITY) => pings.filter((ping) => ping.at <= until).find((ping) => !socket.some((record) => record.ev === "rx" && (record.seg?.len.value ?? 0) > 0 && record.mono > ping.at && record.mono <= until));
|
|
523
|
+
/**
|
|
524
|
+
* This function gives the first drop of the connection by its program. The kernel stamps it as the
|
|
525
|
+
* write of a close frame. Or it stamps the change of the socket out of ESTABLISHED: a close or an
|
|
526
|
+
* abort.
|
|
527
|
+
*/
|
|
528
|
+
const dropOf = (lines, socket, local, delta = NOISE) => {
|
|
529
|
+
const line = lines.find((each) => each.ev === "drop" && onConnection(each, local));
|
|
530
|
+
return line === void 0 ? void 0 : eventOf(socket, line, (record) => isWrite(record) || record.ev === "state" && oldState$2(record) === 1, delta);
|
|
531
|
+
};
|
|
532
|
+
/**
|
|
533
|
+
* This function reads the reconnect of the client after the detection at `detected`. The socket of
|
|
534
|
+
* the run has the cookie `cookie`. The lines of the program count from `from`: the drop line of a
|
|
535
|
+
* drop, which comes before the kernel's stamp of it.
|
|
536
|
+
*/
|
|
537
|
+
const reconnectOf = (run, detected, cookie, from = detected) => {
|
|
538
|
+
const after = run.events.filter((line) => line.t > from);
|
|
539
|
+
const starts = after.filter((line) => line.ev === "reconnecting");
|
|
540
|
+
const dials = after.filter((line) => line.ev === "dial");
|
|
541
|
+
const waits = starts.flatMap((start, at) => {
|
|
542
|
+
const next = starts[at + 1]?.t ?? Number.POSITIVE_INFINITY;
|
|
543
|
+
const dial = dials.find((each) => each.t >= start.t && each.t < next);
|
|
544
|
+
return dial === void 0 ? [] : [between$3(start.t, dial.t)];
|
|
545
|
+
});
|
|
546
|
+
const fresh = run.kernel.filter((record) => record.netns === run.netns && record.cookie !== cookie && record.ev === "state" && record.mono > detected);
|
|
547
|
+
const syns = fresh.filter((record) => newState$2(record) === 2);
|
|
548
|
+
const established = fresh.find((record) => newState$2(record) === 1);
|
|
549
|
+
const connects = dials.flatMap((dial) => {
|
|
550
|
+
const syn = syns.find((record) => record.mono >= dial.t);
|
|
551
|
+
return syn === void 0 ? [] : [between$3(dial.t, syn.mono)];
|
|
552
|
+
});
|
|
553
|
+
return {
|
|
554
|
+
waits,
|
|
555
|
+
syn: syns[0] === void 0 ? null : between$3(detected, syns[0].mono),
|
|
556
|
+
established: established === void 0 ? null : between$3(detected, established.mono),
|
|
557
|
+
connects
|
|
558
|
+
};
|
|
559
|
+
};
|
|
560
|
+
/** This function tells how the waits of a reconnect differ from their bands, each widened by δ_abs, or gives null. */
|
|
561
|
+
const reconnectFinding = (expected, read, delta = NOISE) => {
|
|
562
|
+
const noise = valueIn(delta, "s");
|
|
563
|
+
const round = (value) => Math.round(value * 1e6) / 1e6;
|
|
564
|
+
const found = expected.waits.flatMap((band, at) => {
|
|
565
|
+
const wait = read.waits[at];
|
|
566
|
+
if (wait === void 0) return [`the client made no try ${at + 1} after the detection`];
|
|
567
|
+
const value = valueIn(wait, "s");
|
|
568
|
+
const low = round(valueIn(band.low, "s") - noise);
|
|
569
|
+
const high = round(valueIn(band.high, "s") + noise);
|
|
570
|
+
return value < low || value > high ? [`its wait before try ${at + 1} was ${value} s, outside [${low}, ${high}] s`] : [];
|
|
571
|
+
});
|
|
572
|
+
return found.length === 0 ? null : found.join(". ");
|
|
573
|
+
};
|
|
574
|
+
/** This function reads the call that the client sent into the fault: its `call-error` or `data` line after the request. */
|
|
575
|
+
const callOf = (events, request, detected) => {
|
|
576
|
+
const end = events.find((line) => (line.ev === "call-error" || line.ev === "data") && line.t > request.t);
|
|
577
|
+
if (end === void 0) return {
|
|
578
|
+
outcome: "pending",
|
|
579
|
+
after: null
|
|
580
|
+
};
|
|
581
|
+
return {
|
|
582
|
+
outcome: end.ev === "data" ? "a reply" : String(end["message"] ?? end["code"] ?? "an error"),
|
|
583
|
+
after: detected === void 0 ? null : between$3(detected, end.t)
|
|
584
|
+
};
|
|
585
|
+
};
|
|
586
|
+
//#endregion
|
|
386
587
|
//#region src/driver.ts
|
|
387
588
|
/** These are the errno numbers that the kernel reports to a socket, by the names that predictions use. */
|
|
388
589
|
const ERRNO$1 = {
|
|
@@ -547,9 +748,9 @@ const settled = (cell, run) => {
|
|
|
547
748
|
* that found the socket held by its owner put itself off by HZ/20. Either way, the run measured
|
|
548
749
|
* something other than the timer.
|
|
549
750
|
*/
|
|
550
|
-
const delayed = (place, from, to) => {
|
|
751
|
+
const delayed = (place, from, to, hz, noise) => {
|
|
551
752
|
const firings = place.records.filter((record) => record.ev === "timer" && record.what === "keepalive" && record.mono >= from && record.mono <= to);
|
|
552
|
-
const late = firings.find((record) => (record.behind?.value ?? 0
|
|
753
|
+
const late = firings.find((record) => delayVoids(record.behind?.value ?? 0, hz, noise));
|
|
553
754
|
if (late !== void 0) return `its keepalive timer ran ${late.behind.value} jiffies behind its wheel slot: the VM delayed it, not the kernel`;
|
|
554
755
|
return firings.find((record) => record.owned === true) === void 0 ? null : "its keepalive timer found the socket held by its owner and put itself off by HZ/20: the run measured the owner, not the timer";
|
|
555
756
|
};
|
|
@@ -623,13 +824,13 @@ const measureDriver = (cell, run) => {
|
|
|
623
824
|
if (heal === null) voided = "the fault never lifted";
|
|
624
825
|
else {
|
|
625
826
|
const late = (heal.t0 - run.fault.t0) / 1e9 - valueIn(cell.hold, "s");
|
|
626
|
-
if (late > valueIn(
|
|
827
|
+
if (late > valueIn(INJECT, "s")) voided = `the lift came ${Math.round(late * 1e4) / 10} ms after its hold: the injector was late, not the client`;
|
|
627
828
|
}
|
|
628
829
|
}
|
|
629
830
|
const assumed = prediction.rtt;
|
|
630
831
|
if (voided === null && assumed !== void 0 && rtt !== null && valueIn(rtt, "ms") > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt.value} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
|
|
631
832
|
const findings = [];
|
|
632
|
-
const noise = valueIn(
|
|
833
|
+
const noise = valueIn(noiseOf(cell), "s");
|
|
633
834
|
const until = prediction.detects ? Infinity : after + valueIn(prediction.window, "s") * 1e9;
|
|
634
835
|
for (const match of driver.quiet ?? []) {
|
|
635
836
|
const line = events.find((event) => event.t > after && event.t <= until && matches(event, match));
|
|
@@ -719,7 +920,7 @@ const measureDriver = (cell, run) => {
|
|
|
719
920
|
void: voided,
|
|
720
921
|
finding: `it ended without its reference event (${prediction.from} on the ${driver.socket} socket)`
|
|
721
922
|
};
|
|
722
|
-
if (driver.ends === void 0 && prediction.from === "last-received" && place !== void 0) voided ??= delayed(place, reference, ended);
|
|
923
|
+
if (driver.ends === void 0 && prediction.from === "last-received" && place !== void 0) voided ??= delayed(place, reference, ended, run.hz, noiseOf(cell));
|
|
723
924
|
const name = outcome.kind === "error" ? outcome.errno : outcome.kind === "eof" ? "EOF" : "silent";
|
|
724
925
|
if (prediction.errno !== "OK" && name !== prediction.errno) findings.unshift(`it ended with ${name}, not ${prediction.errno}`);
|
|
725
926
|
const seen = report === void 0 ? void 0 : events.find((event) => event.t >= report.at && (event.ev === "error" || event.ev === "socket-error"));
|
|
@@ -1042,14 +1243,14 @@ const measureGroup = (cell, run) => {
|
|
|
1042
1243
|
if (heal === null) voided = "the fault never lifted";
|
|
1043
1244
|
else {
|
|
1044
1245
|
const late = (heal.t0 - t0) / 1e9 - valueIn(cell.hold, "s");
|
|
1045
|
-
if (late > valueIn(
|
|
1246
|
+
if (late > valueIn(INJECT, "s")) voided = `the lift came ${Math.round(late * 1e4) / 10} ms after its hold: the injector was late, not the group`;
|
|
1046
1247
|
}
|
|
1047
1248
|
}
|
|
1048
1249
|
const assumed = prediction.detects === null ? void 0 : prediction.rtt;
|
|
1049
1250
|
const first = kernel.find((record) => record.netns === run.netns && record.dst === `${run.group?.members[primary]?.ip}:${(cell.group ?? cell.replicaSet)?.port}` && record.tcp?.srtt !== void 0);
|
|
1050
1251
|
const rtt = first?.tcp?.srtt === void 0 ? null : valueIn(first.tcp.srtt, "us") / 1e3;
|
|
1051
1252
|
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`;
|
|
1052
|
-
if (voided === null && prediction.detects === true && prediction.from === "fault" && (run.fault.t1 - run.fault.t0) / 1e6 > valueIn(
|
|
1253
|
+
if (voided === null && prediction.detects === true && prediction.from === "fault" && (run.fault.t1 - run.fault.t0) / 1e6 > valueIn(INJECT, "ms")) voided = `the commit of the fault took ${Math.round((run.fault.t1 - run.fault.t0) / 1e5) / 10} ms, longer than the noise of the pipeline: its start is not known`;
|
|
1053
1254
|
let reference = t0;
|
|
1054
1255
|
if (prediction.detects === false && prediction.recovers !== void 0) {
|
|
1055
1256
|
const into = events.find((event) => event.ev === "sent" && event.t > run.fault.t1);
|
|
@@ -1071,7 +1272,7 @@ const measureGroup = (cell, run) => {
|
|
|
1071
1272
|
if (accepted !== void 0 && lands === "same" && to !== primary) findings.push(`the client wrote to ${to ?? String(accepted["at"])}, where it should stay on ${primary}`);
|
|
1072
1273
|
for (const error of expected?.errors ?? []) {
|
|
1073
1274
|
const met = group.errors.find((each) => each.name === error.name);
|
|
1074
|
-
const noise = valueIn(
|
|
1275
|
+
const noise = valueIn(noiseOf(cell), "s");
|
|
1075
1276
|
if (met === void 0) findings.push(`the client never met ${error.name}`);
|
|
1076
1277
|
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`);
|
|
1077
1278
|
}
|
|
@@ -1118,9 +1319,10 @@ const newState = (record) => record.new;
|
|
|
1118
1319
|
const count = (event, field) => event === void 0 ? 0 : Number(event[field] ?? 0);
|
|
1119
1320
|
/** This function gives the view of the client on the same interval: TCP_INFO, with a sample each 2 ms. */
|
|
1120
1321
|
const socketVantage = (cell, events, ready, request) => {
|
|
1121
|
-
|
|
1322
|
+
const { from } = cell.prediction.detects === true ? cell.prediction : { from: void 0 };
|
|
1323
|
+
if (from === void 0 || from === "peer-closed" || from === "fault" || from === "ping-sent") return null;
|
|
1122
1324
|
const ended = events.find((event) => event.ev === "error" || event.ev === "eof");
|
|
1123
|
-
const reference =
|
|
1325
|
+
const reference = from === "last-received" ? ready : from === "request-sent" ? request : events.find((event) => event.ev === "kernel" && event.t >= request.t && count(event, "retransmits") > 0);
|
|
1124
1326
|
return ended === void 0 || reference === void 0 ? null : between(reference.t, ended.t);
|
|
1125
1327
|
};
|
|
1126
1328
|
const ipOf = (address) => address.split(":")[0];
|
|
@@ -1354,7 +1556,7 @@ const measureSocket = (cell, run) => {
|
|
|
1354
1556
|
*/
|
|
1355
1557
|
const delayed = (from, to, timer) => {
|
|
1356
1558
|
const firings = socket.filter((record) => record.ev === "timer" && record.what === timer && record.mono >= from && record.mono <= to);
|
|
1357
|
-
const late = firings.find((record) => (record.behind?.value ?? 0
|
|
1559
|
+
const late = firings.find((record) => delayVoids(record.behind?.value ?? 0, run.hz, noiseOf(cell)));
|
|
1358
1560
|
if (late !== void 0) return `its ${timer} timer ran ${late.behind.value} jiffies behind its wheel slot: the VM delayed it, not the kernel`;
|
|
1359
1561
|
return firings.find((record) => timer === "keepalive" && record.owned === true) === void 0 ? null : "its keepalive timer found the socket held by its owner and put itself off by HZ/20: the run measured the owner, not the timer";
|
|
1360
1562
|
};
|
|
@@ -1417,22 +1619,29 @@ const measureSocket = (cell, run) => {
|
|
|
1417
1619
|
const reported = socket.find((record) => record.ev === "error" && (record.err ?? 0) !== 0);
|
|
1418
1620
|
const seen = remote ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
|
|
1419
1621
|
const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8);
|
|
1420
|
-
const own = cell.prediction.detects === true && cell.prediction.errno === "CLOSED" ? socket.find((record) => record.ev === "state" && oldState(record) === 1 && newState(record) === 4) : void 0;
|
|
1421
|
-
const
|
|
1622
|
+
const own = cell.prediction.detects === true && cell.prediction.errno === "CLOSED" || cell.prediction.detects === null && cell.prediction.ends === "own-close" ? socket.find((record) => record.ev === "state" && oldState(record) === 1 && newState(record) === 4) : void 0;
|
|
1623
|
+
const lines = programLines(cell, run);
|
|
1624
|
+
const kernelEnded = reported?.mono ?? closeWait?.mono ?? own?.mono ?? seen?.t;
|
|
1625
|
+
const first = dropOf(lines, socket, local, noiseOf(cell));
|
|
1626
|
+
const drop = first !== void 0 && (kernelEnded === void 0 || first.at <= kernelEnded) ? first : void 0;
|
|
1627
|
+
const dropped = drop !== void 0;
|
|
1628
|
+
const outcome = dropped ? { kind: "dropped" } : reported !== void 0 ? {
|
|
1422
1629
|
kind: "error",
|
|
1423
1630
|
errno: ERRNO[reported.err] ?? `E${reported.err}`
|
|
1424
1631
|
} : closeWait !== void 0 || seen?.ev === "eof" ? { kind: "eof" } : own !== void 0 ? { kind: "closed" } : seen !== void 0 ? {
|
|
1425
1632
|
kind: "error",
|
|
1426
1633
|
errno: String(seen["errno"])
|
|
1427
1634
|
} : { kind: "silent" };
|
|
1428
|
-
const ended =
|
|
1429
|
-
const noticed = reported !== void 0 && seen !== void 0 ? between(reported.mono, seen.t) : null;
|
|
1635
|
+
const ended = drop?.at ?? kernelEnded;
|
|
1636
|
+
const noticed = reported !== void 0 && seen !== void 0 && !dropped ? between(reported.mono, seen.t) : null;
|
|
1430
1637
|
const { prediction } = cell;
|
|
1638
|
+
const pings = pingsOf(lines, socket, local, noiseOf(cell));
|
|
1431
1639
|
const before = run.fault?.t1 ?? ready.t;
|
|
1432
1640
|
const referenceAt = (from) => {
|
|
1433
1641
|
if (from === "request-sent") return request.t;
|
|
1434
1642
|
if (from === "fault") return run.fault?.t0;
|
|
1435
1643
|
if (from === "client-start") return (events.find((event) => event.ev === "connecting") ?? events[0])?.t;
|
|
1644
|
+
if (from === "ping-sent") return firstUnanswered(pings, socket, ended)?.at;
|
|
1436
1645
|
return (from === "last-received" ? socket.filter((each) => each.ev === "rx" && each.mono <= before).at(-1) : from === "first-retransmission" ? retransmitted.find((each) => each.why === "rto") : kernel.find((each) => each.netns === peerNetns && each.cookie === peerCookie && each.ev === "state" && oldState(each) === 1))?.mono;
|
|
1437
1646
|
};
|
|
1438
1647
|
const lastRx = kernel.filter((each) => each.netns === run.netns && each.src === local && each.ev === "rx" && each.mono <= before).at(-1)?.mono ?? ready.t;
|
|
@@ -1481,21 +1690,22 @@ const measureSocket = (cell, run) => {
|
|
|
1481
1690
|
}
|
|
1482
1691
|
if (!prediction.detects && prediction.recovers !== void 0) {
|
|
1483
1692
|
const { recovers } = prediction;
|
|
1693
|
+
const start = cell.probe.mode === "idle" ? lines.find((line) => line.ev === "ping" && line.t >= (run.fault?.t1 ?? 0)) ?? request : request;
|
|
1484
1694
|
const heal = run.heal ?? null;
|
|
1485
1695
|
if (voided === null && heal === null) voided = "the stall never ended";
|
|
1486
1696
|
else if (voided === null && heal !== null) {
|
|
1487
|
-
const from = between(
|
|
1488
|
-
const to = between(
|
|
1697
|
+
const from = between(start.t, heal.t0);
|
|
1698
|
+
const to = between(start.t, heal.t1);
|
|
1489
1699
|
if (valueIn(from, "s") <= valueIn(recovers.heal.low, "s") || valueIn(to, "s") >= valueIn(recovers.heal.high, "s")) voided = `the stall ended ${from.value}–${to.value} s after the send, outside the ${recovers.heal.low.value}–${recovers.heal.high.value} s its prediction assumed`;
|
|
1490
1700
|
}
|
|
1491
|
-
const steps = retransmitted.filter((record) => record.mono >
|
|
1701
|
+
const steps = retransmitted.filter((record) => record.mono > start.t).map((record) => between(start.t, record.mono));
|
|
1492
1702
|
if (ended !== void 0) return {
|
|
1493
1703
|
...base,
|
|
1494
1704
|
...nothing,
|
|
1495
1705
|
outcome,
|
|
1496
1706
|
steps,
|
|
1497
1707
|
void: voided,
|
|
1498
|
-
finding: `detected after ${between(
|
|
1708
|
+
finding: `detected after ${between(start.t, ended).value} s, where it should recover`
|
|
1499
1709
|
};
|
|
1500
1710
|
const answer = heal === null ? void 0 : socket.find((record) => record.ev === "rx" && record.mono > heal.t0);
|
|
1501
1711
|
if (answer === void 0) return {
|
|
@@ -1505,13 +1715,13 @@ const measureSocket = (cell, run) => {
|
|
|
1505
1715
|
void: voided,
|
|
1506
1716
|
finding: "it never moved again: nothing came back after the stall ended"
|
|
1507
1717
|
};
|
|
1508
|
-
const reply = events.find((event) => event.ev === "data" && event.t >
|
|
1509
|
-
voided ??= delayed(
|
|
1718
|
+
const reply = events.find((event) => event.ev === "data" && event.t > start.t);
|
|
1719
|
+
voided ??= delayed(start.t, answer.mono, "retransmit");
|
|
1510
1720
|
return {
|
|
1511
1721
|
...base,
|
|
1512
1722
|
outcome: { kind: "answered" },
|
|
1513
|
-
elapsed: between(
|
|
1514
|
-
socket: reply === void 0 ? null : between(
|
|
1723
|
+
elapsed: between(start.t, answer.mono),
|
|
1724
|
+
socket: reply === void 0 ? null : between(start.t, reply.t),
|
|
1515
1725
|
noticed: reply === void 0 ? null : between(answer.mono, reply.t),
|
|
1516
1726
|
steps,
|
|
1517
1727
|
void: voided,
|
|
@@ -1561,7 +1771,7 @@ const measureSocket = (cell, run) => {
|
|
|
1561
1771
|
if (after !== null) finding = `a router forgot the flow ${after.value} s after the last segment, where it should keep it`;
|
|
1562
1772
|
} else if (after === null) finding = "no router forgot the flow in the window, where it should forget it";
|
|
1563
1773
|
else {
|
|
1564
|
-
const noise = valueIn(
|
|
1774
|
+
const noise = valueIn(noiseOf(cell), "s");
|
|
1565
1775
|
const low = valueIn(expected.low, "s") - noise;
|
|
1566
1776
|
const high = valueIn(expected.high, "s") + noise;
|
|
1567
1777
|
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`;
|
|
@@ -1601,23 +1811,41 @@ const measureSocket = (cell, run) => {
|
|
|
1601
1811
|
void: voided,
|
|
1602
1812
|
finding: `it ended without its reference event (${prediction.from})`
|
|
1603
1813
|
};
|
|
1604
|
-
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));
|
|
1605
|
-
const
|
|
1814
|
+
const steps = prediction.from === "ping-sent" ? pings.filter((ping) => ping.at > reference && ping.at <= ended).map((ping) => between(reference, ping.at)) : (prediction.from === "last-received" ? keepalives : prediction.from === "first-retransmission" ? retransmitted.filter((record) => record.why === "rto" && record.mono > reference) : []).map((record) => between(reference, record.mono));
|
|
1815
|
+
const matches = prediction.errno === "EOF" ? outcome.kind === "eof" : prediction.errno === "CLOSED" ? outcome.kind === "closed" : prediction.errno === "DROPPED" ? outcome.kind === "dropped" : outcome.kind === "error" && outcome.errno === prediction.errno;
|
|
1816
|
+
const words = outcome.kind === "error" ? outcome.errno : outcome.kind === "closed" ? "its own close" : outcome.kind === "dropped" ? "a drop by its own program" : "end of file";
|
|
1817
|
+
const reconnect = prediction.reconnects === void 0 ? void 0 : reconnectOf(run, ended, cookie, drop?.line.t ?? ended);
|
|
1818
|
+
const late = reconnect === void 0 || prediction.reconnects === void 0 ? null : reconnectFinding(prediction.reconnects, reconnect, noiseOf(cell));
|
|
1819
|
+
const left = prediction.drop === void 0 || drop === void 0 ? void 0 : leftAfter(socket, drop.line);
|
|
1820
|
+
const how = prediction.drop === void 0 || drop === void 0 || left === void 0 ? null : dropFinding(prediction.drop, drop, left, noiseOf(cell));
|
|
1821
|
+
const found = [
|
|
1822
|
+
matches ? null : `it ended with ${words}, not ${prediction.errno}`,
|
|
1823
|
+
late,
|
|
1824
|
+
how
|
|
1825
|
+
].filter((each) => each !== null);
|
|
1826
|
+
const finding = found.length === 0 ? null : found.join(". ");
|
|
1606
1827
|
const programTimer = cell.probe.http !== void 0 && prediction.from === "last-received" && prediction.timer !== "keepalive";
|
|
1607
1828
|
if (prediction.from === "last-received" && !programTimer || prediction.from === "first-retransmission") voided ??= delayed(reference, ended, prediction.from === "last-received" ? "keepalive" : "retransmit");
|
|
1829
|
+
if (prediction.from === "ping-sent") voided ??= delayed(reference, ended, "retransmit");
|
|
1608
1830
|
const lag = (valueIn(prediction.high, "s") - valueIn(prediction.low, "s")) * 1e9;
|
|
1609
1831
|
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;
|
|
1610
1832
|
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`;
|
|
1611
1833
|
const commit = run.fault === null ? 0 : (run.fault.t1 - run.fault.t0) / 1e6;
|
|
1612
|
-
if (prediction.from === "fault" && commit > valueIn(
|
|
1834
|
+
if (prediction.from === "fault" && commit > valueIn(INJECT, "ms")) voided ??= `the commit of the fault took ${Math.round(commit * 10) / 10} ms, longer than the noise of the pipeline: its start is not known`;
|
|
1835
|
+
const unanswered = prediction.from === "ping-sent" ? firstUnanswered(pings, socket, ended) : void 0;
|
|
1836
|
+
const endLine = drop?.line ?? seen;
|
|
1837
|
+
const view = unanswered !== void 0 ? endLine === void 0 ? null : between(unanswered.line.t, endLine.t) : remote ? null : socketVantage(cell, events, ready, request);
|
|
1613
1838
|
return {
|
|
1614
1839
|
...base,
|
|
1615
1840
|
...chain,
|
|
1616
1841
|
outcome,
|
|
1617
1842
|
elapsed: between(reference, ended),
|
|
1618
|
-
socket:
|
|
1843
|
+
socket: view,
|
|
1619
1844
|
noticed,
|
|
1620
1845
|
steps,
|
|
1846
|
+
...reconnect === void 0 ? {} : { reconnect },
|
|
1847
|
+
...left === void 0 ? {} : { left },
|
|
1848
|
+
...cell.probe.ws !== void 0 && inflight ? { call: callOf(events, request, ended) } : {},
|
|
1621
1849
|
void: voided,
|
|
1622
1850
|
finding
|
|
1623
1851
|
};
|
|
@@ -1679,6 +1907,12 @@ const MONGO = {
|
|
|
1679
1907
|
dockerfile: "harness/image/mongo/Dockerfile",
|
|
1680
1908
|
from: ["harness/image/mongo"]
|
|
1681
1909
|
};
|
|
1910
|
+
/** This image holds `@procedures/client` 0.3.0 on Node 24.21.0: its WebSocket client as a client of the harness, and the script of its server. */
|
|
1911
|
+
const PROCEDURES = {
|
|
1912
|
+
name: "loom-harness-procedures",
|
|
1913
|
+
dockerfile: "harness/image/procedures/Dockerfile",
|
|
1914
|
+
from: ["harness/image/procedures"]
|
|
1915
|
+
};
|
|
1682
1916
|
/** This image is the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow. */
|
|
1683
1917
|
const ROUTER = {
|
|
1684
1918
|
name: "loom-harness-router",
|
|
@@ -1751,6 +1985,10 @@ const goHttpImage = () => build(GO_HTTP);
|
|
|
1751
1985
|
const nodeHttpImage = () => build(NODE_HTTP);
|
|
1752
1986
|
/** This function gives the image of the MongoDB client. */
|
|
1753
1987
|
const mongoClientImage = () => build(MONGO);
|
|
1988
|
+
/** This function gives the image of `@procedures/client`: its WebSocket client and its server. */
|
|
1989
|
+
const proceduresImage = () => build(PROCEDURES);
|
|
1990
|
+
/** This function gives the image that a target operates: its stock image, or an image of the harness (`Target.harness`). */
|
|
1991
|
+
const targetImageOf = (target) => target.harness === "procedures" ? proceduresImage() : Promise.resolve(target.image);
|
|
1754
1992
|
/**
|
|
1755
1993
|
* This time is the interval of the listing of a router, when its hop does not set one. A listing
|
|
1756
1994
|
* evicts each expired flow that it walks past, and the DESTROY event of that flow follows.
|
|
@@ -1869,6 +2107,42 @@ const endpointAfter = (hops, element) => {
|
|
|
1869
2107
|
return at;
|
|
1870
2108
|
};
|
|
1871
2109
|
const STOP = { timeout: 0 };
|
|
2110
|
+
/** These are the values of CONFIG_HZ that Linux offers. */
|
|
2111
|
+
const RATES = [
|
|
2112
|
+
100,
|
|
2113
|
+
250,
|
|
2114
|
+
300,
|
|
2115
|
+
1e3
|
|
2116
|
+
];
|
|
2117
|
+
/**
|
|
2118
|
+
* This function gives HZ from two readings of /proc/timer_list, one second apart: its `now at`
|
|
2119
|
+
* time in ns and its jiffies. It gives the nearest rate that Linux offers, or null when the
|
|
2120
|
+
* measurement is more than 5 % from each of them.
|
|
2121
|
+
*/
|
|
2122
|
+
const hzOf = (first, second) => {
|
|
2123
|
+
const seconds = (second.now - first.now) / 1e9;
|
|
2124
|
+
if (!(seconds > 0)) return null;
|
|
2125
|
+
const rate = (second.jiffies - first.jiffies) / seconds;
|
|
2126
|
+
const nearest = RATES.reduce((best, each) => Math.abs(each - rate) < Math.abs(best - rate) ? each : best);
|
|
2127
|
+
return Math.abs(nearest - rate) / nearest <= .05 ? String(nearest) : null;
|
|
2128
|
+
};
|
|
2129
|
+
/** This function measures HZ in a privileged container: jiffies against the time of the kernel, one second apart. */
|
|
2130
|
+
const measuredHz = async (container) => {
|
|
2131
|
+
const read = "awk '/^now at/ {now=$3} /^jiffies:/ {print now, $2; exit}' /proc/timer_list";
|
|
2132
|
+
const values = (await output(container, [
|
|
2133
|
+
"sh",
|
|
2134
|
+
"-c",
|
|
2135
|
+
`${read}; sleep 1; ${read}`
|
|
2136
|
+
]).catch(() => "")).split(/\s+/).filter((each) => each !== "").map(Number);
|
|
2137
|
+
if (values.length !== 4 || values.some((each) => !Number.isFinite(each))) return null;
|
|
2138
|
+
return hzOf({
|
|
2139
|
+
now: values[0],
|
|
2140
|
+
jiffies: values[1]
|
|
2141
|
+
}, {
|
|
2142
|
+
now: values[2],
|
|
2143
|
+
jiffies: values[3]
|
|
2144
|
+
});
|
|
2145
|
+
};
|
|
1872
2146
|
const output = async (container, argv) => {
|
|
1873
2147
|
const result = await container.exec(argv);
|
|
1874
2148
|
if (result.exitCode !== 0) throw new Error(`${argv.join(" ")} exited ${result.exitCode}: ${result.output.trim()}`);
|
|
@@ -1908,6 +2182,11 @@ const clientOf = async (kind, image) => {
|
|
|
1908
2182
|
command: ["node", "/opt/harness/mongo/client-mongo.mjs"],
|
|
1909
2183
|
clientKind: kind
|
|
1910
2184
|
};
|
|
2185
|
+
if (kind === "procedures-ws") return {
|
|
2186
|
+
clientImage: await proceduresImage(),
|
|
2187
|
+
command: ["node", "/opt/harness/procedures/client-ws.mjs"],
|
|
2188
|
+
clientKind: kind
|
|
2189
|
+
};
|
|
1911
2190
|
return {
|
|
1912
2191
|
clientImage: image,
|
|
1913
2192
|
command: ["/opt/harness/client"],
|
|
@@ -1968,7 +2247,8 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1968
2247
|
try {
|
|
1969
2248
|
for (let at = 0; at <= hops.length; at += 1) networks.push(await new Network().start());
|
|
1970
2249
|
const last = networks[hops.length];
|
|
1971
|
-
|
|
2250
|
+
const targetImage = await targetImageOf(target);
|
|
2251
|
+
let stock = new GenericContainer(targetImage).withNetwork(last).withWaitStrategy(Wait.forLogMessage(target.ready));
|
|
1972
2252
|
if (target.files !== void 0) stock = stock.withCopyContentToContainer(Object.entries(target.files).map(([path, content]) => ({
|
|
1973
2253
|
content,
|
|
1974
2254
|
target: path
|
|
@@ -2498,11 +2778,26 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
2498
2778
|
return {
|
|
2499
2779
|
kernel: await output(client, ["uname", "-r"]),
|
|
2500
2780
|
architecture: await output(client, ["uname", "-m"]),
|
|
2501
|
-
|
|
2781
|
+
...await (async () => {
|
|
2782
|
+
const configured = /^CONFIG_HZ=(\d+)$/m.exec(config)?.[1];
|
|
2783
|
+
if (configured !== void 0) return {
|
|
2784
|
+
hz: configured,
|
|
2785
|
+
hzFrom: "config"
|
|
2786
|
+
};
|
|
2787
|
+
const measured = await measuredHz(admin);
|
|
2788
|
+
return measured === null ? { hz: null } : {
|
|
2789
|
+
hz: measured,
|
|
2790
|
+
hzFrom: "measured"
|
|
2791
|
+
};
|
|
2792
|
+
})(),
|
|
2502
2793
|
boot_id: await output(client, ["cat", "/proc/sys/kernel/random/boot_id"]),
|
|
2503
2794
|
docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
|
|
2504
2795
|
target: {
|
|
2505
|
-
image:
|
|
2796
|
+
image: targetImage,
|
|
2797
|
+
...target.harness === void 0 ? {} : {
|
|
2798
|
+
harness: target.harness,
|
|
2799
|
+
base: target.image
|
|
2800
|
+
},
|
|
2506
2801
|
id: (await inspect(server)).Image,
|
|
2507
2802
|
settings,
|
|
2508
2803
|
sysctls: targetSysctls,
|
|
@@ -3839,7 +4134,7 @@ const judge = (cell, runs) => {
|
|
|
3839
4134
|
const measured = runs.filter((run) => run.warmup !== true).map((run) => measure(cell, run));
|
|
3840
4135
|
return {
|
|
3841
4136
|
measured,
|
|
3842
|
-
verdict: verdictOf(cell.prediction, measured)
|
|
4137
|
+
verdict: verdictOf(cell.prediction, measured, noiseOf(cell))
|
|
3843
4138
|
};
|
|
3844
4139
|
};
|
|
3845
4140
|
/** This function gives the count of lost records in the latest clock line of the prober, since it began. */
|
|
@@ -3857,6 +4152,20 @@ const probeArgs = (cell, lab) => {
|
|
|
3857
4152
|
"--mode",
|
|
3858
4153
|
probe.mode
|
|
3859
4154
|
];
|
|
4155
|
+
if (probe.ws !== void 0) {
|
|
4156
|
+
const { ws } = probe;
|
|
4157
|
+
const waits = (prediction.detects === true ? prediction.reconnects?.waits ?? [] : []).reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
|
|
4158
|
+
args.push("--websocket", ws.socket);
|
|
4159
|
+
args.push("--heartbeat", ws.heartbeat === null ? "off" : "on");
|
|
4160
|
+
if (ws.heartbeat !== null) args.push("--heartbeat-interval-ms", String(valueIn(ws.heartbeat.interval, "ms")), "--heartbeat-timeout-ms", String(valueIn(ws.heartbeat.timeout, "ms")), "--heartbeat-misses", String(ws.heartbeat.misses));
|
|
4161
|
+
args.push("--reconnect", ws.reconnect === null ? "off" : "on");
|
|
4162
|
+
if (ws.reconnect !== null) args.push("--reconnect-initial-ms", String(valueIn(ws.reconnect.initialDelay, "ms")), "--reconnect-multiplier", String(ws.reconnect.multiplier), "--reconnect-max-ms", String(valueIn(ws.reconnect.maxDelay, "ms")), "--reconnect-attempts", String(ws.reconnect.attempts), "--reconnect-jitter", String(ws.reconnect.jitter));
|
|
4163
|
+
args.push("--request-timeout-ms", String(valueIn(ws.requestTimeout, "ms")));
|
|
4164
|
+
args.push("--connection-timeout-ms", String(valueIn(ws.connectionTimeout, "ms")));
|
|
4165
|
+
if (ws.hold !== void 0) args.push("--hold-ms", String(valueIn(ws.hold, "ms")));
|
|
4166
|
+
args.push("--max-s", String(Math.ceil((longest + waits) * 2 + 10)));
|
|
4167
|
+
return args;
|
|
4168
|
+
}
|
|
3860
4169
|
if (probe.http !== void 0) {
|
|
3861
4170
|
const { http } = probe;
|
|
3862
4171
|
const gap = http.gap === void 0 ? 0 : valueIn(http.gap, "s");
|
|
@@ -3894,14 +4203,15 @@ const probeArgs = (cell, lab) => {
|
|
|
3894
4203
|
/**
|
|
3895
4204
|
* This function waits for an error on the socket of the observer, or for its end of file
|
|
3896
4205
|
* (CLOSE_WAIT, state 8). The prober records both. A cell whose observer is the server, or a proxy
|
|
3897
|
-
* of a topology, waits for that.
|
|
4206
|
+
* of a topology, waits for that. A target with its own detector can also drop the connection
|
|
4207
|
+
* itself (`dropped`). The promise resolves after `within` ms at the latest.
|
|
3898
4208
|
*/
|
|
3899
|
-
const observerError = (cell, partial, within) => new Promise((resolve) => {
|
|
4209
|
+
const observerError = (cell, partial, within, dropped = () => false) => new Promise((resolve) => {
|
|
3900
4210
|
const started = Date.now();
|
|
3901
4211
|
const look = () => {
|
|
3902
4212
|
const run = partial();
|
|
3903
4213
|
const at = socketOf(cell, run);
|
|
3904
|
-
if (at !== void 0 && run.kernel.some((record) => record.netns === at.netns && record.cookie === at.cookie && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && record.new === 8)) || Date.now() - started > within) resolve();
|
|
4214
|
+
if (at !== void 0 && run.kernel.some((record) => record.netns === at.netns && record.cookie === at.cookie && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && record.new === 8)) || dropped() || Date.now() - started > within) resolve();
|
|
3905
4215
|
else setTimeout(look, 50);
|
|
3906
4216
|
};
|
|
3907
4217
|
look();
|
|
@@ -4000,6 +4310,31 @@ const beforeOnce = async (lab, cell, environment, index) => {
|
|
|
4000
4310
|
}
|
|
4001
4311
|
};
|
|
4002
4312
|
};
|
|
4313
|
+
/** This function waits for the first ping of the heartbeat of the client at the instant `after` or later: its first ping into a stall. */
|
|
4314
|
+
const pingSent = (probe, after, within) => new Promise((resolve, reject) => {
|
|
4315
|
+
const started = Date.now();
|
|
4316
|
+
const look = () => {
|
|
4317
|
+
const ping = probe.events.find((event) => event.ev === "ping" && event.t >= after);
|
|
4318
|
+
if (ping !== void 0) resolve(ping);
|
|
4319
|
+
else if (Date.now() - started > within) reject(/* @__PURE__ */ new Error(`the client sent no ping into the stall within ${within} ms`));
|
|
4320
|
+
else setTimeout(look, 1);
|
|
4321
|
+
};
|
|
4322
|
+
look();
|
|
4323
|
+
});
|
|
4324
|
+
/**
|
|
4325
|
+
* This function waits until the client wrote `count` lines `dial`, one before each connection
|
|
4326
|
+
* that it tries, or until its deadline. It resolves after `within` ms at the latest.
|
|
4327
|
+
*/
|
|
4328
|
+
const dials = (probe, count, within) => new Promise((resolve) => {
|
|
4329
|
+
const started = Date.now();
|
|
4330
|
+
const look = () => {
|
|
4331
|
+
const tried = probe.events.filter((event) => event.ev === "dial").length;
|
|
4332
|
+
const ended = probe.events.some((event) => event.ev === "deadline");
|
|
4333
|
+
if (tried >= count || ended || Date.now() - started > within) resolve();
|
|
4334
|
+
else setTimeout(look, 20);
|
|
4335
|
+
};
|
|
4336
|
+
look();
|
|
4337
|
+
});
|
|
4003
4338
|
/** This function gives the netem specification of a cell: its latency, then the netem of its fault when it has one. */
|
|
4004
4339
|
const netemSpec = (cell, fault) => {
|
|
4005
4340
|
const parts = [...cell.latency === void 0 ? [] : [`delay ${valueIn(cell.latency, "ms")}ms`], ...fault === void 0 ? [] : [fault]];
|
|
@@ -4162,13 +4497,14 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
4162
4497
|
await probe.until(HTTP_DONE, (gap + window) * 2e3 + 1e4).catch(() => void 0);
|
|
4163
4498
|
} else if (cell.fault === "stall") {
|
|
4164
4499
|
if (cell.stall === void 0) throw new Error(`${cell.name} says no stall's length`);
|
|
4165
|
-
const sent = await requestSent(probe, 5e3);
|
|
4166
|
-
heal = await lab.heal(faultAt, sent.t + valueIn(cell.stall, "s") * 1e9);
|
|
4167
4500
|
const window = valueIn(prediction.detects ? prediction.high : prediction.window, "s");
|
|
4501
|
+
const sent = cell.probe.mode === "idle" ? await pingSent(probe, fault?.t1 ?? 0, window * 1e3) : await requestSent(probe, 5e3);
|
|
4502
|
+
heal = await lab.heal(faultAt, sent.t + valueIn(cell.stall, "s") * 1e9);
|
|
4168
4503
|
await probe.until([
|
|
4169
4504
|
"data",
|
|
4170
4505
|
"error",
|
|
4171
|
-
"eof"
|
|
4506
|
+
"eof",
|
|
4507
|
+
"drop"
|
|
4172
4508
|
], window * 1e3).catch(() => void 0);
|
|
4173
4509
|
} else if (prediction.detects === null) {
|
|
4174
4510
|
const window = valueIn(prediction.window, "s") * 1e3;
|
|
@@ -4180,12 +4516,13 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
4180
4516
|
], window).catch(() => void 0);
|
|
4181
4517
|
} else if (prediction.detects) {
|
|
4182
4518
|
const within = valueIn(prediction.high, "s") * 2e3 + 1e4;
|
|
4183
|
-
if (cell.observer !== void 0 && cell.observer !== "client") await observerError(cell, partial, within);
|
|
4519
|
+
if (cell.observer !== void 0 && cell.observer !== "client") await observerError(cell, partial, within, () => lab.server.slice(serverFrom).some((line) => line.ev === "drop"));
|
|
4184
4520
|
else if (http && cell.probe.mode !== "idle") await probe.until(HTTP_DONE, within).catch(() => void 0);
|
|
4185
4521
|
else await probe.until([
|
|
4186
4522
|
"error",
|
|
4187
4523
|
"eof",
|
|
4188
4524
|
"deadline",
|
|
4525
|
+
"drop",
|
|
4189
4526
|
...http ? ["closed"] : []
|
|
4190
4527
|
], within).catch(() => void 0);
|
|
4191
4528
|
} else await sleep(valueIn(prediction.window, "s") * 1e3);
|
|
@@ -4193,7 +4530,12 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
4193
4530
|
await lift();
|
|
4194
4531
|
if (toxicFirst) await lab.toxic(null);
|
|
4195
4532
|
}
|
|
4196
|
-
if (cell.fault === "freeze-process" && !server) await probe.until(["data"], 5e3).catch(() => void 0);
|
|
4533
|
+
if (cell.fault === "freeze-process" && !server && cell.probe.mode !== "idle") await probe.until(["data"], 5e3).catch(() => void 0);
|
|
4534
|
+
const expected = cell.prediction.detects === true ? cell.prediction.reconnects : void 0;
|
|
4535
|
+
if (expected !== void 0) {
|
|
4536
|
+
const waits = expected.waits.reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
|
|
4537
|
+
await dials(probe, 1 + expected.waits.length, waits * 2e3 + 5e3);
|
|
4538
|
+
}
|
|
4197
4539
|
}
|
|
4198
4540
|
} finally {
|
|
4199
4541
|
await probe.stop();
|
|
@@ -4348,8 +4690,13 @@ const runCell = async (cell, say = () => {}) => {
|
|
|
4348
4690
|
const done = await Promise.all(batch.map(({ count, env }) => inEnvironment(cell, env, count, say)));
|
|
4349
4691
|
for (const each of done) {
|
|
4350
4692
|
manifests.push(each.manifest);
|
|
4351
|
-
|
|
4352
|
-
|
|
4693
|
+
const hz = each.manifest.hz === null ? void 0 : Number(each.manifest.hz);
|
|
4694
|
+
const withHz = (run) => hz === void 0 ? run : {
|
|
4695
|
+
...run,
|
|
4696
|
+
hz
|
|
4697
|
+
};
|
|
4698
|
+
runs.push(...each.runs.map(withHz));
|
|
4699
|
+
warmups.push(withHz(each.warmup));
|
|
4353
4700
|
}
|
|
4354
4701
|
}
|
|
4355
4702
|
const { measured, verdict } = judge(cell, runs);
|
|
@@ -4370,6 +4717,6 @@ const runCell = async (cell, say = () => {}) => {
|
|
|
4370
4717
|
}
|
|
4371
4718
|
};
|
|
4372
4719
|
//#endregion
|
|
4373
|
-
export { CONVERGE_MS, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, NOISE, PUMP, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, clockOffsetOf, crossingsOf, cutOf, deadlineOf, deviceOf, endpointAfter, errorsOf, fieldOf, fieldsOf, goHttpImage, groupOnce, groupVerdictOf, harnessImage, infoOf, initiateScript, isHopPlace, judge, lagsOf, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, nodeClientImage, nodeHttpImage, onMonotonic, openGroupLab, openLab, openReplicaSetLab, peersOf, quantileOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subscriptionsOf, templated, toxicBody, verdictOf, viewOf, writesOf };
|
|
4720
|
+
export { CONVERGE_MS, INJECT, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, NOISE, PUMP, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, callOf, clockOffsetOf, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, dropFinding, dropOf, endpointAfter, errorsOf, fieldOf, fieldsOf, firstUnanswered, goHttpImage, groupOnce, groupVerdictOf, harnessImage, hzOf, infoOf, initiateScript, isHopPlace, judge, lagsOf, leftAfter, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, nodeClientImage, nodeHttpImage, noiseOf, onMonotonic, openGroupLab, openLab, openReplicaSetLab, peersOf, pingsOf, proceduresImage, programLines, quantileOf, reconnectFinding, reconnectOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subscriptionsOf, targetImageOf, templated, toxicBody, verdictOf, viewOf, writesOf };
|
|
4374
4721
|
|
|
4375
4722
|
//# sourceMappingURL=index.js.map
|