@mark1russell7/harness 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,101 @@
1
- # Temporary Holding Version
1
+ # @mark1russell7/harness
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A fault-injection harness for connection timers. In a cell, a client connects to a stock target in new Docker environments. The harness injects a fault, and it measures what the kernel did. Then the harness judges the measurement against the prediction of the cell.
4
+
5
+ The harness does not compute predictions. The author of a cell computes the prediction, for example from a timer algebra, and puts it in the cell.
6
+
7
+ ## The cell
8
+
9
+ A **cell** (`cell.ts`) is all that one measurement needs, as data:
10
+
11
+ - **The topology.** A stock target image, pinned by digest.
12
+ - **The settings.** The sysctls of the client namespace, and the socket options of the client.
13
+ - **The workload.** A connection that stays idle, or a connection with a request in flight.
14
+ - **The fault.** A black hole, a reset, a frozen process, or a stall.
15
+ - **The prediction.** An interval [T_lo, T_hi] after the event that arms the timer, or silence in a window. After a stall, the prediction also tells when the connection recovers.
16
+
17
+ ## The environment
18
+
19
+ `openLab(target)` (`lab.ts`) builds one environment with Testcontainers:
20
+
21
+ - A new network and the target.
22
+ - A client container that holds its network namespace.
23
+ - A privileged sidecar in the namespace of the client, for its sysctls. Docker Desktop keeps `/proc/sys` read-only in a container that operates.
24
+ - An injector sidecar in the namespace of the target, with NET_ADMIN. Thus the image of the target stays stock.
25
+ - The prober ([`@mark1russell7/prober`](https://www.npmjs.com/package/@mark1russell7/prober)): a privileged container that loads the kernel program and watches the two namespaces. Each run keeps its records from its start to its end. A run whose records the prober lost is void.
26
+
27
+ Each command is an argv array.
28
+
29
+ ## The images
30
+
31
+ The harness builds two images. The tag of each image comes from the content of all the files that its Dockerfile reads. The harness copies these files into a new build context, from its own package and from the prober package. Thus the build works in a workspace and from npm.
32
+
33
+ - **The harness image** (`image/Dockerfile`) has two stages, each a digest-pinned Alpine. `Cargo.lock` pins the crates. The image holds:
34
+ - the kernel program of the prober, compiled against the BTF of the kernel that builds it, and its Rust collector.
35
+ - `client`, the TCP client of a cell. Rust's `std::net` sets no socket option by itself, thus the measurement gets the timers of the namespace. The client also samples `TCP_INFO` each 2 ms, a second vantage on its socket. It waits in `poll()`, which does not lock the socket. A keepalive timer that finds its socket held tries again HZ/20 later.
36
+ - `inject`, which applies a fault in one `iptables-restore` commit, with monotonic stamps around the commit. Thus the start of a fault is known in the VM. A `docker exec` from Windows comes 190 ms late at the median.
37
+ - **The Node client image** (`image/node`) is a driver client: ioredis 6.0.0 on Node 24.21.0. The digest of the image and an npm lockfile pin them. `client-ioredis.mjs` prints the lines of the TCP client on the same clock, and it adds the status changes of ioredis as `status` lines.
38
+
39
+ The build does the `cargo test` of the two crates. Thus the record decoder passes its tests before an image exists. Each amount that the native programs print is a quantity in a canonical unit. For example, an RTO is in `jiffy`, a round trip in `us`, and a segment in `B`.
40
+
41
+ ioredis sets the keepalive of its socket itself, after it connects. Thus a cell sets the keepalive of a driver through the keepAlive option of ioredis (`probe.driver`). The keepalive sysctls of the namespace do not apply to it.
42
+
43
+ ## The faults
44
+
45
+ - **A black hole** is DROP rules in the two directions, in the namespace of the peer of the observer. Thus the measurement gets the kernel timers of the observer. For the client, the rules go in the namespace of the target, through the injector. For the server, they go in the namespace of the client, through its admin sidecar. Rules in the namespace of the observer make its sends fail with EPERM, and the kernel records only a retransmission that goes out (`FaultSide`).
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
+ - **A frozen process** is the cgroup freezer (`docker pause`), while the kernel stays up.
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
+
50
+ 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
+
52
+ ## The measurement
53
+
54
+ `measure(cell, run)` (`observe.ts`) reads a run from the records of the kernel:
55
+
56
+ - **The socket.** The socket of the run is the socket that the client connected from, in its namespace.
57
+ - **The time.** It times the detection from the reference event of the prediction to the error report of the kernel. eBPF stamps the two events. For keepalive, the reference is the last received segment. For the give-up, it is the first retransmission of the RTO timer, not the tail loss probe one RTO before it. For a reset, it is the send of the client, which no kernel record marks. The client stamps the send before its write.
58
+ - **The other views.** It also gives the same interval from the `TCP_INFO` samples of the client, and the delay before the client saw the error. It also gives the steps of the kernel: each keepalive probe, or each expiry of the RTO timer.
59
+ - **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
+ - **Findings.** A finding is a wrong errno, a detector that did not fire, or a detection where the prediction is silence.
61
+ - **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.
62
+
63
+ ## The verdict
64
+
65
+ `verdictOf(prediction, runs)` (`verdict.ts`) judges a cell:
66
+
67
+ - **Containment.** Each run lands in [T_lo − δ_abs, T_hi + δ_abs]. `NOISE` (δ_abs) is 4 ms, from the calibration: the worst reset took 1.141 ms, and δ_abs is one jiffy at the minimum.
68
+ - **Median equivalence.** The order-statistic interval of the median must be in the band, made 5 % wider.
69
+ - **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
+ - **Recovery.** The recovery after a stall gets the same test as a detection, by its band. A detection is then a finding.
71
+
72
+ The distribution test needs a simulator, which does not exist at this time.
73
+
74
+ ## Runs
75
+
76
+ - **`runCell(cell)`** (`run.ts`) divides the 11 runs of a cell across two new environments. It records the manifest of each environment, and it removes the fault after each run.
77
+ - The `concurrency` of a cell sets how many environments operate at the same time, each with its own prober.
78
+ - The manifest holds the kernel release, the architecture, the boot id and the Docker version. It also holds the image ids, the packages of the image, the client and the sysctls as read back.
79
+ - A cell whose probe names a `driver` opens its labs with the client of that driver (`openLab(target, "ioredis")`).
80
+ - The harness sets the `targetSettings` of a cell in the target before a client connects, with the `settings` commands of the target (`redis-cli CONFIG SET`, then `CONFIG GET`).
81
+ - **The observer.** A cell observes the client, or the server (`observer`). A cell on the server side measures the socket of the target for the connection of the client. The run waits for the error of that socket. `freeze-process` freezes the peer of the observer: the target for the client, the client for the server.
82
+ - A cell on the server side sets the sysctls of the target through `targetSysctls`, with a privileged sidecar in the namespace of the target. That sidecar starts only when a cell asks for it.
83
+ - Its in-flight data is the reply of the server. Thus the client sends `probe.request` first, for example a BLPOP with a timeout, and the fault occurs while the server holds it.
84
+ - **`judge(cell, runs)`** (`run.ts`) measures and judges the raw records of a cell with the code at this time. `runCell` uses it during a cell. A holder of raw records can judge them again after a change to `observe.ts` or to the verdict.
85
+ - **Testcontainers loads when a lab first opens.** Code that only measures and judges does not load it.
86
+
87
+ ## Installation
88
+
89
+ ```sh
90
+ pnpm add @mark1russell7/harness zod
91
+ ```
92
+
93
+ A run needs Docker, and privileged containers for the prober and the sidecars. The kernel must have BTF (`/sys/kernel/btf/vmlinux`).
94
+
95
+ ## Tests
96
+
97
+ `pnpm --filter @mark1russell7/harness test` starts the tests. They measure test records with the shape of real kernel records (keepalive, and the tail loss probe before the retransmissions of the RTO timer). They also test the containment, the median equivalence, the extension and the silence of the verdict.
98
+
99
+ The cells themselves need Docker and privileged containers. CI does not start them.
100
+
101
+ `pnpm --filter @mark1russell7/harness build` writes the package for npm into `build/`.
@@ -0,0 +1,455 @@
1
+ import { Quantity } from "@mark1russell7/quantity";
2
+ import { KernelRecord } from "@mark1russell7/prober";
3
+ //#region src/cell.d.ts
4
+ /**
5
+ * A fault is a failure that the harness injects, by the name of its manifestation. Each name is
6
+ * the last segment of the identifier of the manifestation in the corpus:
7
+ * - `blackhole` is two DROP rules in the network namespace of the target. Thus the measurement
8
+ * gets the kernel timers of the client.
9
+ * - `freeze-process` is the cgroup freezer on the target, while its kernel stays up.
10
+ * - `rst` answers the next segment of the client with a reset, from the namespace of the target.
11
+ * It is the reference for the noise of the harness itself.
12
+ * - `stall` is a black hole that stops by itself, `Cell.stall` after the client sends its
13
+ * request. The VM times it. It shows what recovers, and when.
14
+ */
15
+ type Fault = "blackhole" | "freeze-process" | "rst" | "stall";
16
+ /**
17
+ * A reference is the event that the time of a detector counts from: the event that arms the
18
+ * timer in its machine document. Keepalive counts from the last received segment. The give-up of
19
+ * the retransmissions counts from the first retransmission of the RTO timer, not from the tail
20
+ * loss probe one RTO before it. The request itself meets a reset, thus a reset counts from the
21
+ * send of the client.
22
+ */
23
+ type Reference = "last-received" | "first-retransmission" | "request-sent";
24
+ /**
25
+ * This prediction tells when a connection moves again after a stall. That occurs at the first
26
+ * firing of its retransmission timer after the stall stops, and its reply comes back one round
27
+ * trip later.
28
+ */
29
+ interface Recovers {
30
+ /** This count identifies the firing that gets through: the number of firings before it. */
31
+ readonly firing: number;
32
+ /** This time is the earliest return of the reply, from the send of the request. */
33
+ readonly low: Quantity;
34
+ /** This time is the latest return of the reply, from the send of the request. */
35
+ readonly high: Quantity;
36
+ /**
37
+ * This range is where the stall must stop, from the send, so that this firing is the first
38
+ * after it. The range starts after the latest time of the firing before, and it stops before
39
+ * the earliest time of this firing.
40
+ */
41
+ readonly heal: {
42
+ readonly low: Quantity;
43
+ readonly high: Quantity;
44
+ };
45
+ }
46
+ /**
47
+ * A prediction is what a cell predicts. Either a detection occurs in an interval [T_lo, T_hi]
48
+ * after the reference event, or nothing detects the fault in a window. Nothing detects a stall
49
+ * that is shorter than the give-up, and the connection recovers from it. The prediction then also
50
+ * tells when.
51
+ */
52
+ type Prediction = {
53
+ readonly detects: true;
54
+ /** This name is the timer of the machine that fires first, or the event that detects the fault, with its errno. */
55
+ readonly timer: string;
56
+ readonly errno: string;
57
+ readonly from: Reference;
58
+ /** This time is the closed form, less the most that the timer can fire early. */
59
+ readonly low: Quantity;
60
+ /** This time is the closed form, plus the most that the timer can fire late. */
61
+ readonly high: Quantity;
62
+ } | {
63
+ readonly detects: false;
64
+ readonly window: Quantity;
65
+ readonly recovers?: Recovers;
66
+ };
67
+ /** A target is the server that a cell connects to: a stock image. */
68
+ interface Target {
69
+ /** This image is a stock image, pinned by digest. */
70
+ readonly image: string;
71
+ readonly port: number;
72
+ /** This text is the log line that tells that the target serves. */
73
+ readonly ready: string;
74
+ /**
75
+ * These commands set and read one setting of the target, as argv in it. The name and the value
76
+ * come after them: `redis-cli CONFIG SET` and `CONFIG GET` for Redis. Without them, the target
77
+ * has no settings to set.
78
+ */
79
+ readonly settings?: {
80
+ readonly set: readonly string[];
81
+ readonly get: readonly string[];
82
+ };
83
+ }
84
+ /** A client is what the runs of a cell use: the TCP client of the harness, or a driver on its socket. */
85
+ type Client = "tcp" | "ioredis";
86
+ /**
87
+ * An observer is the side whose detection a cell measures. The client is one observer. The
88
+ * server is the other observer. It is the hold of the target on the connection of the client, at
89
+ * its server socket in its namespace. The fault is on the peer of the observer. Thus
90
+ * freeze-process freezes the target for the client, and the client for the server.
91
+ */
92
+ type Observer = "client" | "server";
93
+ /** A cell is one cell of the matrix: a topology, its settings, a workload, a fault and the prediction. */
94
+ interface Cell {
95
+ readonly name: string;
96
+ readonly target: Target;
97
+ /** 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
+ readonly sysctls: Readonly<Record<string, string>>;
99
+ readonly probe: {
100
+ /** This mode tells if the client is idle after its first reply, or sends a request into the fault. */
101
+ readonly mode: "idle" | "inflight";
102
+ /** This flag sets SO_KEEPALIVE without overrides for the socket: the keepalive sysctls of the namespace apply. */
103
+ readonly keepalive: boolean;
104
+ /** This time sets TCP_USER_TIMEOUT. Without it, the default of the kernel (off) applies. */
105
+ readonly userTimeout?: Quantity;
106
+ /**
107
+ * This text is the request that an in-flight run sends, as a Redis inline command. Without it,
108
+ * the request is PING. BLPOP with a timeout makes the server answer only at the timeout. Thus
109
+ * a cell on the server side has a reply in flight when the fault occurs.
110
+ */
111
+ readonly request?: string;
112
+ /**
113
+ * This driver replaces the TCP client of the harness, with its own options. It is ioredis 6 on
114
+ * Node, whose keepAlive is the keepalive delay of the socket. The driver then ignores `keepalive`
115
+ * and `userTimeout`.
116
+ */
117
+ readonly driver?: {
118
+ readonly name: "ioredis";
119
+ readonly keepAlive: Quantity;
120
+ };
121
+ };
122
+ readonly fault: Fault;
123
+ /** This time is the length of a stall, from the send of the request. */
124
+ readonly stall?: Quantity;
125
+ readonly prediction: Prediction;
126
+ /** This side is the side whose detection the cell measures. Without it, the side is the client. */
127
+ readonly observer?: Observer;
128
+ /** These are the settings of the target. The harness sets them in each environment before a client connects. */
129
+ readonly targetSettings?: Readonly<Record<string, string>>;
130
+ /** These are the sysctls of the network namespace of the target, for a cell on the server side: its tcp_retries2. */
131
+ readonly targetSysctls?: Readonly<Record<string, string>>;
132
+ /** This count is the number of runs of the cell (11), across new environments (two at least). */
133
+ readonly runs: number;
134
+ readonly environments: number;
135
+ /**
136
+ * This count is the number of environments at the same time, each with its own prober. Without
137
+ * it, the count is one. The calibration does its reference cells at 1, 4 and 8, to find how
138
+ * many environments the machine holds before its noise increases.
139
+ */
140
+ readonly concurrency?: number;
141
+ }
142
+ /** A probe event is one line that the client wrote. `t` is CLOCK_MONOTONIC in nanoseconds: an instant, not an amount. */
143
+ interface ProbeEvent {
144
+ readonly ev: string;
145
+ readonly t: number;
146
+ readonly [field: string]: unknown;
147
+ }
148
+ /** A run is one run of a cell, as the harness records it. */
149
+ interface Run {
150
+ readonly environment: number;
151
+ readonly index: number;
152
+ readonly events: readonly ProbeEvent[];
153
+ /** This bracket is the time of the injector around the commit of the fault, on the same clock. It is null for a freeze. */
154
+ readonly fault: {
155
+ readonly t0: number;
156
+ readonly t1: number;
157
+ } | null;
158
+ /** This bracket is the time around the commit that stopped a stall. */
159
+ readonly heal?: {
160
+ readonly t0: number;
161
+ readonly t1: number;
162
+ } | null;
163
+ /** These are the records of the prober from the start to the end of the run, in the two namespaces. */
164
+ readonly kernel: readonly KernelRecord[];
165
+ /** This inode identifies the network namespace of the client, thus the records of the client. */
166
+ readonly netns: number;
167
+ /** This inode identifies the namespace of the target, where the records of its server socket are. */
168
+ readonly targetNetns: number;
169
+ /**
170
+ * This count is the number of records that the prober lost in the run. One lost record makes
171
+ * the run void. Null also makes the run void: no clock line came after the run to tell the count.
172
+ */
173
+ readonly lost: number | null;
174
+ }
175
+ //#endregion
176
+ //#region src/observe.d.ts
177
+ /**
178
+ * An outcome tells how the wait of a run ended. The socket failed with an errno, or it read the
179
+ * end of the file. Or nothing came, or the reply came back after a stall.
180
+ */
181
+ type Outcome = {
182
+ readonly kind: "error";
183
+ readonly errno: string;
184
+ } | {
185
+ readonly kind: "eof";
186
+ } | {
187
+ readonly kind: "silent";
188
+ } | {
189
+ readonly kind: "answered";
190
+ };
191
+ /** A measurement is what one run measured, against the prediction of its cell. */
192
+ interface Measured {
193
+ readonly environment: number;
194
+ readonly index: number;
195
+ readonly outcome: Outcome;
196
+ /**
197
+ * This time is from the reference event of the prediction to the detection, both as the kernel
198
+ * stamped them in the records of the prober. It is null when nothing detected the fault.
199
+ */
200
+ readonly elapsed: Quantity | null;
201
+ /** This time is the same interval from the TCP_INFO samples of the client, the second vantage. */
202
+ readonly socket: Quantity | null;
203
+ /** This time is how long the client took to see the error after the kernel reported it. */
204
+ readonly noticed: Quantity | null;
205
+ /**
206
+ * These are the steps of the kernel after the reference: each keepalive probe, or each firing of
207
+ * the RTO timer. After a stall, they are each retransmission from the send, the loss probe first.
208
+ */
209
+ readonly steps: readonly Quantity[];
210
+ /** This count is the number of keepalive probes that the kernel sent. */
211
+ readonly probes: number;
212
+ /** This count is the number of segments that the kernel sent again. */
213
+ readonly retransmissions: number;
214
+ /** This count is the number of segments that the socket received after the request. */
215
+ readonly acks: number;
216
+ /** This text is the reason that the run cannot count, or null. A void run is not a pass and not a finding. */
217
+ readonly void: string | null;
218
+ /** This text tells what in the run disagrees with the prediction, other than its timing, or null. */
219
+ readonly finding: string | null;
220
+ }
221
+ /**
222
+ * This function reads one run against its cell, from the records of the kernel first.
223
+ * - The socket of the run is the socket that the client connected from, in its namespace.
224
+ * - The fault must hold before the event that it must meet. That is before the request goes into
225
+ * it, and before the first keepalive probe. A live peer answers that probe, and the answer
226
+ * starts the idle time again. A run where the fault was late is void. A run whose records the
227
+ * prober lost is also void.
228
+ */
229
+ export declare const measure: (cell: Cell, run: Run) => Measured;
230
+ //#endregion
231
+ //#region src/verdict.d.ts
232
+ /**
233
+ * This amount is the noise of the pipeline itself, δ_abs, as the calibration measured it. It is
234
+ * the 99.9th percentile of the whole latency of a reset, at the concurrency cap or less. Its
235
+ * minimum is one jiffy. On 2026-10-12 the worst of 60 resets at 1 and 4 environments took
236
+ * 1.141 ms, thus the minimum sets it to 4 ms. The calibration occurs again after each change to
237
+ * the harness, the kernel or Docker.
238
+ */
239
+ export declare const NOISE: Quantity;
240
+ /** This fraction, δ_rel, is how far the median of a cell can be outside the prediction and stay equivalent. */
241
+ export declare const RELATIVE = .05;
242
+ /** These are the run counts of a cell: 11, then 22 and 44 while its median crosses the edge of the band. */
243
+ export declare const STEPS: readonly [11, 22, 44];
244
+ /** A decision is the result of a verdict. */
245
+ type Decision = "holds" | "fails" | "extend" | "inconclusive";
246
+ /** An interval is a range of times. */
247
+ interface Interval {
248
+ readonly low: Quantity;
249
+ readonly high: Quantity;
250
+ }
251
+ /** A verdict is the judgement of a cell on its runs. */
252
+ interface Verdict {
253
+ readonly decision: Decision;
254
+ readonly counted: number;
255
+ readonly voided: number;
256
+ /** This band is where each run must land: [T_lo − δ_abs, T_hi + δ_abs]. It is null for a silent prediction. */
257
+ readonly band: Interval | null;
258
+ /** These are the runs outside the band, and the reason for each. Each is a finding. */
259
+ readonly outside: ReadonlyArray<{
260
+ readonly run: string;
261
+ readonly why: string;
262
+ }>;
263
+ readonly median: Quantity | null;
264
+ /** This interval is the order-statistic interval of the median at 95 %, or the whole range for fewer than six runs. */
265
+ readonly medianInterval: Interval | null;
266
+ readonly why: string;
267
+ }
268
+ /**
269
+ * This function gives the distribution-free interval of a median: [x_(k), x_(n+1−k)], for the
270
+ * largest k whose two tails each hold 2.5 % or less. For eleven runs, it gives [x_(2), x_(10)],
271
+ * at 98.8 %.
272
+ */
273
+ export declare const medianInterval: (sorted: readonly number[]) => [number, number];
274
+ /**
275
+ * This function gives the verdict of a cell, from two tests:
276
+ * - Containment: each run lands in [T_lo − δ_abs, T_hi + δ_abs]. A run outside is a finding.
277
+ * - Median equivalence: the interval of the median is in the band, made wider by δ_rel. An
278
+ * interval outside the band fails. An interval across its edge extends the cell to 22 runs, then
279
+ * 44, and then the verdict is inconclusive.
280
+ *
281
+ * A silent prediction holds only if no run detected the fault in its window. After a stall, the
282
+ * return of the reply gets the same test as a detection, by its interval, and a detection is a
283
+ * finding. The distribution test (a DKW band) needs a predicted distribution from a simulator,
284
+ * which does not exist at this time.
285
+ */
286
+ export declare const verdictOf: (prediction: Prediction, runs: readonly Measured[]) => Verdict;
287
+ //#endregion
288
+ //#region src/lab.d.ts
289
+ /** This function gives the image of the harness. */
290
+ export declare const harnessImage: () => Promise<string>;
291
+ /** This function gives the image of the Node client. */
292
+ export declare const nodeClientImage: () => Promise<string>;
293
+ /** A network fault is a fault that the injector applies, by the name of its manifestation. */
294
+ type NetworkFault = "blackhole" | "rst";
295
+ /**
296
+ * A fault side is the namespace that gets the rules of a fault: the namespace of the peer of the
297
+ * observer. For the client, it is the namespace of the target (the injector). For the server, it
298
+ * is the namespace of the client (its privileged admin). Rules in the namespace of the observer
299
+ * make its sends fail locally, with EPERM, and the network does not lose them. Thus its
300
+ * retransmissions do not go out. The retransmission tracepoint of the kernel then records none,
301
+ * because it fires only for a retransmission that went out.
302
+ */
303
+ type FaultSide = "target" | "client";
304
+ /** A manifest is what a lab records about its machine, its images and its settings. */
305
+ interface Manifest {
306
+ readonly kernel: string;
307
+ /** This text is the architecture of the machine, as uname -m gives it: x86_64. */
308
+ readonly architecture: string;
309
+ readonly hz: string | null;
310
+ readonly boot_id: string;
311
+ readonly docker: string;
312
+ /** This record is the target, and its settings as it read them back. */
313
+ readonly target: {
314
+ readonly image: string;
315
+ readonly id: string;
316
+ readonly settings: Readonly<Record<string, string>>;
317
+ /** These are the sysctls of its namespace, as read back after the harness set them. */
318
+ readonly sysctls: Readonly<Record<string, string>>;
319
+ };
320
+ readonly harness: {
321
+ readonly image: string;
322
+ readonly id: string;
323
+ readonly packages: string[];
324
+ };
325
+ /** This record is the client of each run: the TCP client of the harness, or a driver from its own image. */
326
+ readonly client: {
327
+ readonly kind: Client;
328
+ readonly image: string;
329
+ };
330
+ /** These are the namespaces that the prober watched, by inode. */
331
+ readonly netns: {
332
+ readonly client: number;
333
+ readonly target: number;
334
+ };
335
+ /** These are the sysctls of the client namespace, as read back after the harness set them. */
336
+ readonly sysctls: Readonly<Record<string, string>>;
337
+ }
338
+ /** A probe is the client container of one run. */
339
+ interface Probe {
340
+ /** This method gives the first event of one of these kinds, or it rejects after `within` ms. */
341
+ until(kinds: readonly string[], within: number): Promise<ProbeEvent>;
342
+ /** This method lets an in-flight probe send its request. */
343
+ go(): Promise<void>;
344
+ /** This method freezes the client with the cgroup freezer: its process stops, and its kernel continues to answer. */
345
+ freeze(): Promise<void>;
346
+ thaw(): Promise<void>;
347
+ readonly events: readonly ProbeEvent[];
348
+ stop(): Promise<void>;
349
+ }
350
+ /** A lab is one environment: the target, the client, the injector and the prober. */
351
+ interface Lab {
352
+ readonly targetIp: string;
353
+ readonly clientIp: string;
354
+ readonly netns: {
355
+ readonly client: number;
356
+ readonly target: number;
357
+ };
358
+ /** These are all the records that the prober wrote, in sequence. */
359
+ readonly kernel: readonly KernelRecord[];
360
+ /** This method sets the sysctls of the client namespace through its privileged sidecar, and reads them back. */
361
+ sysctl(values: Readonly<Record<string, string>>): Promise<Record<string, string>>;
362
+ /** This method sets the settings of the target in it before a client connects, and reads them back. */
363
+ configure(values: Readonly<Record<string, string>>): Promise<Record<string, string>>;
364
+ /** This method sets the sysctls of the target namespace through a privileged sidecar there, and reads them back. */
365
+ targetSysctl(values: Readonly<Record<string, string>>): Promise<Record<string, string>>;
366
+ /**
367
+ * This method applies a fault between the client and the target, from the namespace of the
368
+ * target. The fault is a black hole, or a reset that answers the next segment of the client. The stamps of
369
+ * the injector bracket it.
370
+ */
371
+ fault(kind: NetworkFault, at?: FaultSide): Promise<{
372
+ t0: number;
373
+ t1: number;
374
+ }>;
375
+ /** This method removes a fault. */
376
+ lift(kind: NetworkFault, at?: FaultSide): Promise<void>;
377
+ /**
378
+ * This method stops a black hole at an instant on the monotonic clock of the VM. The injector
379
+ * waits for that instant in the VM, thus the length of a stall does not include the latency of
380
+ * a docker exec. Stamps bracket it, as for a fault.
381
+ */
382
+ heal(at: FaultSide, when: number): Promise<{
383
+ t0: number;
384
+ t1: number;
385
+ }>;
386
+ /**
387
+ * This method removes what the two kernels learned from their earlier connections. These are
388
+ * the TCP metrics that each kernel keeps for a peer address when a connection closes. These metrics set the
389
+ * first round trip of the next connection, and thus its RTO. A run with these metrics is not
390
+ * independent of the run before it.
391
+ */
392
+ forget(): Promise<void>;
393
+ /** This method freezes the target with the cgroup freezer: its process stops, and its kernel continues to answer. */
394
+ freeze(): Promise<void>;
395
+ thaw(): Promise<void>;
396
+ probe(args: readonly string[]): Promise<Probe>;
397
+ /**
398
+ * This method waits for the next clock line of the prober, which gives the count of lost
399
+ * records. It gives null when no clock line comes in 5 s, because then nothing tells the count.
400
+ */
401
+ settle(): Promise<number | null>;
402
+ manifest(sysctls: Readonly<Record<string, string>>, settings?: Readonly<Record<string, string>>, targetSysctls?: Readonly<Record<string, string>>): Promise<Manifest>;
403
+ close(): Promise<void>;
404
+ }
405
+ /**
406
+ * This function opens one environment:
407
+ * - a new network and the stock target.
408
+ * - a client that holds its own network namespace, with a privileged sidecar in it for its
409
+ * sysctls. Docker Desktop keeps /proc/sys read-only in a container that operates.
410
+ * - an injector sidecar in the namespace of the target.
411
+ * - the prober, a privileged container that loads the kernel program and watches the two
412
+ * namespaces.
413
+ *
414
+ * The client of each run is its own container in the namespace of the client. It is the TCP
415
+ * client of the harness, or a driver from its own image (ioredis on Node). The two print the same
416
+ * lines.
417
+ */
418
+ export declare const openLab: (target: Target, kind?: Client) => Promise<Lab>;
419
+ //#endregion
420
+ //#region src/run.d.ts
421
+ /** A cell result is a cell with its manifests, its runs, their measurements and its verdict. */
422
+ interface CellResult {
423
+ readonly cell: Cell;
424
+ readonly manifests: readonly Manifest[];
425
+ readonly runs: readonly Run[];
426
+ readonly measured: readonly Measured[];
427
+ readonly verdict: Verdict;
428
+ /** These times are when its first environment opened and its last environment closed, on the wall clock. */
429
+ readonly at: {
430
+ readonly started: string;
431
+ readonly ended: string;
432
+ };
433
+ }
434
+ /**
435
+ * This function measures and judges the raw records of a cell, with the code of the harness at
436
+ * this time. `runCell` uses it during a cell. A holder of the raw records of a cell can use it
437
+ * again later. Thus the records get a new analysis after a change to observe.ts or to the verdict.
438
+ */
439
+ export declare const judge: (cell: Cell, runs: readonly Run[]) => {
440
+ measured: Measured[];
441
+ verdict: Verdict;
442
+ };
443
+ /** This function divides a count of runs across environments, as equally as possible. */
444
+ export declare const spread: (runs: number, environments: number) => number[];
445
+ /**
446
+ * This function does the runs of a cell. They go across new environments, and each environment records
447
+ * its manifest. Then the verdict judges the runs. A median across the edge of the band extends
448
+ * the cell from 11 runs to 22, and then to 44, each extension in new environments. With a
449
+ * concurrency of more than one, that many environments operate at the same time, each with its
450
+ * own prober. The runs then go across at least as many environments.
451
+ */
452
+ export declare const runCell: (cell: Cell, say?: (line: string) => void) => Promise<CellResult>;
453
+ //#endregion
454
+ export type { Cell, CellResult, Client, Decision, Fault, FaultSide, Interval, KernelRecord, Lab, Manifest, Measured, NetworkFault, Observer, Outcome, Prediction, Probe, ProbeEvent, Recovers, Reference, Run, Target, Verdict };
455
+ //# sourceMappingURL=index.d.ts.map