@mark1russell7/harness 0.7.0 → 0.8.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 +32 -2
- package/build/index.d.ts +203 -6
- package/build/index.js +304 -28
- 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
|
@@ -119,7 +119,8 @@ const middle = (sorted) => {
|
|
|
119
119
|
const verdictOf = (prediction, runs) => {
|
|
120
120
|
const band = verdictOfBand(prediction, runs);
|
|
121
121
|
const counted = runs.filter((run) => run.void === null);
|
|
122
|
-
const
|
|
122
|
+
const asked = counted.some((run) => run.request !== void 0) ? withRequests(band, counted) : band;
|
|
123
|
+
const verdict = withProgram(asked, counted);
|
|
123
124
|
if (prediction.group === void 0 && !counted.some((run) => run.group !== void 0)) return verdict;
|
|
124
125
|
return {
|
|
125
126
|
...verdict,
|
|
@@ -127,6 +128,43 @@ const verdictOf = (prediction, runs) => {
|
|
|
127
128
|
};
|
|
128
129
|
};
|
|
129
130
|
/**
|
|
131
|
+
* This function adds the reconnect of the client and the outcomes of its call in flight to a
|
|
132
|
+
* verdict. Its reason gets a sentence for each. A verdict whose runs have neither stays as it is.
|
|
133
|
+
*/
|
|
134
|
+
const withProgram = (verdict, counted) => {
|
|
135
|
+
const reconnects = counted.flatMap((run) => run.reconnect === void 0 ? [] : [run.reconnect]);
|
|
136
|
+
const calls = counted.flatMap((run) => run.call === void 0 ? [] : [run.call]);
|
|
137
|
+
const lefts = counted.flatMap((run) => run.left === void 0 ? [] : [run.left]);
|
|
138
|
+
if (reconnects.length === 0 && calls.length === 0 && lefts.length === 0) return verdict;
|
|
139
|
+
const ms = (spread) => spread === null ? "none" : `${Math.round(valueIn(spread.median, "s") * 1e6) / 1e3} ms`;
|
|
140
|
+
const tries = Math.max(0, ...reconnects.map((each) => each.waits.length));
|
|
141
|
+
const reconnect = reconnects.length === 0 ? void 0 : {
|
|
142
|
+
waits: Array.from({ length: tries }, (_, at) => spreadOf(reconnects.flatMap((each) => each.waits[at] === void 0 ? [] : [each.waits[at]]))),
|
|
143
|
+
syn: spreadOf(reconnects.flatMap((each) => each.syn === null ? [] : [each.syn])),
|
|
144
|
+
established: spreadOf(reconnects.flatMap((each) => each.established === null ? [] : [each.established])),
|
|
145
|
+
connect: spreadOf(reconnects.flatMap((each) => each.connects))
|
|
146
|
+
};
|
|
147
|
+
const outcomes = {};
|
|
148
|
+
for (const call of calls) outcomes[call.outcome] = (outcomes[call.outcome] ?? 0) + 1;
|
|
149
|
+
const gone = lefts.flatMap((each) => each === null ? [] : [each]);
|
|
150
|
+
const left = lefts.length === 0 ? void 0 : {
|
|
151
|
+
spread: spreadOf(gone),
|
|
152
|
+
stayed: lefts.length - gone.length
|
|
153
|
+
};
|
|
154
|
+
const said = [
|
|
155
|
+
...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}`}.`],
|
|
156
|
+
...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.`],
|
|
157
|
+
...calls.length === 0 ? [] : [`Calls in flight: ${Object.entries(outcomes).map(([outcome, n]) => `${n} ${outcome}`).join("; ")}.`]
|
|
158
|
+
];
|
|
159
|
+
return {
|
|
160
|
+
...verdict,
|
|
161
|
+
...reconnect === void 0 ? {} : { reconnect },
|
|
162
|
+
...calls.length === 0 ? {} : { calls: outcomes },
|
|
163
|
+
...left === void 0 ? {} : { left },
|
|
164
|
+
why: `${verdict.why.endsWith(".") ? verdict.why : `${verdict.why}.`} ${said.join(" ")}`
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
130
168
|
* This function adds the outcomes of the second request of an HTTP client to a verdict. A cell
|
|
131
169
|
* whose outcome is a distribution gets them in its reason too.
|
|
132
170
|
*/
|
|
@@ -383,6 +421,150 @@ const verdictOfBand = (prediction, runs) => {
|
|
|
383
421
|
};
|
|
384
422
|
};
|
|
385
423
|
//#endregion
|
|
424
|
+
//#region src/application.ts
|
|
425
|
+
/**
|
|
426
|
+
* This module reads the acts of the program of the observer: its heartbeat, its drop of the
|
|
427
|
+
* connection, its reconnect and its call in flight. The kernel has no report of a detection by
|
|
428
|
+
* a program. Thus the program writes a line before each of its own acts, on CLOCK_MONOTONIC:
|
|
429
|
+
* - `ping` before each ping of its heartbeat, and `drop` before it closes the connection by itself.
|
|
430
|
+
* - `reconnecting` when it starts its reconnect timer, and `dial` before each new connection.
|
|
431
|
+
*
|
|
432
|
+
* The kernel then stamps each write of the program on the socket (`tx`) and each change of the
|
|
433
|
+
* state of the socket. A line of the client names its socket by its local address (`local`), and
|
|
434
|
+
* a line of a server names it by its peer (`peer`).
|
|
435
|
+
*/
|
|
436
|
+
/** This function gives the interval between two instants on the one clock, in seconds to the microsecond. */
|
|
437
|
+
const between$3 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
|
|
438
|
+
/** This function gives the state of a state record after the change, and before it. TCP_ESTABLISHED is 1, TCP_SYN_SENT 2. */
|
|
439
|
+
const newState$2 = (record) => record.new;
|
|
440
|
+
const oldState$2 = (record) => record.old;
|
|
441
|
+
/** This function gives the lines of the program of the observer: the client, or the target for the server. A proxy has no lines. */
|
|
442
|
+
const programLines = (cell, run) => {
|
|
443
|
+
const observer = cell.observer ?? "client";
|
|
444
|
+
if (observer === "client") return run.events;
|
|
445
|
+
if (observer === "server") return run.server ?? [];
|
|
446
|
+
return [];
|
|
447
|
+
};
|
|
448
|
+
/**
|
|
449
|
+
* This function tells if a line of the program is about the connection of the run. The client
|
|
450
|
+
* names it by its local address, and a server by its peer. A line with neither is about the one
|
|
451
|
+
* connection of its program. A line whose address is null is about a socket that did not open:
|
|
452
|
+
* a try that failed.
|
|
453
|
+
*/
|
|
454
|
+
const onConnection = (line, local) => {
|
|
455
|
+
if (!("local" in line) && !("peer" in line)) return true;
|
|
456
|
+
return (line["local"] ?? line["peer"]) === local;
|
|
457
|
+
};
|
|
458
|
+
/** This function gives the first record of a socket that matches, at the stamp of a line or in δ_abs after it. */
|
|
459
|
+
const kernelAt = (socket, line, matches) => {
|
|
460
|
+
const noise = valueIn(NOISE, "s") * 1e9;
|
|
461
|
+
return socket.find((record) => record.mono >= line.t && record.mono <= line.t + noise && matches(record));
|
|
462
|
+
};
|
|
463
|
+
const eventOf = (socket, line, matches) => {
|
|
464
|
+
const record = kernelAt(socket, line, matches);
|
|
465
|
+
return {
|
|
466
|
+
line,
|
|
467
|
+
at: record?.mono ?? line.t,
|
|
468
|
+
kernel: record !== void 0,
|
|
469
|
+
record: record === void 0 ? null : record.ev === "tx" ? "write" : "state"
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
/**
|
|
473
|
+
* This function gives the time from a line of the program to the close of the socket by its own
|
|
474
|
+
* side. That close is its change from ESTABLISHED to FIN_WAIT1, or to CLOSE (an abort). A FIN of
|
|
475
|
+
* the peer (CLOSE_WAIT) is not such a close. It is null when the socket did not close itself.
|
|
476
|
+
*/
|
|
477
|
+
const leftAfter = (socket, line) => {
|
|
478
|
+
const left = socket.find((record) => record.ev === "state" && oldState$2(record) === 1 && (newState$2(record) === 4 || newState$2(record) === 7) && record.mono >= line.t);
|
|
479
|
+
return left === void 0 ? null : between$3(line.t, left.mono);
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* This function tells how a drop differs from the way that its prediction gives, or gives null.
|
|
483
|
+
* A close closes the socket in δ_abs. A close frame is a write, and the program does not close
|
|
484
|
+
* the socket at the drop. A peer that answers the close frame can close it, and that is no finding.
|
|
485
|
+
*/
|
|
486
|
+
const dropFinding = (expected, drop, left) => {
|
|
487
|
+
const noise = valueIn(NOISE, "s");
|
|
488
|
+
const soon = left !== null && valueIn(left, "s") <= noise;
|
|
489
|
+
if (expected === "close") {
|
|
490
|
+
if (left === null) return "the program did not close its socket after the drop, where a close is at once";
|
|
491
|
+
return soon ? null : `the program closed its socket ${left.value} s after the drop, not at once`;
|
|
492
|
+
}
|
|
493
|
+
if (drop.record !== "write") return "the drop wrote no close frame on its socket";
|
|
494
|
+
return soon ? "the program closed its socket at the drop, where a close frame leaves it open" : null;
|
|
495
|
+
};
|
|
496
|
+
const isWrite = (record) => record.ev === "tx";
|
|
497
|
+
/** This function gives each ping of the program on the connection of the run, with the kernel's stamp of its write. */
|
|
498
|
+
const pingsOf = (lines, socket, local) => lines.filter((line) => line.ev === "ping" && onConnection(line, local)).map((line) => eventOf(socket, line, isWrite));
|
|
499
|
+
/**
|
|
500
|
+
* This function gives the first ping that got no answer: no segment with data reached the socket
|
|
501
|
+
* after its write, up to `until`. Its write starts the wait of the heartbeat that the fault met.
|
|
502
|
+
*/
|
|
503
|
+
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));
|
|
504
|
+
/**
|
|
505
|
+
* This function gives the first drop of the connection by its program. The kernel stamps it as the
|
|
506
|
+
* write of a close frame. Or it stamps the change of the socket out of ESTABLISHED: a close or an
|
|
507
|
+
* abort.
|
|
508
|
+
*/
|
|
509
|
+
const dropOf = (lines, socket, local) => {
|
|
510
|
+
const line = lines.find((each) => each.ev === "drop" && onConnection(each, local));
|
|
511
|
+
return line === void 0 ? void 0 : eventOf(socket, line, (record) => isWrite(record) || record.ev === "state" && oldState$2(record) === 1);
|
|
512
|
+
};
|
|
513
|
+
/**
|
|
514
|
+
* This function reads the reconnect of the client after the detection at `detected`. The socket of
|
|
515
|
+
* the run has the cookie `cookie`. The lines of the program count from `from`: the drop line of a
|
|
516
|
+
* drop, which comes before the kernel's stamp of it.
|
|
517
|
+
*/
|
|
518
|
+
const reconnectOf = (run, detected, cookie, from = detected) => {
|
|
519
|
+
const after = run.events.filter((line) => line.t > from);
|
|
520
|
+
const starts = after.filter((line) => line.ev === "reconnecting");
|
|
521
|
+
const dials = after.filter((line) => line.ev === "dial");
|
|
522
|
+
const waits = starts.flatMap((start, at) => {
|
|
523
|
+
const next = starts[at + 1]?.t ?? Number.POSITIVE_INFINITY;
|
|
524
|
+
const dial = dials.find((each) => each.t >= start.t && each.t < next);
|
|
525
|
+
return dial === void 0 ? [] : [between$3(start.t, dial.t)];
|
|
526
|
+
});
|
|
527
|
+
const fresh = run.kernel.filter((record) => record.netns === run.netns && record.cookie !== cookie && record.ev === "state" && record.mono > detected);
|
|
528
|
+
const syns = fresh.filter((record) => newState$2(record) === 2);
|
|
529
|
+
const established = fresh.find((record) => newState$2(record) === 1);
|
|
530
|
+
const connects = dials.flatMap((dial) => {
|
|
531
|
+
const syn = syns.find((record) => record.mono >= dial.t);
|
|
532
|
+
return syn === void 0 ? [] : [between$3(dial.t, syn.mono)];
|
|
533
|
+
});
|
|
534
|
+
return {
|
|
535
|
+
waits,
|
|
536
|
+
syn: syns[0] === void 0 ? null : between$3(detected, syns[0].mono),
|
|
537
|
+
established: established === void 0 ? null : between$3(detected, established.mono),
|
|
538
|
+
connects
|
|
539
|
+
};
|
|
540
|
+
};
|
|
541
|
+
/** This function tells how the waits of a reconnect differ from their bands, each widened by δ_abs, or gives null. */
|
|
542
|
+
const reconnectFinding = (expected, read) => {
|
|
543
|
+
const noise = valueIn(NOISE, "s");
|
|
544
|
+
const round = (value) => Math.round(value * 1e6) / 1e6;
|
|
545
|
+
const found = expected.waits.flatMap((band, at) => {
|
|
546
|
+
const wait = read.waits[at];
|
|
547
|
+
if (wait === void 0) return [`the client made no try ${at + 1} after the detection`];
|
|
548
|
+
const value = valueIn(wait, "s");
|
|
549
|
+
const low = round(valueIn(band.low, "s") - noise);
|
|
550
|
+
const high = round(valueIn(band.high, "s") + noise);
|
|
551
|
+
return value < low || value > high ? [`its wait before try ${at + 1} was ${value} s, outside [${low}, ${high}] s`] : [];
|
|
552
|
+
});
|
|
553
|
+
return found.length === 0 ? null : found.join(". ");
|
|
554
|
+
};
|
|
555
|
+
/** This function reads the call that the client sent into the fault: its `call-error` or `data` line after the request. */
|
|
556
|
+
const callOf = (events, request, detected) => {
|
|
557
|
+
const end = events.find((line) => (line.ev === "call-error" || line.ev === "data") && line.t > request.t);
|
|
558
|
+
if (end === void 0) return {
|
|
559
|
+
outcome: "pending",
|
|
560
|
+
after: null
|
|
561
|
+
};
|
|
562
|
+
return {
|
|
563
|
+
outcome: end.ev === "data" ? "a reply" : String(end["message"] ?? end["code"] ?? "an error"),
|
|
564
|
+
after: detected === void 0 ? null : between$3(detected, end.t)
|
|
565
|
+
};
|
|
566
|
+
};
|
|
567
|
+
//#endregion
|
|
386
568
|
//#region src/driver.ts
|
|
387
569
|
/** These are the errno numbers that the kernel reports to a socket, by the names that predictions use. */
|
|
388
570
|
const ERRNO$1 = {
|
|
@@ -1118,9 +1300,10 @@ const newState = (record) => record.new;
|
|
|
1118
1300
|
const count = (event, field) => event === void 0 ? 0 : Number(event[field] ?? 0);
|
|
1119
1301
|
/** This function gives the view of the client on the same interval: TCP_INFO, with a sample each 2 ms. */
|
|
1120
1302
|
const socketVantage = (cell, events, ready, request) => {
|
|
1121
|
-
|
|
1303
|
+
const { from } = cell.prediction.detects === true ? cell.prediction : { from: void 0 };
|
|
1304
|
+
if (from === void 0 || from === "peer-closed" || from === "fault" || from === "ping-sent") return null;
|
|
1122
1305
|
const ended = events.find((event) => event.ev === "error" || event.ev === "eof");
|
|
1123
|
-
const reference =
|
|
1306
|
+
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
1307
|
return ended === void 0 || reference === void 0 ? null : between(reference.t, ended.t);
|
|
1125
1308
|
};
|
|
1126
1309
|
const ipOf = (address) => address.split(":")[0];
|
|
@@ -1418,21 +1601,28 @@ const measureSocket = (cell, run) => {
|
|
|
1418
1601
|
const seen = remote ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
|
|
1419
1602
|
const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8);
|
|
1420
1603
|
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
|
|
1604
|
+
const lines = programLines(cell, run);
|
|
1605
|
+
const kernelEnded = reported?.mono ?? closeWait?.mono ?? own?.mono ?? seen?.t;
|
|
1606
|
+
const first = dropOf(lines, socket, local);
|
|
1607
|
+
const drop = first !== void 0 && (kernelEnded === void 0 || first.at <= kernelEnded) ? first : void 0;
|
|
1608
|
+
const dropped = drop !== void 0;
|
|
1609
|
+
const outcome = dropped ? { kind: "dropped" } : reported !== void 0 ? {
|
|
1422
1610
|
kind: "error",
|
|
1423
1611
|
errno: ERRNO[reported.err] ?? `E${reported.err}`
|
|
1424
1612
|
} : closeWait !== void 0 || seen?.ev === "eof" ? { kind: "eof" } : own !== void 0 ? { kind: "closed" } : seen !== void 0 ? {
|
|
1425
1613
|
kind: "error",
|
|
1426
1614
|
errno: String(seen["errno"])
|
|
1427
1615
|
} : { kind: "silent" };
|
|
1428
|
-
const ended =
|
|
1429
|
-
const noticed = reported !== void 0 && seen !== void 0 ? between(reported.mono, seen.t) : null;
|
|
1616
|
+
const ended = drop?.at ?? kernelEnded;
|
|
1617
|
+
const noticed = reported !== void 0 && seen !== void 0 && !dropped ? between(reported.mono, seen.t) : null;
|
|
1430
1618
|
const { prediction } = cell;
|
|
1619
|
+
const pings = pingsOf(lines, socket, local);
|
|
1431
1620
|
const before = run.fault?.t1 ?? ready.t;
|
|
1432
1621
|
const referenceAt = (from) => {
|
|
1433
1622
|
if (from === "request-sent") return request.t;
|
|
1434
1623
|
if (from === "fault") return run.fault?.t0;
|
|
1435
1624
|
if (from === "client-start") return (events.find((event) => event.ev === "connecting") ?? events[0])?.t;
|
|
1625
|
+
if (from === "ping-sent") return firstUnanswered(pings, socket, ended)?.at;
|
|
1436
1626
|
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
1627
|
};
|
|
1438
1628
|
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 +1671,22 @@ const measureSocket = (cell, run) => {
|
|
|
1481
1671
|
}
|
|
1482
1672
|
if (!prediction.detects && prediction.recovers !== void 0) {
|
|
1483
1673
|
const { recovers } = prediction;
|
|
1674
|
+
const start = cell.probe.mode === "idle" ? lines.find((line) => line.ev === "ping" && line.t >= (run.fault?.t1 ?? 0)) ?? request : request;
|
|
1484
1675
|
const heal = run.heal ?? null;
|
|
1485
1676
|
if (voided === null && heal === null) voided = "the stall never ended";
|
|
1486
1677
|
else if (voided === null && heal !== null) {
|
|
1487
|
-
const from = between(
|
|
1488
|
-
const to = between(
|
|
1678
|
+
const from = between(start.t, heal.t0);
|
|
1679
|
+
const to = between(start.t, heal.t1);
|
|
1489
1680
|
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
1681
|
}
|
|
1491
|
-
const steps = retransmitted.filter((record) => record.mono >
|
|
1682
|
+
const steps = retransmitted.filter((record) => record.mono > start.t).map((record) => between(start.t, record.mono));
|
|
1492
1683
|
if (ended !== void 0) return {
|
|
1493
1684
|
...base,
|
|
1494
1685
|
...nothing,
|
|
1495
1686
|
outcome,
|
|
1496
1687
|
steps,
|
|
1497
1688
|
void: voided,
|
|
1498
|
-
finding: `detected after ${between(
|
|
1689
|
+
finding: `detected after ${between(start.t, ended).value} s, where it should recover`
|
|
1499
1690
|
};
|
|
1500
1691
|
const answer = heal === null ? void 0 : socket.find((record) => record.ev === "rx" && record.mono > heal.t0);
|
|
1501
1692
|
if (answer === void 0) return {
|
|
@@ -1505,13 +1696,13 @@ const measureSocket = (cell, run) => {
|
|
|
1505
1696
|
void: voided,
|
|
1506
1697
|
finding: "it never moved again: nothing came back after the stall ended"
|
|
1507
1698
|
};
|
|
1508
|
-
const reply = events.find((event) => event.ev === "data" && event.t >
|
|
1509
|
-
voided ??= delayed(
|
|
1699
|
+
const reply = events.find((event) => event.ev === "data" && event.t > start.t);
|
|
1700
|
+
voided ??= delayed(start.t, answer.mono, "retransmit");
|
|
1510
1701
|
return {
|
|
1511
1702
|
...base,
|
|
1512
1703
|
outcome: { kind: "answered" },
|
|
1513
|
-
elapsed: between(
|
|
1514
|
-
socket: reply === void 0 ? null : between(
|
|
1704
|
+
elapsed: between(start.t, answer.mono),
|
|
1705
|
+
socket: reply === void 0 ? null : between(start.t, reply.t),
|
|
1515
1706
|
noticed: reply === void 0 ? null : between(answer.mono, reply.t),
|
|
1516
1707
|
steps,
|
|
1517
1708
|
void: voided,
|
|
@@ -1601,23 +1792,41 @@ const measureSocket = (cell, run) => {
|
|
|
1601
1792
|
void: voided,
|
|
1602
1793
|
finding: `it ended without its reference event (${prediction.from})`
|
|
1603
1794
|
};
|
|
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
|
|
1795
|
+
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));
|
|
1796
|
+
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;
|
|
1797
|
+
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";
|
|
1798
|
+
const reconnect = prediction.reconnects === void 0 ? void 0 : reconnectOf(run, ended, cookie, drop?.line.t ?? ended);
|
|
1799
|
+
const late = reconnect === void 0 || prediction.reconnects === void 0 ? null : reconnectFinding(prediction.reconnects, reconnect);
|
|
1800
|
+
const left = prediction.drop === void 0 || drop === void 0 ? void 0 : leftAfter(socket, drop.line);
|
|
1801
|
+
const how = prediction.drop === void 0 || drop === void 0 || left === void 0 ? null : dropFinding(prediction.drop, drop, left);
|
|
1802
|
+
const found = [
|
|
1803
|
+
matches ? null : `it ended with ${words}, not ${prediction.errno}`,
|
|
1804
|
+
late,
|
|
1805
|
+
how
|
|
1806
|
+
].filter((each) => each !== null);
|
|
1807
|
+
const finding = found.length === 0 ? null : found.join(". ");
|
|
1606
1808
|
const programTimer = cell.probe.http !== void 0 && prediction.from === "last-received" && prediction.timer !== "keepalive";
|
|
1607
1809
|
if (prediction.from === "last-received" && !programTimer || prediction.from === "first-retransmission") voided ??= delayed(reference, ended, prediction.from === "last-received" ? "keepalive" : "retransmit");
|
|
1810
|
+
if (prediction.from === "ping-sent") voided ??= delayed(reference, ended, "retransmit");
|
|
1608
1811
|
const lag = (valueIn(prediction.high, "s") - valueIn(prediction.low, "s")) * 1e9;
|
|
1609
1812
|
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
1813
|
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
1814
|
const commit = run.fault === null ? 0 : (run.fault.t1 - run.fault.t0) / 1e6;
|
|
1612
1815
|
if (prediction.from === "fault" && commit > valueIn(NOISE, "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`;
|
|
1816
|
+
const unanswered = prediction.from === "ping-sent" ? firstUnanswered(pings, socket, ended) : void 0;
|
|
1817
|
+
const endLine = drop?.line ?? seen;
|
|
1818
|
+
const view = unanswered !== void 0 ? endLine === void 0 ? null : between(unanswered.line.t, endLine.t) : remote ? null : socketVantage(cell, events, ready, request);
|
|
1613
1819
|
return {
|
|
1614
1820
|
...base,
|
|
1615
1821
|
...chain,
|
|
1616
1822
|
outcome,
|
|
1617
1823
|
elapsed: between(reference, ended),
|
|
1618
|
-
socket:
|
|
1824
|
+
socket: view,
|
|
1619
1825
|
noticed,
|
|
1620
1826
|
steps,
|
|
1827
|
+
...reconnect === void 0 ? {} : { reconnect },
|
|
1828
|
+
...left === void 0 ? {} : { left },
|
|
1829
|
+
...cell.probe.ws !== void 0 && inflight ? { call: callOf(events, request, ended) } : {},
|
|
1621
1830
|
void: voided,
|
|
1622
1831
|
finding
|
|
1623
1832
|
};
|
|
@@ -1679,6 +1888,12 @@ const MONGO = {
|
|
|
1679
1888
|
dockerfile: "harness/image/mongo/Dockerfile",
|
|
1680
1889
|
from: ["harness/image/mongo"]
|
|
1681
1890
|
};
|
|
1891
|
+
/** 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. */
|
|
1892
|
+
const PROCEDURES = {
|
|
1893
|
+
name: "loom-harness-procedures",
|
|
1894
|
+
dockerfile: "harness/image/procedures/Dockerfile",
|
|
1895
|
+
from: ["harness/image/procedures"]
|
|
1896
|
+
};
|
|
1682
1897
|
/** This image is the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow. */
|
|
1683
1898
|
const ROUTER = {
|
|
1684
1899
|
name: "loom-harness-router",
|
|
@@ -1751,6 +1966,10 @@ const goHttpImage = () => build(GO_HTTP);
|
|
|
1751
1966
|
const nodeHttpImage = () => build(NODE_HTTP);
|
|
1752
1967
|
/** This function gives the image of the MongoDB client. */
|
|
1753
1968
|
const mongoClientImage = () => build(MONGO);
|
|
1969
|
+
/** This function gives the image of `@procedures/client`: its WebSocket client and its server. */
|
|
1970
|
+
const proceduresImage = () => build(PROCEDURES);
|
|
1971
|
+
/** This function gives the image that a target operates: its stock image, or an image of the harness (`Target.harness`). */
|
|
1972
|
+
const targetImageOf = (target) => target.harness === "procedures" ? proceduresImage() : Promise.resolve(target.image);
|
|
1754
1973
|
/**
|
|
1755
1974
|
* This time is the interval of the listing of a router, when its hop does not set one. A listing
|
|
1756
1975
|
* evicts each expired flow that it walks past, and the DESTROY event of that flow follows.
|
|
@@ -1908,6 +2127,11 @@ const clientOf = async (kind, image) => {
|
|
|
1908
2127
|
command: ["node", "/opt/harness/mongo/client-mongo.mjs"],
|
|
1909
2128
|
clientKind: kind
|
|
1910
2129
|
};
|
|
2130
|
+
if (kind === "procedures-ws") return {
|
|
2131
|
+
clientImage: await proceduresImage(),
|
|
2132
|
+
command: ["node", "/opt/harness/procedures/client-ws.mjs"],
|
|
2133
|
+
clientKind: kind
|
|
2134
|
+
};
|
|
1911
2135
|
return {
|
|
1912
2136
|
clientImage: image,
|
|
1913
2137
|
command: ["/opt/harness/client"],
|
|
@@ -1968,7 +2192,8 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1968
2192
|
try {
|
|
1969
2193
|
for (let at = 0; at <= hops.length; at += 1) networks.push(await new Network().start());
|
|
1970
2194
|
const last = networks[hops.length];
|
|
1971
|
-
|
|
2195
|
+
const targetImage = await targetImageOf(target);
|
|
2196
|
+
let stock = new GenericContainer(targetImage).withNetwork(last).withWaitStrategy(Wait.forLogMessage(target.ready));
|
|
1972
2197
|
if (target.files !== void 0) stock = stock.withCopyContentToContainer(Object.entries(target.files).map(([path, content]) => ({
|
|
1973
2198
|
content,
|
|
1974
2199
|
target: path
|
|
@@ -2502,7 +2727,11 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
2502
2727
|
boot_id: await output(client, ["cat", "/proc/sys/kernel/random/boot_id"]),
|
|
2503
2728
|
docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
|
|
2504
2729
|
target: {
|
|
2505
|
-
image:
|
|
2730
|
+
image: targetImage,
|
|
2731
|
+
...target.harness === void 0 ? {} : {
|
|
2732
|
+
harness: target.harness,
|
|
2733
|
+
base: target.image
|
|
2734
|
+
},
|
|
2506
2735
|
id: (await inspect(server)).Image,
|
|
2507
2736
|
settings,
|
|
2508
2737
|
sysctls: targetSysctls,
|
|
@@ -3857,6 +4086,20 @@ const probeArgs = (cell, lab) => {
|
|
|
3857
4086
|
"--mode",
|
|
3858
4087
|
probe.mode
|
|
3859
4088
|
];
|
|
4089
|
+
if (probe.ws !== void 0) {
|
|
4090
|
+
const { ws } = probe;
|
|
4091
|
+
const waits = (prediction.detects === true ? prediction.reconnects?.waits ?? [] : []).reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
|
|
4092
|
+
args.push("--websocket", ws.socket);
|
|
4093
|
+
args.push("--heartbeat", ws.heartbeat === null ? "off" : "on");
|
|
4094
|
+
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));
|
|
4095
|
+
args.push("--reconnect", ws.reconnect === null ? "off" : "on");
|
|
4096
|
+
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));
|
|
4097
|
+
args.push("--request-timeout-ms", String(valueIn(ws.requestTimeout, "ms")));
|
|
4098
|
+
args.push("--connection-timeout-ms", String(valueIn(ws.connectionTimeout, "ms")));
|
|
4099
|
+
if (ws.hold !== void 0) args.push("--hold-ms", String(valueIn(ws.hold, "ms")));
|
|
4100
|
+
args.push("--max-s", String(Math.ceil((longest + waits) * 2 + 10)));
|
|
4101
|
+
return args;
|
|
4102
|
+
}
|
|
3860
4103
|
if (probe.http !== void 0) {
|
|
3861
4104
|
const { http } = probe;
|
|
3862
4105
|
const gap = http.gap === void 0 ? 0 : valueIn(http.gap, "s");
|
|
@@ -3894,14 +4137,15 @@ const probeArgs = (cell, lab) => {
|
|
|
3894
4137
|
/**
|
|
3895
4138
|
* This function waits for an error on the socket of the observer, or for its end of file
|
|
3896
4139
|
* (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.
|
|
4140
|
+
* of a topology, waits for that. A target with its own detector can also drop the connection
|
|
4141
|
+
* itself (`dropped`). The promise resolves after `within` ms at the latest.
|
|
3898
4142
|
*/
|
|
3899
|
-
const observerError = (cell, partial, within) => new Promise((resolve) => {
|
|
4143
|
+
const observerError = (cell, partial, within, dropped = () => false) => new Promise((resolve) => {
|
|
3900
4144
|
const started = Date.now();
|
|
3901
4145
|
const look = () => {
|
|
3902
4146
|
const run = partial();
|
|
3903
4147
|
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();
|
|
4148
|
+
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
4149
|
else setTimeout(look, 50);
|
|
3906
4150
|
};
|
|
3907
4151
|
look();
|
|
@@ -4000,6 +4244,31 @@ const beforeOnce = async (lab, cell, environment, index) => {
|
|
|
4000
4244
|
}
|
|
4001
4245
|
};
|
|
4002
4246
|
};
|
|
4247
|
+
/** 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. */
|
|
4248
|
+
const pingSent = (probe, after, within) => new Promise((resolve, reject) => {
|
|
4249
|
+
const started = Date.now();
|
|
4250
|
+
const look = () => {
|
|
4251
|
+
const ping = probe.events.find((event) => event.ev === "ping" && event.t >= after);
|
|
4252
|
+
if (ping !== void 0) resolve(ping);
|
|
4253
|
+
else if (Date.now() - started > within) reject(/* @__PURE__ */ new Error(`the client sent no ping into the stall within ${within} ms`));
|
|
4254
|
+
else setTimeout(look, 1);
|
|
4255
|
+
};
|
|
4256
|
+
look();
|
|
4257
|
+
});
|
|
4258
|
+
/**
|
|
4259
|
+
* This function waits until the client wrote `count` lines `dial`, one before each connection
|
|
4260
|
+
* that it tries, or until its deadline. It resolves after `within` ms at the latest.
|
|
4261
|
+
*/
|
|
4262
|
+
const dials = (probe, count, within) => new Promise((resolve) => {
|
|
4263
|
+
const started = Date.now();
|
|
4264
|
+
const look = () => {
|
|
4265
|
+
const tried = probe.events.filter((event) => event.ev === "dial").length;
|
|
4266
|
+
const ended = probe.events.some((event) => event.ev === "deadline");
|
|
4267
|
+
if (tried >= count || ended || Date.now() - started > within) resolve();
|
|
4268
|
+
else setTimeout(look, 20);
|
|
4269
|
+
};
|
|
4270
|
+
look();
|
|
4271
|
+
});
|
|
4003
4272
|
/** This function gives the netem specification of a cell: its latency, then the netem of its fault when it has one. */
|
|
4004
4273
|
const netemSpec = (cell, fault) => {
|
|
4005
4274
|
const parts = [...cell.latency === void 0 ? [] : [`delay ${valueIn(cell.latency, "ms")}ms`], ...fault === void 0 ? [] : [fault]];
|
|
@@ -4162,13 +4431,14 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
4162
4431
|
await probe.until(HTTP_DONE, (gap + window) * 2e3 + 1e4).catch(() => void 0);
|
|
4163
4432
|
} else if (cell.fault === "stall") {
|
|
4164
4433
|
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
4434
|
const window = valueIn(prediction.detects ? prediction.high : prediction.window, "s");
|
|
4435
|
+
const sent = cell.probe.mode === "idle" ? await pingSent(probe, fault?.t1 ?? 0, window * 1e3) : await requestSent(probe, 5e3);
|
|
4436
|
+
heal = await lab.heal(faultAt, sent.t + valueIn(cell.stall, "s") * 1e9);
|
|
4168
4437
|
await probe.until([
|
|
4169
4438
|
"data",
|
|
4170
4439
|
"error",
|
|
4171
|
-
"eof"
|
|
4440
|
+
"eof",
|
|
4441
|
+
"drop"
|
|
4172
4442
|
], window * 1e3).catch(() => void 0);
|
|
4173
4443
|
} else if (prediction.detects === null) {
|
|
4174
4444
|
const window = valueIn(prediction.window, "s") * 1e3;
|
|
@@ -4180,12 +4450,13 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
4180
4450
|
], window).catch(() => void 0);
|
|
4181
4451
|
} else if (prediction.detects) {
|
|
4182
4452
|
const within = valueIn(prediction.high, "s") * 2e3 + 1e4;
|
|
4183
|
-
if (cell.observer !== void 0 && cell.observer !== "client") await observerError(cell, partial, within);
|
|
4453
|
+
if (cell.observer !== void 0 && cell.observer !== "client") await observerError(cell, partial, within, () => lab.server.slice(serverFrom).some((line) => line.ev === "drop"));
|
|
4184
4454
|
else if (http && cell.probe.mode !== "idle") await probe.until(HTTP_DONE, within).catch(() => void 0);
|
|
4185
4455
|
else await probe.until([
|
|
4186
4456
|
"error",
|
|
4187
4457
|
"eof",
|
|
4188
4458
|
"deadline",
|
|
4459
|
+
"drop",
|
|
4189
4460
|
...http ? ["closed"] : []
|
|
4190
4461
|
], within).catch(() => void 0);
|
|
4191
4462
|
} else await sleep(valueIn(prediction.window, "s") * 1e3);
|
|
@@ -4193,7 +4464,12 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
4193
4464
|
await lift();
|
|
4194
4465
|
if (toxicFirst) await lab.toxic(null);
|
|
4195
4466
|
}
|
|
4196
|
-
if (cell.fault === "freeze-process" && !server) await probe.until(["data"], 5e3).catch(() => void 0);
|
|
4467
|
+
if (cell.fault === "freeze-process" && !server && cell.probe.mode !== "idle") await probe.until(["data"], 5e3).catch(() => void 0);
|
|
4468
|
+
const expected = cell.prediction.detects === true ? cell.prediction.reconnects : void 0;
|
|
4469
|
+
if (expected !== void 0) {
|
|
4470
|
+
const waits = expected.waits.reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
|
|
4471
|
+
await dials(probe, 1 + expected.waits.length, waits * 2e3 + 5e3);
|
|
4472
|
+
}
|
|
4197
4473
|
}
|
|
4198
4474
|
} finally {
|
|
4199
4475
|
await probe.stop();
|
|
@@ -4370,6 +4646,6 @@ const runCell = async (cell, say = () => {}) => {
|
|
|
4370
4646
|
}
|
|
4371
4647
|
};
|
|
4372
4648
|
//#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 };
|
|
4649
|
+
export { CONVERGE_MS, 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, deviceOf, dropFinding, dropOf, endpointAfter, errorsOf, fieldOf, fieldsOf, firstUnanswered, goHttpImage, groupOnce, groupVerdictOf, harnessImage, infoOf, initiateScript, isHopPlace, judge, lagsOf, leftAfter, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, nodeClientImage, nodeHttpImage, 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
4650
|
|
|
4375
4651
|
//# sourceMappingURL=index.js.map
|