@mark1russell7/harness 0.1.0 → 0.2.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 +11 -0
- package/build/index.d.ts +102 -11
- package/build/index.js +193 -29
- package/build/index.js.map +1 -1
- package/image/Dockerfile +2 -1
- package/image/native/src/bin/client.rs +47 -8
- package/image/native/src/bin/inject.rs +212 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,14 @@ ioredis sets the keepalive of its socket itself, after it connects. Thus a cell
|
|
|
46
46
|
- **A reset** is a `REJECT --reject-with tcp-reset` rule in the namespace of the target, for the segments of the client. The next send of the client gets a reset one round trip later. It is the reference fault of the calibration.
|
|
47
47
|
- **A frozen process** is the cgroup freezer (`docker pause`), while the kernel stays up.
|
|
48
48
|
- **A stall** is a black hole that stops by itself, `Cell.stall` after the send of the request. The run reads the stamp of the client on the send. Then `inject clear PEER --at NS` holds the end in the VM until that instant (`lab.heal`). Thus the latency of a docker exec does not change the length of the stall.
|
|
49
|
+
- **An ICMP unreachable** (`unreachable`, `Cell.icmp`) answers each segment from the observer with host, net or port unreachable, or admin prohibited. Linux keeps it as a soft error, continues to retransmit, and gives the errno of the ICMP at the timeout. Host and net unreachable also revert the backoff (RFC 6069).
|
|
50
|
+
- **A simplex partition** (`simplex`, `Cell.drops`) drops the segments to the server, or the segments to the client, but not both.
|
|
51
|
+
- **A FIN** (`fin`) stops the process of the peer with SIGKILL while its kernel stays up. For the client, the target starts its server as the child of a shell (`Target.command`), and `Target.kill` stops it. For the server, the harness stops the container of the client.
|
|
52
|
+
- **An MTU black hole** (`mtu-blackhole`, `Cell.mtu`) drops each segment longer than the length, in the namespace that receives it.
|
|
53
|
+
- **netem** (`netem`, `Cell.netem`) is tc netem on the egress of the target: loss, duplication, reordering or corruption. The outcome is a distribution.
|
|
54
|
+
- **A latency** (`Cell.latency`) is tc netem delay on the egress of the target before the client connects. Thus the round trip and the RTO include it.
|
|
55
|
+
|
|
56
|
+
A length match and netem act on GSO super-packets of up to 64 KB, not on segments, while TSO, GSO and GRO are on. Thus the cells of `mtu-blackhole` and `netem` turn them off on the two ends (`lab.offload`).
|
|
49
57
|
|
|
50
58
|
Each run starts from kernels that keep nothing from the run before it (`lab.forget()`). When a connection closes, each kernel keeps TCP metrics for the address of its peer. These metrics set the first round trip of the next connection, and thus its RTO. The harness removes the `tcp_metrics` of the two namespaces before each run.
|
|
51
59
|
|
|
@@ -59,6 +67,8 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
|
|
|
59
67
|
- **Void runs.** A run is void when the timer that drives the measurement was two jiffies or more behind its wheel slot in the timed interval. The wheel does a slot in the tick when it is due. Thus such a firing measured the host, which stopped a vCPU, not the kernel. A keepalive timer that found its socket held also makes the run void. A run whose fault came too late is void too.
|
|
60
68
|
- **Findings.** A finding is a wrong errno, a detector that did not fire, or a detection where the prediction is silence.
|
|
61
69
|
- **Recovery.** After a stall (`prediction.recovers`), it times the send to the first segment back after the stall stopped. That segment is the answer of the server to the retransmission that got through. A run whose stall stopped outside the window of its prediction is void. A run where nothing came back is a finding.
|
|
70
|
+
- **An end of file.** A FIN counts from the change of state of the socket of the peer when it closes (`peer-closed`). It ends at the change of the socket of the observer to CLOSE_WAIT. The kernel stamps the two changes.
|
|
71
|
+
- **A stream.** In a stream (`probe.mode` is `stream`), the client sends `count` requests. The run keeps the time from each send to the first data after it (`samples`).
|
|
62
72
|
|
|
63
73
|
## The verdict
|
|
64
74
|
|
|
@@ -68,6 +78,7 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
|
|
|
68
78
|
- **Median equivalence.** The order-statistic interval of the median must be in the band, made 5 % wider.
|
|
69
79
|
- **Extension.** A median across the edge of the band extends the cell from 11 runs to 22, then 44. Then the verdict is inconclusive.
|
|
70
80
|
- **Recovery.** The recovery after a stall gets the same test as a detection, by its band. A detection is then a finding.
|
|
81
|
+
- **Measurements.** A cell whose outcome is a distribution (`detects: null`) has no band. Its decision is `measured`, and the verdict gives the median and the slowest reply time.
|
|
71
82
|
|
|
72
83
|
The distribution test needs a simulator, which does not exist at this time.
|
|
73
84
|
|
package/build/index.d.ts
CHANGED
|
@@ -11,16 +11,25 @@ import { KernelRecord } from "@mark1russell7/prober";
|
|
|
11
11
|
* It is the reference for the noise of the harness itself.
|
|
12
12
|
* - `stall` is a black hole that stops by itself, `Cell.stall` after the client sends its
|
|
13
13
|
* request. The VM times it. It shows what recovers, and when.
|
|
14
|
+
* - `fin` stops the process of the peer with SIGKILL, while its kernel stays up. The kernel closes its sockets
|
|
15
|
+
* with a FIN, or with a reset when unread data is in the queue.
|
|
16
|
+
* - `unreachable` answers each segment from the observer with an ICMP unreachable (`Cell.icmp`).
|
|
17
|
+
* Linux keeps it as a soft error on an established socket, and it continues to retransmit.
|
|
18
|
+
* - `simplex` drops the segments in one direction only (`Cell.drops`).
|
|
19
|
+
* - `mtu-blackhole` drops each segment to the observer that is longer than `Cell.mtu` bytes.
|
|
20
|
+
* - `netem` is tc netem on the egress of the target (`Cell.netem`): loss, duplication,
|
|
21
|
+
* reordering or corruption. Its outcome is a distribution.
|
|
14
22
|
*/
|
|
15
|
-
type Fault = "blackhole" | "freeze-process" | "rst" | "stall";
|
|
23
|
+
type Fault = "blackhole" | "freeze-process" | "rst" | "stall" | "fin" | "unreachable" | "simplex" | "mtu-blackhole" | "netem";
|
|
16
24
|
/**
|
|
17
25
|
* A reference is the event that the time of a detector counts from: the event that arms the
|
|
18
26
|
* timer in its machine document. Keepalive counts from the last received segment. The give-up of
|
|
19
27
|
* the retransmissions counts from the first retransmission of the RTO timer, not from the tail
|
|
20
28
|
* loss probe one RTO before it. The request itself meets a reset, thus a reset counts from the
|
|
21
|
-
* send of the client.
|
|
29
|
+
* send of the client. A FIN counts from the change of state of the socket of the peer when its
|
|
30
|
+
* process dies (`peer-closed`).
|
|
22
31
|
*/
|
|
23
|
-
type Reference = "last-received" | "first-retransmission" | "request-sent";
|
|
32
|
+
type Reference = "last-received" | "first-retransmission" | "request-sent" | "peer-closed";
|
|
24
33
|
/**
|
|
25
34
|
* This prediction tells when a connection moves again after a stall. That occurs at the first
|
|
26
35
|
* firing of its retransmission timer after the stall stops, and its reply comes back one round
|
|
@@ -48,11 +57,15 @@ interface Recovers {
|
|
|
48
57
|
* after the reference event, or nothing detects the fault in a window. Nothing detects a stall
|
|
49
58
|
* that is shorter than the give-up, and the connection recovers from it. The prediction then also
|
|
50
59
|
* tells when.
|
|
60
|
+
*
|
|
61
|
+
* A cell whose outcome is a distribution, for example under netem loss, has no band. Its
|
|
62
|
+
* prediction is `detects: null` with the reason, and its runs are measurements only.
|
|
51
63
|
*/
|
|
52
64
|
type Prediction = {
|
|
53
65
|
readonly detects: true;
|
|
54
66
|
/** This name is the timer of the machine that fires first, or the event that detects the fault, with its errno. */
|
|
55
67
|
readonly timer: string;
|
|
68
|
+
/** This name is the errno of the detection, or EOF for an end of file without an error. */
|
|
56
69
|
readonly errno: string;
|
|
57
70
|
readonly from: Reference;
|
|
58
71
|
/** This time is the closed form, less the most that the timer can fire early. */
|
|
@@ -63,6 +76,10 @@ type Prediction = {
|
|
|
63
76
|
readonly detects: false;
|
|
64
77
|
readonly window: Quantity;
|
|
65
78
|
readonly recovers?: Recovers;
|
|
79
|
+
} | {
|
|
80
|
+
readonly detects: null;
|
|
81
|
+
readonly window: Quantity;
|
|
82
|
+
readonly why: string;
|
|
66
83
|
};
|
|
67
84
|
/** A target is the server that a cell connects to: a stock image. */
|
|
68
85
|
interface Target {
|
|
@@ -80,6 +97,13 @@ interface Target {
|
|
|
80
97
|
readonly set: readonly string[];
|
|
81
98
|
readonly get: readonly string[];
|
|
82
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* This command replaces the command of the image, for example a shell that holds the server
|
|
102
|
+
* process as its child. Then a `fin` fault can stop the server, and the container stays up.
|
|
103
|
+
*/
|
|
104
|
+
readonly command?: readonly string[];
|
|
105
|
+
/** This command stops the server process in the target with SIGKILL, as argv, for a `fin` fault. */
|
|
106
|
+
readonly kill?: readonly string[];
|
|
83
107
|
}
|
|
84
108
|
/** A client is what the runs of a cell use: the TCP client of the harness, or a driver on its socket. */
|
|
85
109
|
type Client = "tcp" | "ioredis";
|
|
@@ -97,8 +121,15 @@ interface Cell {
|
|
|
97
121
|
/** These are the sysctls of the network namespace of the client, as the kernel spells them. The harness sets them before the client connects. */
|
|
98
122
|
readonly sysctls: Readonly<Record<string, string>>;
|
|
99
123
|
readonly probe: {
|
|
100
|
-
/**
|
|
101
|
-
|
|
124
|
+
/**
|
|
125
|
+
* This mode tells if the client is idle after its first reply, or sends a request into the
|
|
126
|
+
* fault. In a stream, it sends `count` requests into it, each `interval` after the reply before.
|
|
127
|
+
*/
|
|
128
|
+
readonly mode: "idle" | "inflight" | "stream";
|
|
129
|
+
/** This count is the number of requests of a stream. */
|
|
130
|
+
readonly count?: number;
|
|
131
|
+
/** This time is the pause after each reply of a stream, before the next request. */
|
|
132
|
+
readonly interval?: Quantity;
|
|
102
133
|
/** This flag sets SO_KEEPALIVE without overrides for the socket: the keepalive sysctls of the namespace apply. */
|
|
103
134
|
readonly keepalive: boolean;
|
|
104
135
|
/** This time sets TCP_USER_TIMEOUT. Without it, the default of the kernel (off) applies. */
|
|
@@ -122,6 +153,21 @@ interface Cell {
|
|
|
122
153
|
readonly fault: Fault;
|
|
123
154
|
/** This time is the length of a stall, from the send of the request. */
|
|
124
155
|
readonly stall?: Quantity;
|
|
156
|
+
/** This code is the ICMP unreachable of an `unreachable` fault. */
|
|
157
|
+
readonly icmp?: "host" | "net" | "port" | "admin";
|
|
158
|
+
/** This direction is the one that a `simplex` fault drops: the segments to the server, or to the client. */
|
|
159
|
+
readonly drops?: "to-server" | "to-client";
|
|
160
|
+
/** This length is the longest segment, in bytes, that an `mtu-blackhole` fault lets through to the observer. */
|
|
161
|
+
readonly mtu?: number;
|
|
162
|
+
/** This specification is the tc netem of a `netem` fault, on the egress of the target: for example "loss 5%". */
|
|
163
|
+
readonly netem?: string;
|
|
164
|
+
/**
|
|
165
|
+
* This delay goes on the egress of the target before the client connects, with tc netem. Thus
|
|
166
|
+
* each round trip is longer by the delay, and so is the RTO.
|
|
167
|
+
*/
|
|
168
|
+
readonly latency?: Quantity;
|
|
169
|
+
/** These commands run in the target before a client connects, each as argv: for example, a large value to read. */
|
|
170
|
+
readonly targetSetup?: readonly (readonly string[])[];
|
|
125
171
|
readonly prediction: Prediction;
|
|
126
172
|
/** This side is the side whose detection the cell measures. Without it, the side is the client. */
|
|
127
173
|
readonly observer?: Observer;
|
|
@@ -207,6 +253,11 @@ interface Measured {
|
|
|
207
253
|
* the RTO timer. After a stall, they are each retransmission from the send, the loss probe first.
|
|
208
254
|
*/
|
|
209
255
|
readonly steps: readonly Quantity[];
|
|
256
|
+
/**
|
|
257
|
+
* These are the reply times of a measurement-only cell. For each request after the first, a
|
|
258
|
+
* sample is the time from its send to the first data after it.
|
|
259
|
+
*/
|
|
260
|
+
readonly samples?: readonly Quantity[];
|
|
210
261
|
/** This count is the number of keepalive probes that the kernel sent. */
|
|
211
262
|
readonly probes: number;
|
|
212
263
|
/** This count is the number of segments that the kernel sent again. */
|
|
@@ -241,8 +292,11 @@ export declare const NOISE: Quantity;
|
|
|
241
292
|
export declare const RELATIVE = .05;
|
|
242
293
|
/** These are the run counts of a cell: 11, then 22 and 44 while its median crosses the edge of the band. */
|
|
243
294
|
export declare const STEPS: readonly [11, 22, 44];
|
|
244
|
-
/**
|
|
245
|
-
|
|
295
|
+
/**
|
|
296
|
+
* A decision is the result of a verdict. A cell whose outcome is a distribution has no band, and
|
|
297
|
+
* its decision is `measured`: a record, not a test.
|
|
298
|
+
*/
|
|
299
|
+
type Decision = "holds" | "fails" | "extend" | "inconclusive" | "measured";
|
|
246
300
|
/** An interval is a range of times. */
|
|
247
301
|
interface Interval {
|
|
248
302
|
readonly low: Quantity;
|
|
@@ -290,8 +344,20 @@ export declare const verdictOf: (prediction: Prediction, runs: readonly Measured
|
|
|
290
344
|
export declare const harnessImage: () => Promise<string>;
|
|
291
345
|
/** This function gives the image of the Node client. */
|
|
292
346
|
export declare const nodeClientImage: () => Promise<string>;
|
|
293
|
-
/**
|
|
294
|
-
|
|
347
|
+
/**
|
|
348
|
+
* A network fault is a fault that the injector applies as a filter rule. It is a black hole (both
|
|
349
|
+
* directions, or one), or a reset on the next segment. It can also be an ICMP unreachable for each
|
|
350
|
+
* segment, or a drop of each segment that is longer than a length.
|
|
351
|
+
*/
|
|
352
|
+
type NetworkFault = "blackhole" | "rst" | "reject" | "mtu";
|
|
353
|
+
/** These are the options of a network fault: the direction of a black hole, the code of a reject, the length of an MTU drop. */
|
|
354
|
+
interface FaultOptions {
|
|
355
|
+
/** This direction is relative to the namespace of the rule: `in` drops what comes from the peer, `out` what goes to it. */
|
|
356
|
+
readonly direction?: "in" | "out";
|
|
357
|
+
readonly code?: "host" | "net" | "port" | "admin";
|
|
358
|
+
/** This length is the longest segment in bytes that an MTU drop lets through. */
|
|
359
|
+
readonly above?: number;
|
|
360
|
+
}
|
|
295
361
|
/**
|
|
296
362
|
* A fault side is the namespace that gets the rules of a fault: the namespace of the peer of the
|
|
297
363
|
* observer. For the client, it is the namespace of the target (the injector). For the server, it
|
|
@@ -344,6 +410,8 @@ interface Probe {
|
|
|
344
410
|
/** This method freezes the client with the cgroup freezer: its process stops, and its kernel continues to answer. */
|
|
345
411
|
freeze(): Promise<void>;
|
|
346
412
|
thaw(): Promise<void>;
|
|
413
|
+
/** This method stops the process of the client with SIGKILL. Its kernel then closes its socket with a FIN, or a reset. */
|
|
414
|
+
kill(): Promise<void>;
|
|
347
415
|
readonly events: readonly ProbeEvent[];
|
|
348
416
|
stop(): Promise<void>;
|
|
349
417
|
}
|
|
@@ -368,12 +436,35 @@ interface Lab {
|
|
|
368
436
|
* target. The fault is a black hole, or a reset that answers the next segment of the client. The stamps of
|
|
369
437
|
* the injector bracket it.
|
|
370
438
|
*/
|
|
371
|
-
fault(kind: NetworkFault, at?: FaultSide): Promise<{
|
|
439
|
+
fault(kind: NetworkFault, at?: FaultSide, options?: FaultOptions): Promise<{
|
|
372
440
|
t0: number;
|
|
373
441
|
t1: number;
|
|
374
442
|
}>;
|
|
375
443
|
/** This method removes a fault. */
|
|
376
|
-
lift(kind: NetworkFault, at?: FaultSide): Promise<void>;
|
|
444
|
+
lift(kind: NetworkFault, at?: FaultSide, options?: FaultOptions): Promise<void>;
|
|
445
|
+
/**
|
|
446
|
+
* This method puts tc netem on the egress of a namespace, for example "delay 20ms" or "loss 5%",
|
|
447
|
+
* or removes it with null. The stamps of the injector bracket it.
|
|
448
|
+
*/
|
|
449
|
+
netem(spec: string | null, at?: FaultSide): Promise<{
|
|
450
|
+
t0: number;
|
|
451
|
+
t1: number;
|
|
452
|
+
}>;
|
|
453
|
+
/**
|
|
454
|
+
* This method sets the TSO, GSO and GRO of a namespace. With them off, netem and a length match
|
|
455
|
+
* act on segments, not on GSO super-packets of up to 64 KB.
|
|
456
|
+
*/
|
|
457
|
+
offload(on: boolean, at?: FaultSide): Promise<void>;
|
|
458
|
+
/** This method stops the server process in the target with SIGKILL, with the command of the target. */
|
|
459
|
+
kill(): Promise<void>;
|
|
460
|
+
/**
|
|
461
|
+
* This method resolves the neighbors of the path in the two directions, with a ping from each
|
|
462
|
+
* namespace. Without it, the first connection of an environment waits for ARP. Under a netem
|
|
463
|
+
* delay, that wait is one more delayed round, and its round trip and RTO are longer.
|
|
464
|
+
*/
|
|
465
|
+
warm(): Promise<void>;
|
|
466
|
+
/** This method runs commands in the target before a client connects, each as argv. */
|
|
467
|
+
setup(commands: ReadonlyArray<readonly string[]>): Promise<void>;
|
|
377
468
|
/**
|
|
378
469
|
* This method stops a black hole at an instant on the monotonic clock of the VM. The injector
|
|
379
470
|
* waits for that instant in the VM, thus the length of a stall does not include the latency of
|
package/build/index.js
CHANGED
|
@@ -18,10 +18,13 @@ const ERRNO = {
|
|
|
18
18
|
};
|
|
19
19
|
/** This function gives the interval between two instants on the one clock, in seconds to the microsecond. */
|
|
20
20
|
const between = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
|
|
21
|
+
/** These functions give the states of a state record: before the change, and after it. TCP_ESTABLISHED is 1, TCP_CLOSE_WAIT 8. */
|
|
22
|
+
const oldState = (record) => record.old;
|
|
23
|
+
const newState = (record) => record.new;
|
|
21
24
|
const count = (event, field) => event === void 0 ? 0 : Number(event[field] ?? 0);
|
|
22
25
|
/** This function gives the view of the client on the same interval: TCP_INFO, with a sample each 2 ms. */
|
|
23
26
|
const socketVantage = (cell, events, ready, request) => {
|
|
24
|
-
if (!cell.prediction.detects) return null;
|
|
27
|
+
if (!cell.prediction.detects || cell.prediction.from === "peer-closed") return null;
|
|
25
28
|
const ended = events.find((event) => event.ev === "error" || event.ev === "eof");
|
|
26
29
|
const reference = cell.prediction.from === "last-received" ? ready : cell.prediction.from === "request-sent" ? request : events.find((event) => event.ev === "kernel" && event.t >= request.t && count(event, "retransmits") > 0);
|
|
27
30
|
return ended === void 0 || reference === void 0 ? null : between(reference.t, ended.t);
|
|
@@ -38,11 +41,14 @@ const measure = (cell, run) => {
|
|
|
38
41
|
const { events, kernel } = run;
|
|
39
42
|
const ready = events.find((event) => event.ev === "ready");
|
|
40
43
|
const local = events.find((event) => event.ev === "connected")?.["local"];
|
|
41
|
-
const
|
|
44
|
+
const inflight = cell.probe.mode !== "idle";
|
|
45
|
+
const request = inflight ? events.filter((event) => event.ev === "sent")[1] : ready;
|
|
42
46
|
const server = cell.observer === "server";
|
|
43
47
|
const netns = server ? run.targetNetns : run.netns;
|
|
44
48
|
const cookie = kernel.find((record) => record.netns === netns && (server ? record.dst : record.src) === local)?.cookie;
|
|
45
49
|
const socket = kernel.filter((record) => record.netns === netns && record.cookie === cookie);
|
|
50
|
+
const peerNetns = server ? run.netns : run.targetNetns;
|
|
51
|
+
const peerCookie = kernel.find((record) => record.netns === peerNetns && (server ? record.src : record.dst) === local)?.cookie;
|
|
46
52
|
const keepalives = socket.filter((record) => record.ev === "probe" && record.what === "keepalive");
|
|
47
53
|
const retransmitted = socket.filter((record) => record.ev === "retrans");
|
|
48
54
|
/**
|
|
@@ -103,24 +109,42 @@ const measure = (cell, run) => {
|
|
|
103
109
|
};
|
|
104
110
|
let voided = null;
|
|
105
111
|
if (run.fault !== null) {
|
|
112
|
+
const replyFirst = server && inflight && cell.fault !== "mtu-blackhole";
|
|
106
113
|
const replied = events.some((event) => event.ev === "data" && event.t > request.t && event.t < run.fault.t1);
|
|
107
|
-
if (
|
|
108
|
-
else if (
|
|
109
|
-
else if (
|
|
114
|
+
if (inflight && !replyFirst && run.fault.t1 > request.t) voided = "the request went out before the fault held";
|
|
115
|
+
else if (replyFirst && run.fault.t1 < request.t) voided = "the request went out into the fault, so the server never had it";
|
|
116
|
+
else if (replyFirst && replied) voided = "the server's reply arrived before the fault held";
|
|
110
117
|
else if (keepalives[0] !== void 0 && keepalives[0].mono < run.fault.t1) voided = "a keepalive probe went out before the fault held";
|
|
111
118
|
}
|
|
112
119
|
const reported = socket.find((record) => record.ev === "error" && (record.err ?? 0) !== 0);
|
|
113
120
|
const seen = server ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
|
|
121
|
+
const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8);
|
|
114
122
|
const outcome = reported !== void 0 ? {
|
|
115
123
|
kind: "error",
|
|
116
124
|
errno: ERRNO[reported.err] ?? `E${reported.err}`
|
|
117
|
-
} : seen?.ev === "eof" ? { kind: "eof" } : seen !== void 0 ? {
|
|
125
|
+
} : closeWait !== void 0 || seen?.ev === "eof" ? { kind: "eof" } : seen !== void 0 ? {
|
|
118
126
|
kind: "error",
|
|
119
127
|
errno: String(seen["errno"])
|
|
120
128
|
} : { kind: "silent" };
|
|
121
|
-
const ended = reported?.mono ?? seen?.t;
|
|
129
|
+
const ended = reported?.mono ?? closeWait?.mono ?? seen?.t;
|
|
122
130
|
const noticed = reported !== void 0 && seen !== void 0 ? between(reported.mono, seen.t) : null;
|
|
123
131
|
const { prediction } = cell;
|
|
132
|
+
if (prediction.detects === null) {
|
|
133
|
+
const sends = events.filter((event) => event.ev === "sent").slice(1);
|
|
134
|
+
const samples = sends.flatMap((send, n) => {
|
|
135
|
+
const next = sends[n + 1]?.t ?? Infinity;
|
|
136
|
+
const reply = events.find((event) => event.ev === "data" && event.t > send.t && event.t < next);
|
|
137
|
+
return reply === void 0 ? [] : [between(send.t, reply.t)];
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
...base,
|
|
141
|
+
...nothing,
|
|
142
|
+
outcome: ended !== void 0 ? outcome : { kind: "answered" },
|
|
143
|
+
steps: retransmitted.filter((record) => record.mono > request.t).map((record) => between(request.t, record.mono)),
|
|
144
|
+
samples,
|
|
145
|
+
void: voided
|
|
146
|
+
};
|
|
147
|
+
}
|
|
124
148
|
if (!prediction.detects && prediction.recovers !== void 0) {
|
|
125
149
|
const { recovers } = prediction;
|
|
126
150
|
const heal = run.heal ?? null;
|
|
@@ -183,7 +207,7 @@ const measure = (cell, run) => {
|
|
|
183
207
|
finding: `nothing detected it: ${prediction.timer} never fired`
|
|
184
208
|
};
|
|
185
209
|
const before = run.fault?.t1 ?? ready.t;
|
|
186
|
-
const kernelReference = prediction.from === "last-received" ? socket.filter((record) => record.ev === "rx" && record.mono <= before).at(-1) : prediction.from === "first-retransmission" ? retransmitted.find((record) => record.why === "rto") : void 0;
|
|
210
|
+
const kernelReference = prediction.from === "last-received" ? socket.filter((record) => record.ev === "rx" && record.mono <= before).at(-1) : prediction.from === "first-retransmission" ? retransmitted.find((record) => record.why === "rto") : prediction.from === "peer-closed" ? kernel.find((record) => record.netns === peerNetns && record.cookie === peerCookie && record.ev === "state" && oldState(record) === 1) : void 0;
|
|
187
211
|
const reference = prediction.from === "request-sent" ? request.t : kernelReference?.mono;
|
|
188
212
|
if (reference === void 0) return {
|
|
189
213
|
...base,
|
|
@@ -194,8 +218,8 @@ const measure = (cell, run) => {
|
|
|
194
218
|
finding: `it ended without its reference event (${prediction.from})`
|
|
195
219
|
};
|
|
196
220
|
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));
|
|
197
|
-
const finding = outcome.kind === "error" && outcome.errno === prediction.errno ? null : `it ended with ${outcome.kind === "error" ? outcome.errno : "end of file"}, not ${prediction.errno}`;
|
|
198
|
-
if (prediction.from !== "request-sent") voided ??= delayed(reference, ended, prediction.from === "last-received" ? "keepalive" : "retransmit");
|
|
221
|
+
const finding = (prediction.errno === "EOF" ? outcome.kind === "eof" : outcome.kind === "error" && outcome.errno === prediction.errno) ? null : `it ended with ${outcome.kind === "error" ? outcome.errno : "end of file"}, not ${prediction.errno}`;
|
|
222
|
+
if (prediction.from !== "request-sent" && prediction.from !== "peer-closed") voided ??= delayed(reference, ended, prediction.from === "last-received" ? "keepalive" : "retransmit");
|
|
199
223
|
return {
|
|
200
224
|
...base,
|
|
201
225
|
outcome,
|
|
@@ -284,6 +308,20 @@ const verdictOf = (prediction, runs) => {
|
|
|
284
308
|
outside,
|
|
285
309
|
why: "no run counted"
|
|
286
310
|
};
|
|
311
|
+
if (prediction.detects === null) {
|
|
312
|
+
const samples = counted.flatMap((run) => (run.samples ?? []).map((sample) => valueIn(sample, "s"))).sort((a, b) => a - b);
|
|
313
|
+
const retransmissions = counted.reduce((sum, run) => sum + run.retransmissions, 0);
|
|
314
|
+
const ended = counted.filter((run) => run.outcome.kind === "error" || run.outcome.kind === "eof");
|
|
315
|
+
const why = samples.length === 0 ? `${counted.length} runs measured, with no reply: ${prediction.why}` : `${counted.length} runs, ${samples.length} replies: median ${s(middle(samples)).value} s, slowest ${s(samples.at(-1)).value} s, ${retransmissions} retransmissions, ${ended.length} connections ended. ${prediction.why}`;
|
|
316
|
+
return {
|
|
317
|
+
...empty,
|
|
318
|
+
decision: "measured",
|
|
319
|
+
band: null,
|
|
320
|
+
outside: [],
|
|
321
|
+
median: samples.length === 0 ? null : s(middle(samples)),
|
|
322
|
+
why
|
|
323
|
+
};
|
|
324
|
+
}
|
|
287
325
|
if (!prediction.detects && prediction.recovers === void 0) {
|
|
288
326
|
const decision = outside.length === 0 ? "holds" : "fails";
|
|
289
327
|
const why = decision === "holds" ? `nothing detected it in ${counted.length} runs of ${valueIn(prediction.window, "s")} s` : `${outside.length} of ${counted.length} runs detected it`;
|
|
@@ -457,6 +495,19 @@ const build = (image) => {
|
|
|
457
495
|
const harnessImage = () => build(HARNESS);
|
|
458
496
|
/** This function gives the image of the Node client. */
|
|
459
497
|
const nodeClientImage = () => build(NODE);
|
|
498
|
+
/** This function gives the arguments of the injector for the options of a fault. */
|
|
499
|
+
const optionArgs = (options) => [
|
|
500
|
+
...options.direction === void 0 ? [] : ["--direction", options.direction],
|
|
501
|
+
...options.code === void 0 ? [] : ["--code", options.code],
|
|
502
|
+
...options.above === void 0 ? [] : ["--above", String(options.above)]
|
|
503
|
+
];
|
|
504
|
+
/** These are the actions of the injector that lift each network fault. */
|
|
505
|
+
const LIFT = {
|
|
506
|
+
blackhole: "clear",
|
|
507
|
+
rst: "clear-rst",
|
|
508
|
+
reject: "clear-reject",
|
|
509
|
+
mtu: "clear-mtu"
|
|
510
|
+
};
|
|
460
511
|
const lines = () => {
|
|
461
512
|
const seen = [];
|
|
462
513
|
const other = [];
|
|
@@ -539,7 +590,8 @@ const openLab = async (target, kind = "tcp") => {
|
|
|
539
590
|
};
|
|
540
591
|
try {
|
|
541
592
|
network = await new Network().start();
|
|
542
|
-
const
|
|
593
|
+
const stock = new GenericContainer(target.image).withNetwork(network).withWaitStrategy(Wait.forLogMessage(target.ready));
|
|
594
|
+
const server = await (target.command === void 0 ? stock : stock.withCommand([...target.command])).start();
|
|
543
595
|
opened.push(server);
|
|
544
596
|
const client = await new GenericContainer(image).withNetwork(network).start();
|
|
545
597
|
opened.push(client);
|
|
@@ -620,26 +672,80 @@ const openLab = async (target, kind = "tcp") => {
|
|
|
620
672
|
}
|
|
621
673
|
return read;
|
|
622
674
|
},
|
|
623
|
-
async fault(kind, at = "target") {
|
|
675
|
+
async fault(kind, at = "target", options = {}) {
|
|
624
676
|
const [where, peer] = at === "client" ? [admin, targetIp] : [injector, clientIp];
|
|
625
|
-
const
|
|
677
|
+
const args = [
|
|
626
678
|
"/opt/harness/inject",
|
|
627
679
|
kind,
|
|
628
|
-
peer
|
|
629
|
-
|
|
680
|
+
peer,
|
|
681
|
+
...optionArgs(options)
|
|
682
|
+
];
|
|
683
|
+
const stamp = JSON.parse(await output(where, args));
|
|
630
684
|
return {
|
|
631
685
|
t0: stamp.t0,
|
|
632
686
|
t1: stamp.t1
|
|
633
687
|
};
|
|
634
688
|
},
|
|
635
|
-
async lift(kind, at = "target") {
|
|
689
|
+
async lift(kind, at = "target", options = {}) {
|
|
636
690
|
const [where, peer] = at === "client" ? [admin, targetIp] : [injector, clientIp];
|
|
637
691
|
await output(where, [
|
|
638
692
|
"/opt/harness/inject",
|
|
639
|
-
kind
|
|
640
|
-
peer
|
|
693
|
+
LIFT[kind],
|
|
694
|
+
peer,
|
|
695
|
+
...optionArgs(options)
|
|
641
696
|
]);
|
|
642
697
|
},
|
|
698
|
+
async netem(spec, at = "target") {
|
|
699
|
+
const where = at === "client" ? admin : injector;
|
|
700
|
+
const args = spec === null ? [
|
|
701
|
+
"/opt/harness/inject",
|
|
702
|
+
"clear-netem",
|
|
703
|
+
"eth0"
|
|
704
|
+
] : [
|
|
705
|
+
"/opt/harness/inject",
|
|
706
|
+
"netem",
|
|
707
|
+
"eth0",
|
|
708
|
+
...spec.split(/\s+/)
|
|
709
|
+
];
|
|
710
|
+
const stamp = JSON.parse(await output(where, args));
|
|
711
|
+
return {
|
|
712
|
+
t0: stamp.t0,
|
|
713
|
+
t1: stamp.t1
|
|
714
|
+
};
|
|
715
|
+
},
|
|
716
|
+
async offload(on, at = "target") {
|
|
717
|
+
await output(at === "client" ? admin : injector, [
|
|
718
|
+
"/opt/harness/inject",
|
|
719
|
+
"offload",
|
|
720
|
+
"eth0",
|
|
721
|
+
on ? "on" : "off"
|
|
722
|
+
]);
|
|
723
|
+
},
|
|
724
|
+
async kill() {
|
|
725
|
+
if (target.kill === void 0) throw new Error(`${target.image} names no way to kill its server`);
|
|
726
|
+
await output(server, [...target.kill]);
|
|
727
|
+
},
|
|
728
|
+
async warm() {
|
|
729
|
+
await output(admin, [
|
|
730
|
+
"ping",
|
|
731
|
+
"-c",
|
|
732
|
+
"2",
|
|
733
|
+
"-W",
|
|
734
|
+
"2",
|
|
735
|
+
targetIp
|
|
736
|
+
]);
|
|
737
|
+
await output(injector, [
|
|
738
|
+
"ping",
|
|
739
|
+
"-c",
|
|
740
|
+
"2",
|
|
741
|
+
"-W",
|
|
742
|
+
"2",
|
|
743
|
+
clientIp
|
|
744
|
+
]);
|
|
745
|
+
},
|
|
746
|
+
async setup(commands) {
|
|
747
|
+
for (const argv of commands) await output(server, [...argv]);
|
|
748
|
+
},
|
|
643
749
|
async heal(at, when) {
|
|
644
750
|
const [where, peer] = at === "client" ? [admin, targetIp] : [injector, clientIp];
|
|
645
751
|
const stamp = JSON.parse(await output(where, [
|
|
@@ -679,7 +785,8 @@ const openLab = async (target, kind = "tcp") => {
|
|
|
679
785
|
go: () => output(container, ["touch", "/tmp/go"]).then(() => void 0),
|
|
680
786
|
freeze: () => runtime.container.getById(container.getId()).pause().then(() => void 0),
|
|
681
787
|
thaw: () => runtime.container.getById(container.getId()).unpause().then(() => void 0),
|
|
682
|
-
|
|
788
|
+
kill: () => runtime.container.getById(container.getId()).kill({ signal: "SIGKILL" }).then(() => void 0),
|
|
789
|
+
stop: () => container.stop(STOP).then(() => void 0).catch(() => void 0)
|
|
683
790
|
};
|
|
684
791
|
},
|
|
685
792
|
async settle() {
|
|
@@ -765,6 +872,8 @@ const probeArgs = (cell, lab) => {
|
|
|
765
872
|
probe.mode
|
|
766
873
|
];
|
|
767
874
|
if (probe.request !== void 0) args.push("--request", probe.request);
|
|
875
|
+
if (probe.count !== void 0) args.push("--count", String(probe.count));
|
|
876
|
+
if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
|
|
768
877
|
if (probe.driver !== void 0) {
|
|
769
878
|
args.push("--keepalive-ms", String(valueIn(probe.driver.keepAlive, "ms")));
|
|
770
879
|
args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
|
|
@@ -777,13 +886,14 @@ const probeArgs = (cell, lab) => {
|
|
|
777
886
|
};
|
|
778
887
|
/**
|
|
779
888
|
* This function waits for an error on the socket of the target for the connection of the
|
|
780
|
-
* client
|
|
781
|
-
* waits for that. The promise resolves after `within` ms
|
|
889
|
+
* client, or for its end of file (CLOSE_WAIT, state 8). The prober records both in the namespace
|
|
890
|
+
* of the target. A cell on the server side waits for that. The promise resolves after `within` ms
|
|
891
|
+
* at the latest.
|
|
782
892
|
*/
|
|
783
893
|
const serverError = (lab, local, within) => new Promise((resolve) => {
|
|
784
894
|
const started = Date.now();
|
|
785
895
|
const look = () => {
|
|
786
|
-
if (lab.kernel.some((record) => record.ev === "error" && (record.err ?? 0) !== 0
|
|
896
|
+
if (lab.kernel.some((record) => record.netns === lab.netns.target && record.dst === local && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && record.new === 8)) || Date.now() - started > within) resolve();
|
|
787
897
|
else setTimeout(look, 50);
|
|
788
898
|
};
|
|
789
899
|
look();
|
|
@@ -813,6 +923,11 @@ const requestSent = (probe, within) => new Promise((resolve, reject) => {
|
|
|
813
923
|
* next run. The run keeps the records of the prober from its start to its end. The frozen process
|
|
814
924
|
* is the peer of the observer: the target, or the client for the server.
|
|
815
925
|
*/
|
|
926
|
+
/** This function gives the netem specification of a cell: its latency, then the netem of its fault when it has one. */
|
|
927
|
+
const netemSpec = (cell, fault) => {
|
|
928
|
+
const parts = [...cell.latency === void 0 ? [] : [`delay ${valueIn(cell.latency, "ms")}ms`], ...fault === void 0 ? [] : [fault]];
|
|
929
|
+
return parts.length === 0 ? null : parts.join(" ");
|
|
930
|
+
};
|
|
816
931
|
const once = async (lab, cell, environment, index) => {
|
|
817
932
|
await lab.forget();
|
|
818
933
|
const from = lab.kernel.length;
|
|
@@ -834,17 +949,51 @@ const once = async (lab, cell, environment, index) => {
|
|
|
834
949
|
let heal = null;
|
|
835
950
|
try {
|
|
836
951
|
await probe.until(["ready"], 15e3);
|
|
837
|
-
const replyInFlight = server && cell.
|
|
952
|
+
const replyInFlight = cell.probe.mode === "inflight" && (server && cell.fault !== "mtu-blackhole" || cell.fault === "fin");
|
|
838
953
|
if (replyInFlight) {
|
|
839
954
|
await go();
|
|
840
955
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
841
956
|
}
|
|
842
957
|
const faultAt = server ? "client" : "target";
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
958
|
+
const direction = cell.drops === void 0 ? void 0 : cell.drops === "to-server" === (faultAt === "target") ? "in" : "out";
|
|
959
|
+
const options = {
|
|
960
|
+
...direction === void 0 ? {} : { direction },
|
|
961
|
+
...cell.icmp === void 0 ? {} : { code: cell.icmp },
|
|
962
|
+
...cell.mtu === void 0 ? {} : { above: cell.mtu }
|
|
963
|
+
};
|
|
964
|
+
const apply = async () => {
|
|
965
|
+
switch (cell.fault) {
|
|
966
|
+
case "freeze-process":
|
|
967
|
+
await peer.freeze();
|
|
968
|
+
return null;
|
|
969
|
+
case "fin":
|
|
970
|
+
await (server ? probe.kill() : lab.kill());
|
|
971
|
+
return null;
|
|
972
|
+
case "unreachable": return lab.fault("reject", faultAt, { code: cell.icmp ?? "host" });
|
|
973
|
+
case "simplex": return lab.fault("blackhole", faultAt, options);
|
|
974
|
+
case "mtu-blackhole": return lab.fault("mtu", "client", { above: cell.mtu ?? 1200 });
|
|
975
|
+
case "netem": return lab.netem(netemSpec(cell, cell.netem), "target");
|
|
976
|
+
case "rst": return lab.fault("rst", faultAt);
|
|
977
|
+
default: return lab.fault("blackhole", faultAt);
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
const lift = async () => {
|
|
981
|
+
switch (cell.fault) {
|
|
982
|
+
case "freeze-process": return peer.thaw();
|
|
983
|
+
case "fin": return;
|
|
984
|
+
case "unreachable": return lab.lift("reject", faultAt, { code: cell.icmp ?? "host" });
|
|
985
|
+
case "simplex": return lab.lift("blackhole", faultAt, options);
|
|
986
|
+
case "mtu-blackhole": return lab.lift("mtu", "client", { above: cell.mtu ?? 1200 });
|
|
987
|
+
case "netem":
|
|
988
|
+
await lab.netem(netemSpec(cell, void 0), "target");
|
|
989
|
+
return;
|
|
990
|
+
case "rst": return lab.lift("rst", faultAt);
|
|
991
|
+
default: return heal === null ? lab.lift("blackhole", faultAt) : void 0;
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
fault = await apply();
|
|
846
995
|
try {
|
|
847
|
-
if (cell.probe.mode
|
|
996
|
+
if (cell.probe.mode !== "idle" && !replyInFlight) await go();
|
|
848
997
|
const { prediction } = cell;
|
|
849
998
|
if (cell.fault === "stall") {
|
|
850
999
|
if (cell.stall === void 0) throw new Error(`${cell.name} says no stall's length`);
|
|
@@ -856,6 +1005,14 @@ const once = async (lab, cell, environment, index) => {
|
|
|
856
1005
|
"error",
|
|
857
1006
|
"eof"
|
|
858
1007
|
], window * 1e3).catch(() => void 0);
|
|
1008
|
+
} else if (prediction.detects === null) {
|
|
1009
|
+
const window = valueIn(prediction.window, "s") * 1e3;
|
|
1010
|
+
await probe.until([
|
|
1011
|
+
"done",
|
|
1012
|
+
"error",
|
|
1013
|
+
"eof",
|
|
1014
|
+
"deadline"
|
|
1015
|
+
], window).catch(() => void 0);
|
|
859
1016
|
} else if (prediction.detects) {
|
|
860
1017
|
const within = valueIn(prediction.high, "s") * 2e3 + 1e4;
|
|
861
1018
|
if (server) {
|
|
@@ -868,8 +1025,7 @@ const once = async (lab, cell, environment, index) => {
|
|
|
868
1025
|
], within).catch(() => void 0);
|
|
869
1026
|
} else await new Promise((resolve) => setTimeout(resolve, valueIn(prediction.window, "s") * 1e3));
|
|
870
1027
|
} finally {
|
|
871
|
-
|
|
872
|
-
else if (heal === null) await lab.lift(network, faultAt);
|
|
1028
|
+
await lift();
|
|
873
1029
|
}
|
|
874
1030
|
if (cell.fault === "freeze-process" && !server) await probe.until(["data"], 5e3).catch(() => void 0);
|
|
875
1031
|
} finally {
|
|
@@ -897,6 +1053,14 @@ const inEnvironment = async (cell, environment, count, say) => {
|
|
|
897
1053
|
try {
|
|
898
1054
|
const settings = await lab.configure(cell.targetSettings ?? {});
|
|
899
1055
|
const targetSysctls = await lab.targetSysctl(cell.targetSysctls ?? {});
|
|
1056
|
+
if (cell.targetSetup !== void 0) await lab.setup(cell.targetSetup);
|
|
1057
|
+
if (cell.fault === "mtu-blackhole" || cell.fault === "netem") {
|
|
1058
|
+
await lab.offload(false, "target");
|
|
1059
|
+
await lab.offload(false, "client");
|
|
1060
|
+
}
|
|
1061
|
+
const latency = netemSpec(cell, void 0);
|
|
1062
|
+
if (latency !== null) await lab.netem(latency, "target");
|
|
1063
|
+
await lab.warm();
|
|
900
1064
|
const manifest = await lab.manifest(await lab.sysctl(cell.sysctls), settings, targetSysctls);
|
|
901
1065
|
const runs = [];
|
|
902
1066
|
for (let index = 0; index < count; index += 1) {
|