@mark1russell7/harness 0.14.4 → 0.15.1

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/build/index.js CHANGED
@@ -1,25 +1,308 @@
1
- import { A as unlockedManifests, C as programOf, D as subjectProblems, E as subjectClient, F as jarVersions, I as locksOf, L as npmVersions, M as LOCK_NAMES, N as cargoVersions, O as targetImageOf, P as goVersions, R as versionsOf, S as planOf, T as shippedSource, _ as kafkaClientImage, a as SHIPPED_NAMES, b as nodeHttpImage, c as buildRecordOf, d as harnessImage, f as harnessVersion, g as inContext, h as imageProblems, i as SHIPPED_CLIENTS, j as unpinnedBases, k as targetSourceOf, l as goHttpImage, m as imageOf, n as PINNED, o as amqpClientImage, p as hikariClientImage, r as REPOSITORY, s as basesOf, t as CLIENT_NAMES, u as grpcClientImage, v as mongoClientImage, w as routerImage, x as pgClientImage, y as nodeClientImage } from "./image-EZ8ioZ3L.js";
1
+ import { A as unlockedManifests, B as isHopPlace, C as programOf, D as subjectProblems, E as subjectClient, F as jarVersions, H as requestFirst, I as locksOf, L as npmVersions, M as LOCK_NAMES, N as cargoVersions, O as targetImageOf, P as goVersions, R as versionsOf, S as planOf, T as shippedSource, V as latestEndOf, _ as kafkaClientImage, a as SHIPPED_NAMES, b as nodeHttpImage, c as buildRecordOf, d as harnessImage, f as harnessVersion, g as inContext, h as imageProblems, i as SHIPPED_CLIENTS, j as unpinnedBases, k as targetSourceOf, l as goHttpImage, m as imageOf, n as PINNED, o as amqpClientImage, p as hikariClientImage, r as REPOSITORY, s as basesOf, t as CLIENT_NAMES, u as grpcClientImage, v as mongoClientImage, w as routerImage, x as pgClientImage, y as nodeClientImage, z as CLIENT_CAPABILITIES } from "./image-BxVTFZ1o.js";
2
2
  import { quantity, valueIn } from "@mark1russell7/quantity";
3
- import { median, medianInterval as medianInterval$1, quantileSorted } from "@mark1russell7/rigor-statistics";
3
+ import { median, medianInterval as medianInterval$1, quantile, quantileSorted } from "@mark1russell7/rigor-statistics";
4
+ import { rungVerdict } from "@mark1russell7/rigor-bench";
5
+ import { closedLoopPlan, measureLoad, schedule, scheduleHeld } from "@mark1russell7/rigor-chaos";
4
6
  import { createHash, randomBytes } from "node:crypto";
5
7
  import { execFile } from "node:child_process";
6
8
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
7
9
  import { release, tmpdir } from "node:os";
8
10
  import { join } from "node:path";
9
11
  import { existsSync, readFileSync } from "node:fs";
10
- //#region src/cell.ts
11
- /** This function tells if a fault place is a hop of a topology. */
12
- const isHopPlace = (place) => typeof place === "object" && "hop" in place;
12
+ //#region src/load.ts
13
+ /**
14
+ * The measure and the verdict of an open-loop cell (SYS-68). A client on a schedule sends each
15
+ * arrival at its time, thus a stall counts in each arrival that it delayed. The package rigor-chaos
16
+ * counts each latency from the planned time. The package rigor-bench judges a quantile by a
17
+ * distribution-free interval.
18
+ */
19
+ /** This path is where the client of an open-loop cell finds its schedule. */
20
+ const SCHEDULE_PATH = "/tmp/schedule.json";
21
+ /** This function gives the time of a line of the client, in ms from the start of the schedule. */
22
+ const fromStart = (start, t) => (t - start) / 1e6;
23
+ /**
24
+ * This function gives the measure of one run of a schedule from the lines of its client. It gives
25
+ * null when the client did not start the schedule. The first `sent` line of a client is its request
26
+ * before `ready`, and it names no arrival. The end of the run is its last line.
27
+ */
28
+ const loadOf = (events, plan) => {
29
+ const start = events.find((event) => event.ev === "scheduled")?.t;
30
+ if (start === void 0) return null;
31
+ return measureOf(events, start, plan, void 0);
32
+ };
33
+ /** This function tells if a status of a reply is a failure: a number outside 200 to 299. */
34
+ const failedStatus = (status) => typeof status === "number" && (status < 200 || status > 299);
35
+ /** This function gives the measure of the arrivals of one stream: the lines with the key `key`, or each line for no key. */
36
+ const measureOf = (events, start, plan, key) => {
37
+ const sent = /* @__PURE__ */ new Map();
38
+ const done = /* @__PURE__ */ new Map();
39
+ let failed = 0;
40
+ for (const event of events) {
41
+ const index = event["i"];
42
+ if (typeof index !== "number" || key !== void 0 && event["k"] !== key) continue;
43
+ if (event.ev === "sent") sent.set(index, fromStart(start, event.t));
44
+ if (event.ev === "replied") {
45
+ done.set(index, fromStart(start, event.t));
46
+ if (failedStatus(event["status"])) failed += 1;
47
+ }
48
+ }
49
+ const completions = [...sent].map(([index, at]) => ({
50
+ index,
51
+ sent: at,
52
+ done: done.get(index) ?? null
53
+ }));
54
+ const end = events.reduce((latest, event) => Math.max(latest, fromStart(start, event.t)), 0);
55
+ return {
56
+ ...measureLoad(plan, completions, end),
57
+ failed
58
+ };
59
+ };
60
+ /** This path is where the client `load` finds its plan. */
61
+ const LOAD_PATH = "/tmp/load.json";
62
+ /** This text is the format of the plan of the client `load`. */
63
+ const LOAD_FORMAT = "systems-load/1";
13
64
  /**
14
- * This function tells if the request of a client goes before the fault of its cell. That is so for a
15
- * hop event that comes while the request is in flight (`HopEvent.inFlight`). Then a request after the fault is void.
65
+ * This function gives the schedule of each stream of a cell, and the text of the plan of the client
66
+ * `load`. It throws for no stream, for two streams with one key and for a stream with no request.
16
67
  */
17
- const requestFirst = (cell) => cell.fault === "hop-event" && cell.event?.inFlight === true;
68
+ const loadPlanOf = (streams) => {
69
+ if (streams.length === 0) throw new RangeError("a plan of the client load has no stream");
70
+ const keys = /* @__PURE__ */ new Set();
71
+ for (const stream of streams) {
72
+ if (keys.has(stream.key)) throw new RangeError(`two streams have the key ${stream.key}`);
73
+ if (stream.requests.length === 0) throw new RangeError(`the stream ${stream.key} has no request`);
74
+ keys.add(stream.key);
75
+ }
76
+ const scheduled = streams.map((stream) => ({
77
+ key: stream.key,
78
+ schedule: schedule(stream.plan.profile, stream.plan.options),
79
+ requests: stream.requests
80
+ }));
81
+ return {
82
+ streams: scheduled.map(({ key, schedule: each }) => ({
83
+ key,
84
+ schedule: each
85
+ })),
86
+ content: JSON.stringify({
87
+ format: LOAD_FORMAT,
88
+ streams: scheduled
89
+ })
90
+ };
91
+ };
92
+ /**
93
+ * This function gives the measure of each stream of a run of the client `load`, and their pool. Each
94
+ * stream counts its arrivals from 0, thus the lines of a stream are the lines with its key. The pool
95
+ * sums the counts and joins the times, in the order of the streams. It gives null when the client did
96
+ * not start its schedule.
97
+ */
98
+ const streamsLoadOf = (events, streams) => {
99
+ const start = events.find((event) => event.ev === "scheduled")?.t;
100
+ if (start === void 0) return null;
101
+ const each = streams.map((stream) => [stream.key, measureOf(events, start, stream.schedule, stream.key)]);
102
+ return {
103
+ pool: poolOf(each.map(([, measure]) => measure)),
104
+ streams: Object.fromEntries(each)
105
+ };
106
+ };
107
+ /** This function gives the pool of some measures: it sums the counts and joins the times, in the order of the measures. */
108
+ const poolOf = (measures) => {
109
+ const sum = (count) => measures.reduce((total, measure) => total + count(measure), 0);
110
+ const join = (times) => measures.flatMap(times);
111
+ return {
112
+ planned: sum((measure) => measure.planned),
113
+ unsent: sum((measure) => measure.unsent),
114
+ unfinished: sum((measure) => measure.unfinished),
115
+ failed: sum((measure) => measure.failed ?? 0),
116
+ latency: join((measure) => measure.latency),
117
+ service: join((measure) => measure.service),
118
+ lag: join((measure) => measure.lag),
119
+ censored: join((measure) => measure.censored)
120
+ };
121
+ };
122
+ /**
123
+ * This time is the tolerance of a closed-loop estimate, in ms. A send later than its expected time
124
+ * by more than this tolerance is a pause of the client: a driver gap.
125
+ */
126
+ const CLOSED_TOLERANCE = 5;
127
+ /**
128
+ * This function gives the estimate of one closed-loop stream from its requests, in the order of their
129
+ * sends. Each time is a time of a line of the client. The client waits `interval` ms after each reply,
130
+ * thus the interval is also the pause of the driver. The function gives undefined for no request, and
131
+ * for a request with no reply before the last request.
132
+ */
133
+ const estimateOf = (requests, end, interval) => {
134
+ const start = requests[0]?.sent;
135
+ if (start === void 0 || requests.slice(0, -1).some((request) => request.done === null)) return void 0;
136
+ const loop = requests.map((request) => ({
137
+ sent: fromStart(start, request.sent),
138
+ done: request.done === null ? null : fromStart(start, request.done)
139
+ }));
140
+ const stop = Math.max(fromStart(start, end), ...loop.map((request) => request.done ?? request.sent), interval);
141
+ const run = closedLoopPlan(loop, {
142
+ interval,
143
+ end: stop,
144
+ tolerance: 5,
145
+ pause: interval
146
+ });
147
+ return {
148
+ ...measureLoad(run.plan, run.completions, stop),
149
+ hidden: run.hidden,
150
+ driverGaps: run.driverGaps,
151
+ driverGapTotal: run.driverGapTotal
152
+ };
153
+ };
154
+ /** This function gives the spread of a list of times in ms, or null for no time. */
155
+ const spreadOf$1 = (values) => values.length === 0 ? null : {
156
+ p50: quantity(quantile(values, .5, "nearest-rank"), "ms"),
157
+ p99: quantity(quantile(values, .99, "nearest-rank"), "ms"),
158
+ max: quantity(values.reduce((most, value) => Math.max(most, value), 0), "ms")
159
+ };
160
+ /** This time is the date of the record of a band and the date of its verdict. The record has no age, because it comes from this run. */
161
+ const NOW = "2026-01-01T00:00:00.000Z";
162
+ /**
163
+ * This function judges one band of the latency over the pooled arrivals of the counted runs. The
164
+ * top of the band reads the interval of rigor-bench: the band holds only if the interval is under its top.
165
+ * The latencies of one run can follow each other, thus the width of the interval is an estimate.
166
+ * The bottom of the band reads the quantile itself. It gives the text of the verdict, and if it holds.
167
+ */
168
+ const bandVerdict = (band, values, censored) => {
169
+ const high = valueIn(band.high, "ms");
170
+ const rung = {
171
+ name: "band",
172
+ at: {},
173
+ budgets: [{
174
+ kind: "quantile",
175
+ measure: "latency",
176
+ unit: "ms",
177
+ quantile: band.quantile,
178
+ limit: high
179
+ }],
180
+ test: "the cell"
181
+ };
182
+ const top = rungVerdict(rung, {
183
+ rung: "band",
184
+ commit: "the cell",
185
+ at: NOW,
186
+ real: ["the counted runs"],
187
+ synthetic: [],
188
+ evidence: [{
189
+ kind: "quantile",
190
+ measure: "latency",
191
+ values,
192
+ censored
193
+ }]
194
+ }, { now: NOW });
195
+ const read = values.length + censored.length === 0 ? null : quantile([...values, ...censored], band.quantile, "nearest-rank");
196
+ const low = band.low === void 0 ? null : valueIn(band.low, "ms");
197
+ const above = low === null || read !== null && read >= low;
198
+ const place = low === null ? `under ${high} ms` : `in [${low}, ${high}] ms`;
199
+ const outcome = top.outcome === "within" && !above ? "under the bottom of the band" : top.outcome;
200
+ return {
201
+ holds: top.outcome === "within" && above,
202
+ words: `p${band.quantile * 100} of the latency ${read === null ? "has no value" : `is ${read} ms`}, wanted ${place}: ${outcome}. ${top.reason}`
203
+ };
204
+ };
18
205
  /**
19
- * This function gives the latest end of a band in seconds, from the reply of the client. It is
20
- * the high bound, after the latest reference of the band (`startsBy`).
206
+ * This function judges the share of failed arrivals over the counted runs against a limit. Each
207
+ * planned arrival is a trial. An arrival fails if the client did not send it. It also fails with no
208
+ * reply at the end of its run, or with the status of a failure. The rate budget of rigor-bench
209
+ * reads the upper end of the Wilson interval at 0.95. Thus 381 arrivals with no failure hold a limit
210
+ * of 1 %, and fewer arrivals cannot decide.
21
211
  */
22
- const latestEndOf = (prediction) => valueIn(prediction.high, "s") + (prediction.startsBy === void 0 ? 0 : valueIn(prediction.startsBy, "s"));
212
+ const failuresVerdict = (limit, failures, trials) => {
213
+ const verdict = rungVerdict({
214
+ name: "failures",
215
+ at: {},
216
+ budgets: [{
217
+ kind: "rate",
218
+ measure: "failures",
219
+ limit
220
+ }],
221
+ test: "the cell"
222
+ }, {
223
+ rung: "failures",
224
+ commit: "the cell",
225
+ at: NOW,
226
+ real: ["the counted runs"],
227
+ synthetic: [],
228
+ evidence: [{
229
+ kind: "rate",
230
+ measure: "failures",
231
+ failures,
232
+ trials
233
+ }]
234
+ }, { now: NOW });
235
+ return {
236
+ holds: verdict.outcome === "within",
237
+ words: `${failures} of ${trials} arrivals failed, wanted a share under ${limit * 100} %: ${verdict.outcome}. ${verdict.reason}`
238
+ };
239
+ };
240
+ /**
241
+ * This function judges the load of an open-loop cell over its counted runs. A run that did not keep
242
+ * its schedule measures its driver, not the target, thus it is a finding of that run. Each
243
+ * band reads all the arrivals of the counted runs.
244
+ */
245
+ const loadVerdict = (prediction, runs) => {
246
+ const limit = valueIn(prediction.lag, "ms");
247
+ const outside = runs.flatMap((run) => {
248
+ if (run.load === void 0) return [{
249
+ run: run.name,
250
+ why: "the client never started its schedule"
251
+ }];
252
+ const check = scheduleHeld(run.load, limit);
253
+ return check.held ? [] : [{
254
+ run: run.name,
255
+ why: check.reason
256
+ }];
257
+ });
258
+ const measures = runs.flatMap((run) => run.load === void 0 ? [] : [run.load]);
259
+ const latency = measures.flatMap((measure) => measure.latency);
260
+ const censored = measures.flatMap((measure) => measure.censored);
261
+ const bands = prediction.bands.map((band) => bandVerdict(band, latency, censored));
262
+ const total = (count) => measures.reduce((sum, measure) => sum + count(measure), 0);
263
+ const planned = total((measure) => measure.planned);
264
+ const unsent = total((measure) => measure.unsent);
265
+ const unfinished = total((measure) => measure.unfinished);
266
+ const failed = total((measure) => measure.failed ?? 0);
267
+ const failures = prediction.failures === void 0 ? void 0 : failuresVerdict(prediction.failures, unsent + unfinished + failed, planned);
268
+ return {
269
+ holds: runs.length > 0 && outside.length === 0 && bands.every((band) => band.holds) && (failures?.holds ?? true),
270
+ outside,
271
+ load: {
272
+ planned,
273
+ unsent,
274
+ unfinished,
275
+ failed,
276
+ latency: spreadOf$1([...latency, ...censored]),
277
+ service: spreadOf$1(measures.flatMap((measure) => measure.service)),
278
+ lag: spreadOf$1(measures.flatMap((measure) => measure.lag.map((lag) => Math.abs(lag)))),
279
+ bands: bands.map((band) => band.words),
280
+ ...failures === void 0 ? {} : { failures: failures.words }
281
+ }
282
+ };
283
+ };
284
+ /** This function gives the estimate verdict of the counted runs, or undefined when no run has an estimate. */
285
+ const estimateVerdict = (estimates) => {
286
+ const runs = estimates.filter((estimate) => estimate !== void 0);
287
+ if (runs.length === 0) return void 0;
288
+ const pool = poolOf(runs);
289
+ const total = (count) => runs.reduce((sum, estimate) => sum + count(estimate), 0);
290
+ const hidden = total((estimate) => estimate.hidden);
291
+ const driverGaps = total((estimate) => estimate.driverGaps);
292
+ const driverGapTotal = total((estimate) => estimate.driverGapTotal);
293
+ const latency = spreadOf$1([...pool.latency, ...pool.censored]);
294
+ const seconds = (time) => (valueIn(time, "ms") / 1e3).toFixed(3);
295
+ const spread = latency === null ? "no latency" : `median ${seconds(latency.p50)} s, p99 ${seconds(latency.p99)} s, slowest ${seconds(latency.max)} s`;
296
+ const gaps = driverGaps === 0 ? "no pause of the client" : `${driverGaps} pauses of the client, ${Math.round(driverGapTotal)} ms in all`;
297
+ return {
298
+ planned: pool.planned,
299
+ hidden,
300
+ driverGaps,
301
+ driverGapTotal: quantity(driverGapTotal, "ms"),
302
+ latency,
303
+ words: `The estimate of the open loop puts back ${hidden} hidden sends, with ${gaps}: ${spread}.`
304
+ };
305
+ };
23
306
  //#endregion
24
307
  //#region src/verdict.ts
25
308
  /** This function gives the words of the outcome of a second request: its connection, a retry, and its end. */
@@ -345,14 +628,15 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
345
628
  why: "no run counted"
346
629
  };
347
630
  if (prediction.detects === null) {
348
- if (prediction.queue !== void 0 || prediction.ports !== void 0) {
631
+ const law = prediction.queue !== void 0 ? "queue" : prediction.ports !== void 0 ? "port range" : prediction.pause !== void 0 ? "paused consumer" : prediction.conntrack !== void 0 ? "conntrack table" : void 0;
632
+ if (law !== void 0) {
349
633
  const off = new Set(outside.map((each) => each.run)).size;
350
634
  return {
351
635
  ...empty,
352
636
  decision: off === 0 ? "holds" : "fails",
353
637
  band: null,
354
638
  outside,
355
- why: `${counted.length - off} of ${counted.length} runs hold the law of the ${prediction.queue === void 0 ? "port range" : "queue"}; ${prediction.why}`
639
+ why: `${counted.length - off} of ${counted.length} runs hold the law of the ${law}; ${prediction.why}`
356
640
  };
357
641
  }
358
642
  const crossed = counted.flatMap((run) => run.crossings === void 0 ? [] : [run.crossings]);
@@ -420,6 +704,33 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
420
704
  why: `${counted.length} runs measured: ${outcomes}; ${timed}.${late} ${prediction.why}`
421
705
  };
422
706
  }
707
+ if (counted.some((run) => run.load !== void 0)) {
708
+ const judged = loadVerdict(prediction.load ?? {
709
+ lag: quantity(Number.MAX_SAFE_INTEGER, "ms"),
710
+ bands: []
711
+ }, counted.map((run) => ({
712
+ name: name(run),
713
+ load: run.load
714
+ })));
715
+ const middleOfLoad = judged.load.latency?.p50;
716
+ const failed = judged.load.failed === 0 ? "" : `, ${judged.load.failed} with the status of a failure`;
717
+ const read = [
718
+ `${counted.length} runs on a schedule, ${judged.load.planned} arrivals, ${judged.load.unsent} not sent, ${judged.load.unfinished} with no reply${failed}.`,
719
+ ...judged.load.bands,
720
+ ...judged.load.failures === void 0 ? [] : [judged.load.failures],
721
+ prediction.why
722
+ ].join(" ");
723
+ return {
724
+ ...empty,
725
+ decision: prediction.load === void 0 ? "measured" : judged.holds ? "holds" : "fails",
726
+ band: null,
727
+ outside: judged.outside,
728
+ median: middleOfLoad === void 0 ? null : s(valueIn(middleOfLoad, "s")),
729
+ ...judged.load.latency === null ? {} : { slowest: s(valueIn(judged.load.latency.max, "s")) },
730
+ load: judged.load,
731
+ why: read
732
+ };
733
+ }
423
734
  const samples = counted.flatMap((run) => (run.samples ?? []).map((sample) => valueIn(sample, "s"))).sort((a, b) => a - b);
424
735
  const retransmissions = counted.reduce((sum, run) => sum + run.retransmissions, 0);
425
736
  const ended = counted.filter((run) => run.outcome.kind === "error" || run.outcome.kind === "eof");
@@ -433,7 +744,9 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
433
744
  reordering.fast,
434
745
  reordering.seen
435
746
  ].some((count) => (count ?? 0) > 0) ? ` Reordering: ${reorderingWords(reordering)}.` : "";
436
- const why = samples.length === 0 ? `${counted.length} runs measured, with no reply: ${prediction.why}` : `${counted.length} runs, ${samples.length} replies: median ${s(median(samples)).value} s, p99 ${s(quantileOf(samples, .99)).value} s, slowest ${s(samples.at(-1)).value} s, ${retransmissions} retransmissions, ${ended.length} connections ended.${reordered} ${prediction.why}`;
747
+ const estimate = estimateVerdict(counted.map((run) => run.estimate));
748
+ const estimated = estimate === void 0 ? "" : ` ${estimate.words}`;
749
+ const why = samples.length === 0 ? `${counted.length} runs measured, with no reply: ${prediction.why}` : `${counted.length} runs, ${samples.length} replies: median ${s(median(samples)).value} s, p99 ${s(quantileOf(samples, .99)).value} s, slowest ${s(samples.at(-1)).value} s, ${retransmissions} retransmissions, ${ended.length} connections ended.${reordered}${estimated} ${prediction.why}`;
437
750
  return {
438
751
  ...empty,
439
752
  decision: "measured",
@@ -442,6 +755,7 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
442
755
  median: samples.length === 0 ? null : s(median(samples)),
443
756
  ...samples.length === 0 ? {} : { slowest: s(samples.at(-1)) },
444
757
  ...reordering === void 0 ? {} : { reordering },
758
+ ...estimate === void 0 ? {} : { estimate },
445
759
  why
446
760
  };
447
761
  }
@@ -1351,6 +1665,285 @@ const measureQueue = (run, expected) => {
1351
1665
  };
1352
1666
  };
1353
1667
  //#endregion
1668
+ //#region src/conntrack.ts
1669
+ /** This errno tells that a connect got a slot. */
1670
+ const NONE = "none";
1671
+ const numberOr = (line, field) => {
1672
+ const value = line?.[field];
1673
+ return typeof value === "number" ? value : null;
1674
+ };
1675
+ /** This function gives the time from one stamp to a stamp of a field of a line, when the line has it. */
1676
+ const untilField = (from, line, field) => {
1677
+ const stamp = numberOr(line, field);
1678
+ return from === void 0 || stamp === null ? null : between$2(from.t, stamp);
1679
+ };
1680
+ const stallOf = (line) => line === void 0 ? null : {
1681
+ errno: String(line["errno"]),
1682
+ time: quantity(Math.round((numberOr(line, "elapsed") ?? 0) / 1e3) / 1e6, "s"),
1683
+ drops: numberOr(line, "drops") ?? 0,
1684
+ retransFail: numberOr(line, "retrans_fail") ?? 0
1685
+ };
1686
+ /** This function tells where a reading differs from the law of its cell. */
1687
+ const findingsOf$1 = (read, failed, expected) => {
1688
+ const found = [];
1689
+ const { max, neighbor, refused, drained, pressure } = expected;
1690
+ if (typeof failed === "string") found.push(`a connect failed with ${failed} before the client held its flows`);
1691
+ if (read.flows !== max) found.push(`the client held ${read.flows} flows, not ${max}`);
1692
+ if (read.count !== max) found.push(`the count read ${read.count} at the fill, not ${max}`);
1693
+ if (neighbor !== void 0 && read.neighbor !== neighbor) found.push(`the namespace of the target read ${read.neighbor ?? "nothing"}, not ${neighbor}`);
1694
+ if (refused !== void 0) {
1695
+ const stall = read.stall;
1696
+ if (stall === null) found.push("no connect after the fill ended");
1697
+ else {
1698
+ if (stall.errno !== refused.errno) found.push(`the connect after the fill ended with ${stall.errno}, not ${refused.errno}`);
1699
+ if (!inside(stall.time, refused.within)) found.push(`the connect after the fill took ${valueIn(stall.time, "s")} s, outside ${bandWords(refused.within)}`);
1700
+ if (stall.drops !== refused.drops) found.push(`the field drop rose by ${stall.drops}, not ${refused.drops}`);
1701
+ if (stall.retransFail !== refused.retransFail) found.push(`TCPRetransFail rose by ${stall.retransFail}, not ${refused.retransFail}`);
1702
+ }
1703
+ }
1704
+ if (drained !== void 0) {
1705
+ for (const [name, time] of [["first fall", read.firstFall], ["zero", read.zero]]) if (time === null || !inside(time, drained)) found.push(`the ${name} of the count came at ${time === null ? "no time" : `${valueIn(time, "s")} s`}, outside ${bandWords(drained)}`);
1706
+ }
1707
+ if (pressure !== void 0) {
1708
+ const stall = read.stall;
1709
+ if (read.closed !== max) found.push(`the count read ${read.closed ?? "nothing"} after each side closed, not ${max}`);
1710
+ const { highest } = pressure;
1711
+ if (highest === void 0) {
1712
+ if (read.lowest !== pressure.lowest) found.push(`the lowest count was ${read.lowest ?? "nothing"}, not ${pressure.lowest}`);
1713
+ if (read.after !== pressure.after) found.push(`the count read ${read.after ?? "nothing"} after the connect, not ${pressure.after}`);
1714
+ } else if (read.lowest === null || read.lowest < pressure.lowest || read.lowest > highest) found.push(`the lowest count was ${read.lowest ?? "nothing"}, outside ${pressure.lowest} to ${highest}`);
1715
+ else if (read.after !== read.lowest + 1) found.push(`the count read ${read.after ?? "nothing"} after the connect, not ${read.lowest + 1}`);
1716
+ if (stall === null) found.push("no connect after the close ended");
1717
+ else {
1718
+ if (stall.errno !== NONE) found.push(`the connect after the close ended with ${stall.errno}`);
1719
+ if (!pressure.points.some((point) => inside(stall.time, point))) found.push(`the connect after the close took ${valueIn(stall.time, "s")} s, on no point of the schedule`);
1720
+ }
1721
+ }
1722
+ return found;
1723
+ };
1724
+ /**
1725
+ * This function reads one run of a conntrack cell from the lines of its client and its target. An
1726
+ * `unsettled` line makes the run void. The entries of the run before it were still in the table, thus
1727
+ * the run did not start from an empty table.
1728
+ */
1729
+ const measureConntrack = (run, expected) => {
1730
+ const { events, kernel } = run;
1731
+ const retransmissions = kernel.filter((record) => record.netns === run.netns && record.ev === "retrans").length;
1732
+ const base = {
1733
+ environment: run.environment,
1734
+ index: run.index,
1735
+ outcome: { kind: "answered" },
1736
+ elapsed: null,
1737
+ socket: null,
1738
+ noticed: null,
1739
+ steps: [],
1740
+ probes: 0,
1741
+ retransmissions,
1742
+ acks: 0
1743
+ };
1744
+ const voided = (why) => ({
1745
+ ...base,
1746
+ void: why,
1747
+ finding: null
1748
+ });
1749
+ const unsettled = events.find((event) => event.ev === "unsettled");
1750
+ if (unsettled !== void 0) return voided(`the table held ${String(unsettled["count"])} entries at the end of the wait before the run`);
1751
+ if (events.find((event) => event.ev === "done") === void 0) {
1752
+ const error = events.find((event) => event.ev === "error");
1753
+ return voided(error === void 0 ? "the client did not reach its end" : `the client did not reach its end: ${String(error["what"])}`);
1754
+ }
1755
+ if (run.lost === null) return voided("the prober wrote no clock line after the run, so nothing says what it dropped");
1756
+ if (run.lost > 0) return voided(`the prober dropped ${run.lost} records`);
1757
+ const full = events.find((event) => event.ev === "full");
1758
+ if (full === void 0) return voided("the client wrote no line for its fill");
1759
+ const stall = events.find((event) => event.ev === "stall");
1760
+ const closing = events.find((event) => event.ev === "closing");
1761
+ const drained = events.find((event) => event.ev === "drained");
1762
+ const closed = events.find((event) => event.ev === "closed");
1763
+ const neighbor = (run.server ?? []).filter((line) => line.ev === "count" && line.t >= full.t && (stall === void 0 || line.t <= stall.t)).at(-1);
1764
+ const conntrack = {
1765
+ flows: numberOr(full, "flows") ?? 0,
1766
+ count: numberOr(full, "count") ?? 0,
1767
+ neighbor: numberOr(neighbor, "count"),
1768
+ stall: stallOf(stall),
1769
+ firstFall: untilField(closing, drained, "first_fall"),
1770
+ zero: untilField(closing, drained, "zero"),
1771
+ closed: numberOr(closed, "count"),
1772
+ lowest: numberOr(drained ?? stall, "lowest"),
1773
+ after: numberOr(stall, "after")
1774
+ };
1775
+ const found = findingsOf$1(conntrack, full["errno"], expected);
1776
+ return {
1777
+ ...base,
1778
+ void: null,
1779
+ finding: found.length === 0 ? null : found.join(". "),
1780
+ conntrack
1781
+ };
1782
+ };
1783
+ //#endregion
1784
+ //#region src/pause.ts
1785
+ /** This expression finds the stamp of a log line of a broker, as its console writes it, on UTC. */
1786
+ const LOG_TIME = /\[(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d),(\d{3})\]/;
1787
+ /** This function gives the time of a log line on the monotonic clock of a member, in ns, from the `start` line of that member. It is null for a line with no stamp. */
1788
+ const logTimeOf = (line, start) => {
1789
+ const found = LOG_TIME.exec(line);
1790
+ if (found === null) return null;
1791
+ const wall = Date.UTC(Number(found[1]), Number(found[2]) - 1, Number(found[3]), Number(found[4]), Number(found[5]), Number(found[6]), Number(found[7]));
1792
+ return start.t + (wall - Number(start["wall"])) * 1e6;
1793
+ };
1794
+ /** This function gives the time between two instants of the monotonic clock, in s, to 1 microsecond. */
1795
+ const secondsBetween = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
1796
+ const isInside = (time, band) => {
1797
+ const seconds = valueIn(time, "s");
1798
+ return seconds >= valueIn(band.low, "s") && seconds <= valueIn(band.high, "s");
1799
+ };
1800
+ const bandText = (band) => `[${valueIn(band.low, "s")}, ${valueIn(band.high, "s")}] s`;
1801
+ const isChange = (event) => event.ev === "assigned" || event.ev === "revoked" || event.ev === "lost";
1802
+ /** This function gives the partitions that a line of the rebalance listener names, in its field `partitions` or `held`. */
1803
+ const partitionsOf = (event, field) => event[field] ?? [];
1804
+ /** This function gives the last line of the rebalance listener in a list of lines. */
1805
+ const lastChange = (events) => events.findLast(isChange);
1806
+ /** This function gives the offset that a commit names for a partition, or null. */
1807
+ const offsetOf = (event, partition) => {
1808
+ const value = event["offsets"]?.[String(partition)];
1809
+ return typeof value === "number" ? value : null;
1810
+ };
1811
+ /**
1812
+ * This function tells if the two members can take the fault. Each member holds one partition, the
1813
+ * two partitions differ, and the first member has two acknowledged commits on its partition after its
1814
+ * last change. Thus the first member reads and commits at a steady rate.
1815
+ */
1816
+ const pauseReady = (first, other) => {
1817
+ const mine = lastChange(first);
1818
+ const theirs = lastChange(other);
1819
+ if (mine === void 0 || theirs === void 0) return false;
1820
+ const held = partitionsOf(mine, "held");
1821
+ const others = partitionsOf(theirs, "held");
1822
+ if (held.length !== 1 || others.length !== 1 || held[0] === others[0]) return false;
1823
+ return first.filter((event) => event.ev === "commit" && event.t > mine.t && offsetOf(event, held[0]) !== null).length >= 2;
1824
+ };
1825
+ /** This function tells if a log line of a broker tells that a member left a group, under either protocol. */
1826
+ const isFenceLine = (line) => /Member \S+ fenced from the group because|Member \S+ in group \S+ has failed, removing it from the group/.test(line);
1827
+ /** This function tells if a fence line names a group. The two protocols write the name in different words. */
1828
+ const namesGroup = (line, group) => line.includes(`[GroupId ${group}]`) || line.includes(`in group ${group} has`);
1829
+ /** This function compares a reading to the law, and gives each disagreement. */
1830
+ const findingsOf = (reading, prediction, fences, otherReports) => {
1831
+ const found = [];
1832
+ const { commit, fence, takeover } = reading;
1833
+ if (prediction.fence === null) {
1834
+ if (fences.length > 0) found.push(`the group removed a member: ${fences[0]}`);
1835
+ if (otherReports) found.push("the other member reported the partition");
1836
+ } else {
1837
+ if (fence === null) found.push("no log line tells of a fence, and the other member did not report the partition");
1838
+ else if (!isInside(fence, prediction.fence)) found.push(`the fence is ${valueIn(fence, "s")} s, outside ${bandText(prediction.fence)}`);
1839
+ if (reading.otherFirst !== reading.lastAck) found.push(`the other member started at offset ${reading.otherFirst}, and the last commit is ${reading.lastAck}`);
1840
+ }
1841
+ if (prediction.takeover !== null) {
1842
+ if (takeover === null) found.push("the other member did not report the partition");
1843
+ else if (!isInside(takeover, prediction.takeover)) found.push(`the other member reported the partition at ${valueIn(takeover, "s")} s, outside ${bandText(prediction.takeover)}`);
1844
+ }
1845
+ if (commit === null) {
1846
+ found.push("the member made no commit after the stop");
1847
+ return found;
1848
+ }
1849
+ switch (prediction.thaw) {
1850
+ case "fails":
1851
+ if (commit.name !== "CommitFailedException") found.push(`the commit after the stop gave ${commit.name ?? "an acknowledgment"}, not CommitFailedException`);
1852
+ break;
1853
+ case "acknowledged":
1854
+ if (!commit.acked) found.push(`the commit after the stop failed with ${commit.name}`);
1855
+ if (commit.epoch !== reading.epochBefore) found.push(`the epoch changed from ${reading.epochBefore} to ${commit.epoch}`);
1856
+ break;
1857
+ case "rejoin": {
1858
+ const within = prediction.rejoinWithin;
1859
+ if (reading.rejoin === null) found.push("the member did not get a new epoch");
1860
+ else if (within !== void 0 && valueIn(reading.rejoin, "s") > valueIn(within, "s")) found.push(`the new epoch came at ${valueIn(reading.rejoin, "s")} s, above ${valueIn(within, "s")} s`);
1861
+ if (!commit.acked) found.push(`the stale commit failed with ${commit.name}`);
1862
+ if (reading.reassigned) found.push("the member held the partition again before its commit");
1863
+ if (reading.committed !== reading.stale) found.push(`the committed offset is ${reading.committed}, and the stale commit named ${reading.stale}`);
1864
+ if (reading.stale === null || reading.otherLast === null || reading.stale >= reading.otherLast) found.push(`the stale commit ${reading.stale} is not below the progress ${reading.otherLast} of the other member`);
1865
+ break;
1866
+ }
1867
+ }
1868
+ return found;
1869
+ };
1870
+ /** This function reads one run of a paused consumer against its law. */
1871
+ const measurePause = (run, prediction) => {
1872
+ const retransmissions = run.kernel.filter((record) => record.netns === run.netns && record.ev === "retrans").length;
1873
+ const base = {
1874
+ environment: run.environment,
1875
+ index: run.index,
1876
+ outcome: { kind: "answered" },
1877
+ elapsed: null,
1878
+ socket: null,
1879
+ noticed: null,
1880
+ steps: [],
1881
+ probes: 0,
1882
+ retransmissions,
1883
+ acks: 0
1884
+ };
1885
+ const voided = (why) => ({
1886
+ ...base,
1887
+ void: why,
1888
+ finding: null
1889
+ });
1890
+ const { fault, heal, pause } = run;
1891
+ if (fault === null || pause === void 0) return voided("the partitions were not one for each member before the stop");
1892
+ if (heal === void 0 || heal === null) return voided("the stop did not end");
1893
+ const stop = fault.t0;
1894
+ const thaw = heal.t0;
1895
+ const before = run.events.filter((event) => event.t < stop);
1896
+ const after = run.events.filter((event) => event.t >= stop);
1897
+ const start = run.events.find((event) => event.ev === "start");
1898
+ if (start === void 0) return voided("the member wrote no start line");
1899
+ const mine = lastChange(before);
1900
+ const theirs = lastChange(pause.other.filter((event) => event.t < stop));
1901
+ const held = mine === void 0 ? [] : partitionsOf(mine, "held");
1902
+ const others = theirs === void 0 ? [] : partitionsOf(theirs, "held");
1903
+ const partition = held[0];
1904
+ if (partition === void 0 || held.length !== 1 || others.length !== 1 || others[0] === partition) return voided("the partitions were not one for each member before the stop");
1905
+ if (after.find((event) => event.ev === "pause")?.["in"] !== "process") return voided("the stop landed outside the processing of a batch");
1906
+ if (before.findLast((event) => event.ev === "read")?.["partition"] !== partition) return voided("the last read before the stop was on another partition");
1907
+ const lastCommit = before.findLast((event) => event.ev === "commit" && offsetOf(event, partition) !== null);
1908
+ if (lastCommit === void 0) return voided("the member had no acknowledged commit on its partition before the stop");
1909
+ const member = String(mine?.["member"]);
1910
+ const fenceAt = pause.fences.filter((line) => line.includes(member)).map((line) => logTimeOf(line, start)).find((time) => time !== null && time >= stop) ?? null;
1911
+ const taken = pause.other.find((event) => event.ev === "assigned" && event.t >= stop && partitionsOf(event, "partitions").includes(partition));
1912
+ if (prediction.fence !== null && fenceAt === null && taken !== void 0) return voided("the broker log lost the line of the fence");
1913
+ const thawed = after.find((event) => event.ev === "commit" || event.ev === "commit-fail");
1914
+ const rejoined = after.find((event) => event.ev === "rejoined");
1915
+ const ackAt = thawed?.t ?? Number.POSITIVE_INFINITY;
1916
+ const otherCommit = pause.other.findLast((event) => event.ev === "commit" && event.t < ackAt && offsetOf(event, partition) !== null);
1917
+ const committed = after.find((event) => event.ev === "committed");
1918
+ const epochOf = (event) => typeof event?.["epoch"] === "number" ? event["epoch"] : null;
1919
+ const pauseReading = {
1920
+ partition,
1921
+ fence: fenceAt === null ? null : secondsBetween(stop, fenceAt),
1922
+ takeover: taken === void 0 ? null : secondsBetween(stop, taken.t),
1923
+ commit: thawed === void 0 ? null : {
1924
+ acked: thawed.ev === "commit",
1925
+ name: typeof thawed["name"] === "string" ? thawed["name"] : null,
1926
+ message: typeof thawed["message"] === "string" ? thawed["message"] : null,
1927
+ epoch: epochOf(thawed)
1928
+ },
1929
+ epochBefore: epochOf(lastCommit),
1930
+ lastAck: offsetOf(lastCommit, partition),
1931
+ otherFirst: pause.other.find((event) => event.ev === "read" && event.t >= stop && event["partition"] === partition)?.["first"] ?? null,
1932
+ otherLast: otherCommit === void 0 ? null : offsetOf(otherCommit, partition),
1933
+ rejoin: rejoined === void 0 ? null : secondsBetween(thaw, rejoined.t),
1934
+ reassigned: after.some((event) => event.ev === "assigned" && event.t >= thaw && event.t <= ackAt && partitionsOf(event, "partitions").includes(partition)),
1935
+ committed: committed === void 0 ? null : offsetOf(committed, partition),
1936
+ stale: thawed === void 0 ? null : offsetOf(thawed, partition)
1937
+ };
1938
+ const findings = findingsOf(pauseReading, prediction, pause.fences, taken !== void 0);
1939
+ return {
1940
+ ...base,
1941
+ void: null,
1942
+ finding: findings.length === 0 ? null : findings.join(". "),
1943
+ pause: pauseReading
1944
+ };
1945
+ };
1946
+ //#endregion
1354
1947
  //#region src/ports.ts
1355
1948
  /** This errno ends a search that found no port. */
1356
1949
  const NO_PORT = "EADDRNOTAVAIL";
@@ -2312,6 +2905,21 @@ const requestOf = (events) => {
2312
2905
  };
2313
2906
  /** This function reads the socket of one run against its cell, as `measure` does without the second request of an HTTP client. */
2314
2907
  const measureSocket = (cell, run) => {
2908
+ if (run.missing !== void 0) return {
2909
+ environment: run.environment,
2910
+ index: run.index,
2911
+ outcome: { kind: "answered" },
2912
+ elapsed: null,
2913
+ socket: null,
2914
+ noticed: null,
2915
+ steps: [],
2916
+ probes: 0,
2917
+ retransmissions: 0,
2918
+ acks: 0,
2919
+ void: `the environment cannot do this cell: ${run.missing}`,
2920
+ finding: null
2921
+ };
2922
+ if (cell.prediction.detects === null && cell.prediction.pause !== void 0) return measurePause(run, cell.prediction.pause);
2315
2923
  if (cell.group !== void 0 || cell.replicaSet !== void 0 || cell.cluster !== void 0 || cell.pair !== void 0 || cell.kafka !== void 0) {
2316
2924
  const read = measureGroup(cell, run);
2317
2925
  return {
@@ -2332,6 +2940,7 @@ const measureSocket = (cell, run) => {
2332
2940
  }
2333
2941
  if (cell.prediction.detects === null && cell.prediction.queue !== void 0) return measureQueue(run, cell.prediction.queue);
2334
2942
  if (cell.prediction.detects === null && cell.prediction.ports !== void 0) return measurePorts(run, cell.prediction.ports);
2943
+ if (cell.prediction.detects === null && cell.prediction.conntrack !== void 0) return measureConntrack(run, cell.prediction.conntrack);
2335
2944
  if (cell.prediction.detects !== null && cell.prediction.driver !== void 0) return measureDriver(cell, run);
2336
2945
  const { events, kernel } = run;
2337
2946
  const ready = events.find((event) => event.ev === "ready");
@@ -2462,13 +3071,26 @@ const measureSocket = (cell, run) => {
2462
3071
  forgot: forgotten === void 0 ? null : between(lastRx, forgotten.t)
2463
3072
  };
2464
3073
  if (prediction.detects === null) {
2465
- const sends = events.filter((event) => event.ev === "sent").slice(1);
3074
+ const load = run.streams !== void 0 ? streamsLoadOf(events, run.streams)?.pool ?? null : run.schedule === void 0 ? void 0 : loadOf(events, run.schedule);
3075
+ if (load === null) return {
3076
+ ...base,
3077
+ ...nothing,
3078
+ void: "the client never started its schedule"
3079
+ };
3080
+ const sends = load === void 0 ? events.filter((event) => event.ev === "sent").slice(1) : [];
2466
3081
  const answers = events.some((event) => event.ev === "replied") ? "replied" : "data";
2467
- const samples = sends.flatMap((send, n) => {
3082
+ const requests = sends.map((send, n) => {
2468
3083
  const next = sends[n + 1]?.t ?? Infinity;
2469
- const reply = events.find((event) => event.ev === answers && event.t > send.t && event.t < next);
2470
- return reply === void 0 ? [] : [between(send.t, reply.t)];
3084
+ return {
3085
+ send,
3086
+ reply: events.find((event) => event.ev === answers && event.t > send.t && event.t < next)
3087
+ };
2471
3088
  });
3089
+ const samples = requests.flatMap(({ send, reply }) => reply === void 0 ? [] : [between(send.t, reply.t)]);
3090
+ const estimate = cell.probe.mode === "stream" && cell.probe.driver === void 0 && cell.probe.interval !== void 0 ? estimateOf(requests.map(({ send, reply }) => ({
3091
+ sent: send.t,
3092
+ done: reply?.t ?? null
3093
+ })), events.reduce((latest, event) => Math.max(latest, event.t), 0), valueIn(cell.probe.interval, "ms")) : void 0;
2472
3094
  const from = prediction.from === void 0 ? void 0 : referenceAt(prediction.from);
2473
3095
  const until = ended ?? after.at(-1)?.t;
2474
3096
  const peerRecords = peerCookie === void 0 ? [] : kernel.filter((record) => record.netns === peerNetns && record.cookie === peerCookie);
@@ -2500,6 +3122,8 @@ const measureSocket = (cell, run) => {
2500
3122
  noticed: prediction.from === void 0 ? null : noticed,
2501
3123
  steps: retransmitted.filter((record) => record.mono > request.t).map((record) => between(request.t, record.mono)),
2502
3124
  samples,
3125
+ ...load === void 0 ? {} : { load },
3126
+ ...estimate === void 0 ? {} : { estimate },
2503
3127
  reordering: reorderingOf(socket.filter((record) => record.mono > request.t), peerRecords.filter((record) => record.mono > request.t)),
2504
3128
  ...crossings,
2505
3129
  void: voided
@@ -3446,6 +4070,7 @@ const clientOf = async (kind) => {
3446
4070
  clientImage: await imageOf(client.image),
3447
4071
  command: client.command,
3448
4072
  clientKind: typeof kind === "string" ? kind : "image",
4073
+ ...client.capabilities === void 0 || client.capabilities.length === 0 ? {} : { capabilities: client.capabilities },
3449
4074
  ...typeof client.image === "string" ? {} : { source: client.image }
3450
4075
  };
3451
4076
  };
@@ -3477,18 +4102,21 @@ const deviceOf = (route) => {
3477
4102
  */
3478
4103
  const openLab = async (target, kind = "tcp", topology) => {
3479
4104
  const image = await harnessImage();
3480
- const { clientImage, command, clientKind, source } = await clientOf(kind);
4105
+ const { clientImage, command, clientKind, capabilities, source } = await clientOf(kind);
3481
4106
  const hops = topology?.hops ?? [];
3482
4107
  const { GenericContainer, Network, Wait, getContainerRuntimeClient } = await containers();
3483
4108
  const runtime = await getContainerRuntimeClient();
3484
4109
  const opened = [];
3485
4110
  const networks = [];
3486
4111
  const serverLines = lines();
4112
+ let restoreHost = () => Promise.resolve([]);
3487
4113
  let fabric;
3488
4114
  const close = async () => {
4115
+ const restored = await restoreHost().then(() => null, (error) => error);
3489
4116
  await fabric?.close();
3490
4117
  for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
3491
4118
  for (const network of networks.reverse()) await network.stop().catch(() => void 0);
4119
+ if (restored !== null) throw restored;
3492
4120
  };
3493
4121
  /** This function gives the address of a container on a network, as Docker has it at this time. A container can join a network after its start. */
3494
4122
  const ipOn = async (container, network) => {
@@ -3793,6 +4421,33 @@ const openLab = async (target, kind = "tcp", topology) => {
3793
4421
  };
3794
4422
  };
3795
4423
  const proxies = inChain.filter((hop) => hop.hop.kind === "proxy");
4424
+ let hostAdmin;
4425
+ const hostChanges = [];
4426
+ let hostDone;
4427
+ restoreHost = async () => {
4428
+ if (hostDone !== void 0) return hostDone;
4429
+ const restored = [];
4430
+ for (const change of [...hostChanges].reverse()) {
4431
+ await output(hostAdmin, [
4432
+ "sysctl",
4433
+ "-q",
4434
+ "-w",
4435
+ `${change.name}=${change.before}`
4436
+ ]);
4437
+ const read = await output(hostAdmin, [
4438
+ "sysctl",
4439
+ "-n",
4440
+ change.name
4441
+ ]);
4442
+ if (read !== change.before) throw new Error(`${change.name} reads ${read} after the restore, not ${change.before}`);
4443
+ restored.unshift({
4444
+ ...change,
4445
+ restored: read
4446
+ });
4447
+ }
4448
+ hostDone = restored;
4449
+ return restored;
4450
+ };
3796
4451
  return {
3797
4452
  serverIp,
3798
4453
  targetIp,
@@ -3839,6 +4494,53 @@ const openLab = async (target, kind = "tcp", topology) => {
3839
4494
  ]);
3840
4495
  return read;
3841
4496
  },
4497
+ async namespaceSetup(commands) {
4498
+ for (const command of commands) {
4499
+ await output(admin, [...command]);
4500
+ await output(injector, [...command]);
4501
+ }
4502
+ },
4503
+ async hostSysctl(settings) {
4504
+ if (settings.length === 0) return;
4505
+ if (hostAdmin === void 0) {
4506
+ hostAdmin = await new GenericContainer(image).withNetworkMode("host").withPrivilegedMode().start();
4507
+ opened.push(hostAdmin);
4508
+ }
4509
+ for (const { name, value, guard } of settings) {
4510
+ if (guard !== void 0) {
4511
+ const reads = Number(await output(hostAdmin, [
4512
+ "sysctl",
4513
+ "-n",
4514
+ guard.name
4515
+ ]));
4516
+ if (!(reads <= guard.atMost)) throw new Error(`${guard.name} of the host reads ${reads}, above ${guard.atMost}`);
4517
+ }
4518
+ const before = await output(hostAdmin, [
4519
+ "sysctl",
4520
+ "-n",
4521
+ name
4522
+ ]);
4523
+ hostChanges.push({
4524
+ name,
4525
+ before,
4526
+ set: value,
4527
+ restored: null
4528
+ });
4529
+ await output(hostAdmin, [
4530
+ "sysctl",
4531
+ "-q",
4532
+ "-w",
4533
+ `${name}=${value}`
4534
+ ]);
4535
+ const set = await output(hostAdmin, [
4536
+ "sysctl",
4537
+ "-n",
4538
+ name
4539
+ ]);
4540
+ if (set !== value) throw new Error(`${name} of the host reads ${set} after the write, not ${value}`);
4541
+ }
4542
+ },
4543
+ restoreHost: () => restoreHost(),
3842
4544
  async configure(values) {
3843
4545
  const read = {};
3844
4546
  if (Object.keys(values).length === 0) return read;
@@ -4090,9 +4792,10 @@ const openLab = async (target, kind = "tcp", topology) => {
4090
4792
  query
4091
4793
  ]);
4092
4794
  },
4093
- async probe(args) {
4795
+ async probe(args, files = []) {
4094
4796
  const events = lines();
4095
- const container = await new GenericContainer(clientImage).withNetworkMode(`container:${client.getId()}`).withCommand([...command, ...args]).withLogConsumer(events.consume).start();
4797
+ const prober = new GenericContainer(clientImage).withNetworkMode(`container:${client.getId()}`).withCommand([...command, ...args]).withCopyContentToContainer(files.map((file) => ({ ...file }))).withLogConsumer(events.consume);
4798
+ const container = await (capabilities === void 0 ? prober : prober.withAddedCapabilities(...capabilities)).start();
4096
4799
  return {
4097
4800
  events: events.seen,
4098
4801
  until: (kinds, within) => events.until(kinds, within, "client"),
@@ -4193,6 +4896,7 @@ const openLab = async (target, kind = "tcp", topology) => {
4193
4896
  client: {
4194
4897
  kind: clientKind,
4195
4898
  image: clientImage,
4899
+ ...capabilities === void 0 ? {} : { capabilities },
4196
4900
  ...source === void 0 ? {} : {
4197
4901
  source,
4198
4902
  ...buildOf(source)
@@ -6978,6 +7682,7 @@ const openKafkaLab = async (kafka) => {
6978
7682
  const injectors = /* @__PURE__ */ new Map();
6979
7683
  const parts = {};
6980
7684
  const raw = {};
7685
+ const fences = [];
6981
7686
  const openNode = async (name, environment) => {
6982
7687
  let partial = "";
6983
7688
  const box = await new GenericContainer(kafka.image).withNetwork(started).withNetworkAliases(name).withHostname(name).withEnvironment(environment).withWaitStrategy(Wait.forLogMessage(kafka.ready)).withStartupTimeout(18e4).withLogConsumer((stream) => stream.on("data", (chunk) => {
@@ -6988,6 +7693,7 @@ const openKafkaLab = async (kafka) => {
6988
7693
  const tail = raw[name] ??= [];
6989
7694
  tail.push(text);
6990
7695
  if (tail.length > 400) tail.shift();
7696
+ if (isFenceLine(text)) fences.push(text);
6991
7697
  }
6992
7698
  })).start();
6993
7699
  opened.push(box);
@@ -7059,6 +7765,7 @@ const openKafkaLab = async (kafka) => {
7059
7765
  netns: { client: clientNetns },
7060
7766
  kernel: records.seen,
7061
7767
  raw,
7768
+ fences,
7062
7769
  async sysctl(values) {
7063
7770
  const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
7064
7771
  if (settings.length > 0) await output(admin, [
@@ -7086,11 +7793,46 @@ const openKafkaLab = async (kafka) => {
7086
7793
  }
7087
7794
  throw new Error(`the brokers did not come up in ${within} ms: ${last}`);
7088
7795
  },
7089
- async create(topic) {
7796
+ async create(topic, partitions = 1) {
7090
7797
  const ids = kafka.brokers.map((name) => nodeIdOf(kafka, name)).join(",");
7091
- const reply = await tool("create", topic, String(kafka.minInsync), ids);
7798
+ const reply = await tool("create", topic, String(kafka.minInsync), ids, String(partitions));
7092
7799
  if (!reply.includes("\"ok\":true")) throw new Error(`the create of the topic failed: ${reply}`);
7093
7800
  },
7801
+ async fill(topic, partitions, count) {
7802
+ const reply = await tool("fill", topic, String(partitions), String(count));
7803
+ if (!reply.includes("\"ok\":true")) throw new Error(`the fill of the topic failed: ${reply}`);
7804
+ },
7805
+ async consumer(args) {
7806
+ const events = lines();
7807
+ const container = await new GenericContainer(clientImage).withNetworkMode(`container:${holder.getId()}`).withCommand([
7808
+ "sh",
7809
+ "-c",
7810
+ "java \"$@\"; :",
7811
+ "consumer",
7812
+ CLIENT_HEAP,
7813
+ "-cp",
7814
+ CLASS_PATH,
7815
+ "ConsumerKafka",
7816
+ bootstrap,
7817
+ ...args
7818
+ ]).withLogConsumer(events.consume).start();
7819
+ const signaller = await withPid(new GenericContainer(image).withNetworkMode(`container:${holder.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${container.getId()}`).start();
7820
+ return {
7821
+ events: events.seen,
7822
+ until: (kinds, within) => events.until(kinds, within, "client"),
7823
+ signal: (signal, at) => stamped(signaller, [
7824
+ "/opt/harness/inject",
7825
+ "signal",
7826
+ "java",
7827
+ signal,
7828
+ ...after(at)
7829
+ ]),
7830
+ stop: async () => {
7831
+ await container.stop({ timeout: 2 }).catch(() => void 0);
7832
+ await signaller.stop(STOP).catch(() => void 0);
7833
+ }
7834
+ };
7835
+ },
7094
7836
  async converge(topic, within = CONVERGE_MS) {
7095
7837
  const begin = Date.now();
7096
7838
  let calm = 0;
@@ -7342,6 +8084,105 @@ const kafkaOnce = async (lab, cell, environment, index) => {
7342
8084
  }
7343
8085
  };
7344
8086
  };
8087
+ /** This count is the partitions of the topic of a run of a paused consumer: one for each member. */
8088
+ const PAUSE_PARTITIONS = 2;
8089
+ /** This time bounds the wait for one partition for each member and for two commits, in ms. It has no source: it is far above two batches and one rebalance. */
8090
+ const PAUSE_READY_MS = 9e4;
8091
+ /** This time bounds the wait for the next batch of the first member, in ms. It has no source: it is far above one batch. */
8092
+ const PAUSE_BATCH_MS = 3e4;
8093
+ /**
8094
+ * This function does one run of a paused consumer:
8095
+ * - The topic of the run has two partitions, each with records that a run does not finish.
8096
+ * - Two members join one group, and the run waits until each member holds one partition and the
8097
+ * first member has two commits on its partition.
8098
+ * - A SIGSTOP stops the first member while it holds its next batch, and a SIGCONT ends the stop
8099
+ * after the hold of the cell. The run then waits for the tail.
8100
+ *
8101
+ * The fault is null when the members did not get one partition each. The measure then voids the run.
8102
+ * The law reads no kernel record, thus a lost record does not void this run.
8103
+ */
8104
+ const kafkaPauseOnce = async (lab, cell, environment, index) => {
8105
+ const pause = cell.kafka?.pause;
8106
+ if (pause === void 0) throw new Error("a run of a paused consumer needs `Kafka.pause`");
8107
+ if (cell.fault !== "freeze-process") throw new Error(`a paused consumer has no fault ${cell.fault}`);
8108
+ if (cell.hold === void 0) throw new Error("a paused consumer needs a hold: its law reads it");
8109
+ const hold = valueIn(cell.hold, "s");
8110
+ const topic = topicOf(environment, index);
8111
+ const group = `pause-e${environment}-r${index + 1}`;
8112
+ await lab.brokersUp();
8113
+ await lab.create(topic, PAUSE_PARTITIONS);
8114
+ const leader = await lab.converge(topic);
8115
+ await lab.fill(topic, PAUSE_PARTITIONS, pause.fill);
8116
+ await lab.forget();
8117
+ const from = lab.kernel.length;
8118
+ const lostBefore = droppedSoFar$1(lab.kernel);
8119
+ const common = [
8120
+ "--topic",
8121
+ topic,
8122
+ "--group",
8123
+ group,
8124
+ "--protocol",
8125
+ pause.protocol,
8126
+ "--max-s",
8127
+ String(Math.ceil(hold + 180)),
8128
+ ...Object.entries(cell.driverOptions ?? {}).flatMap(([name, value]) => ["--set", `${name}=${String(value)}`])
8129
+ ];
8130
+ const members = [];
8131
+ let fault = null;
8132
+ let heal = null;
8133
+ let otherEvents = [];
8134
+ let firstEvents = [];
8135
+ try {
8136
+ const first = await lab.consumer([
8137
+ ...common,
8138
+ "--process-ms",
8139
+ String(valueIn(pause.process, "ms")),
8140
+ "--after-pause",
8141
+ pause.afterPause
8142
+ ]);
8143
+ members.push(first);
8144
+ const other = await lab.consumer([
8145
+ ...common,
8146
+ "--process-ms",
8147
+ "0",
8148
+ "--after-pause",
8149
+ "commit"
8150
+ ]);
8151
+ members.push(other);
8152
+ firstEvents = first.events;
8153
+ otherEvents = other.events;
8154
+ const begun = Date.now();
8155
+ while (Date.now() - begun < PAUSE_READY_MS && !pauseReady(first.events, other.events)) await sleep$1(100);
8156
+ if (pauseReady(first.events, other.events)) {
8157
+ const seen = first.events.at(-1)?.t ?? 0;
8158
+ const batch = await waitFor(first, (event) => event.ev === "processing" && event.t > seen, PAUSE_BATCH_MS);
8159
+ if (batch !== void 0) {
8160
+ fault = await first.signal("STOP", batch.t + valueIn(pause.stopAfter, "s") * 1e9);
8161
+ heal = await first.signal("CONT", fault.t0 + hold * 1e9);
8162
+ await sleep$1(KAFKA_TAIL_MS);
8163
+ }
8164
+ }
8165
+ } finally {
8166
+ for (const member of members) await member.stop();
8167
+ }
8168
+ const settled = await lab.settle();
8169
+ return {
8170
+ environment,
8171
+ index,
8172
+ events: [...firstEvents],
8173
+ fault,
8174
+ ...heal === null ? {} : { heal },
8175
+ kernel: lab.kernel.slice(from),
8176
+ netns: lab.netns.client,
8177
+ targetNetns: lab.members[leader].netns,
8178
+ lost: settled === null ? null : settled - lostBefore,
8179
+ pause: {
8180
+ other: [...otherEvents],
8181
+ fences: lab.fences.filter((line) => namesGroup(line, group)),
8182
+ group
8183
+ }
8184
+ };
8185
+ };
7345
8186
  //#endregion
7346
8187
  //#region src/run.ts
7347
8188
  /**
@@ -7433,6 +8274,8 @@ const probeArgs = (cell, lab) => {
7433
8274
  }
7434
8275
  if (probe.keepalive) args.push("--keepalive");
7435
8276
  if (probe.userTimeout !== void 0) args.push("--user-timeout-ms", String(valueIn(probe.userTimeout, "ms")));
8277
+ if (probe.plan !== void 0) args.push("--schedule", SCHEDULE_PATH);
8278
+ if (probe.streams !== void 0) args.push("--plan", LOAD_PATH);
7436
8279
  args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
7437
8280
  return args;
7438
8281
  };
@@ -7615,7 +8458,16 @@ const once = async (lab, cell, environment, index, dry = false) => {
7615
8458
  const middleboxFrom = lab.middlebox.length;
7616
8459
  const serverFrom = lab.server.length;
7617
8460
  const lostBefore = droppedSoFar(lab.kernel);
7618
- const probe = await lab.probe(probeArgs(cell, lab));
8461
+ const plan = cell.probe.plan === void 0 ? void 0 : schedule(cell.probe.plan.profile, cell.probe.plan.options);
8462
+ const load = cell.probe.streams === void 0 ? void 0 : loadPlanOf(cell.probe.streams);
8463
+ const files = [...plan === void 0 ? [] : [{
8464
+ content: JSON.stringify(plan),
8465
+ target: SCHEDULE_PATH
8466
+ }], ...load === void 0 ? [] : [{
8467
+ content: load.content,
8468
+ target: LOAD_PATH
8469
+ }]];
8470
+ const probe = await lab.probe(probeArgs(cell, lab), files.length === 0 ? void 0 : files);
7619
8471
  const server = cell.observer === "server";
7620
8472
  const peer = server ? {
7621
8473
  freeze: () => probe.freeze(),
@@ -7628,7 +8480,11 @@ const once = async (lab, cell, environment, index, dry = false) => {
7628
8480
  const last = probe.events.slice(-3).map((event) => JSON.stringify(event));
7629
8481
  throw new Error(`${error.message}; the client's last lines: ${last.join(" ")}`);
7630
8482
  });
7631
- const placed = lab.topology === null ? {} : { topology: lab.topology };
8483
+ const placed = {
8484
+ ...lab.topology === null ? {} : { topology: lab.topology },
8485
+ ...plan === void 0 ? {} : { schedule: plan },
8486
+ ...load === void 0 ? {} : { streams: load.streams }
8487
+ };
7632
8488
  const fabricOf = () => {
7633
8489
  if (lab.fabric === void 0) throw new Error(`${cell.name} silences a router of a lab with no fabric`);
7634
8490
  return lab.fabric;
@@ -7773,7 +8629,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
7773
8629
  fault = await apply();
7774
8630
  try {
7775
8631
  if (cell.idleBefore !== void 0 && cell.probe.mode !== "idle") await sleep(valueIn(cell.idleBefore, "ms"));
7776
- if ((cell.probe.mode === "inflight" || cell.probe.mode === "stream") && !replyInFlight) await go();
8632
+ if ((cell.probe.mode === "inflight" || cell.probe.mode === "stream" || cell.probe.mode === "schedule") && !replyInFlight) await go();
7777
8633
  const { prediction } = cell;
7778
8634
  const http = cell.probe.http !== void 0;
7779
8635
  if (prediction.detects !== null && prediction.driver !== void 0) {
@@ -7838,7 +8694,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
7838
8694
  await lift();
7839
8695
  if (toxicFirst) await lab.toxic(null);
7840
8696
  }
7841
- if (cell.fault === "freeze-process" && !server && cell.probe.mode !== "idle") await probe.until(["data"], 5e3).catch(() => void 0);
8697
+ if (cell.fault === "freeze-process" && !server && cell.probe.mode !== "idle" && cell.probe.mode !== "schedule") await probe.until(["data"], 5e3).catch(() => void 0);
7842
8698
  const expected = cell.prediction.detects !== false ? cell.prediction.reconnects : void 0;
7843
8699
  if (expected !== void 0) {
7844
8700
  const waits = expected.waits.reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
@@ -7918,9 +8774,11 @@ const inEnvironment = async (cell, environment, count, say) => {
7918
8774
  if (latency !== null) await lab.netem(latency, "target");
7919
8775
  await lab.warm();
7920
8776
  const manifest = await lab.manifest(await lab.sysctl(cell.sysctls), settings, targetSysctls);
8777
+ const missing = await lab.namespaceSetup(cell.namespaceSetup ?? []).then(() => lab.hostSysctl(cell.hostSysctls ?? [])).then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
7921
8778
  const timed = (run) => ({
7922
8779
  ...run,
7923
- jitter: jitterOf(cell, run)
8780
+ jitter: jitterOf(cell, run),
8781
+ ...missing === void 0 ? {} : { missing }
7924
8782
  });
7925
8783
  const warmup = {
7926
8784
  ...timed(await withSteal(() => once(lab, cell, environment, -1, endsEnvironment(cell)))),
@@ -7933,8 +8791,12 @@ const inEnvironment = async (cell, environment, count, say) => {
7933
8791
  runs.push(run);
7934
8792
  say(line(cell, run, measure(cell, run)));
7935
8793
  }
8794
+ const host = await lab.restoreHost();
7936
8795
  return {
7937
- manifest,
8796
+ manifest: host.length === 0 ? manifest : {
8797
+ ...manifest,
8798
+ host
8799
+ },
7938
8800
  runs,
7939
8801
  warmup
7940
8802
  };
@@ -8064,16 +8926,17 @@ const inClusterEnvironment = async (cell, environment, count, say) => {
8064
8926
  */
8065
8927
  const inKafkaEnvironment = async (cell, environment, count, say) => {
8066
8928
  const lab = await openKafkaLab(cell.kafka);
8929
+ const once = cell.kafka.pause === void 0 ? kafkaOnce : kafkaPauseOnce;
8067
8930
  try {
8068
8931
  const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
8069
8932
  const warmup = {
8070
- ...await kafkaOnce(lab, cell, environment, -1),
8933
+ ...await once(lab, cell, environment, -1),
8071
8934
  warmup: true
8072
8935
  };
8073
8936
  say(line(cell, warmup, measure(cell, warmup)));
8074
8937
  const runs = [];
8075
8938
  for (let index = 0; index < count; index += 1) {
8076
- const run = await kafkaOnce(lab, cell, environment, index);
8939
+ const run = await once(lab, cell, environment, index);
8077
8940
  runs.push(run);
8078
8941
  say(line(cell, run, measure(cell, run)));
8079
8942
  }
@@ -8140,6 +9003,6 @@ const runCell = async (cell, say = () => {}) => {
8140
9003
  }
8141
9004
  };
8142
9005
  //#endregion
8143
- export { AWS_FROM, CLIENT_NAMES, CLUSTER_STAGES, CONVERGE_MS, DAEMONS, DEBUG, DIRTY, DROP_HANDLING, FABRIC_CLIENT, FABRIC_DEVICE, FABRIC_TARGET, FRR_IMAGE, GIVES_AWAY, HASH_SCRIPT, INJECT, INSERT, KAFKA_TAIL_MS, KEPT, LINGER_MS, LISTING, LOCK_NAMES, LOSS_MARGIN, MEMBER_MARKS, MONITOR_DOWN, NODE_MARKS, NOISE, PINNED, PUMP, PUMP_SLACK, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, REPOSITORY, RESTART, ROUTERS, SEED_DOCUMENT, SHIPPED_CLIENTS, SHIPPED_NAMES, STAGES, STANDBY_READY, STATUS_SCRIPT, STEPS, STEP_DOWN, amqpClientImage, basesOf, bootstrapOf, brokerEnv, buildOf, buildRecordOf, callOf, cancelsOf, cargoVersions, clockOffsetOf, closeOrExit, cloudOf, clusterArgs, clusterConf, clusterCutOf, clusterHostsOf, clusterOnce, clusterStagesOf, clusterViewOf, commitProblems, comparable, controllerEnv, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, drillOnce, drillSpan, dropFinding, dropOf, endpointAfter, envOf, errorsOf, exitOf, fieldOf, fieldsOf, firstUnanswered, frrConf, goHttpImage, goVersions, groupOnce, groupVerdictOf, grpcClientImage, harnessImage, harnessRecord, harnessVersion, hashOf, highestHeld, hikariClientImage, hzOf, imageOf, imageProblems, inContext, infoOf, initiateScript, isHopPlace, jarVersions, jitterOf, jitterOfLines, judge, kafkaArgs, kafkaClientImage, kafkaCutOf, kafkaOnce, kafkaViewOf, lagsOf, laterMove, latestEndOf, leafLacks, leftAfter, locksOf, logNameOf, manifestProblems, matches, measure, measureDriver, measureGroup, measurePorts, measureQueue, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, monotonicAt, monotonicOf, nodeClientImage, nodeCommand, nodeHttpImage, nodeIdOf, nodeLineOf, nodeNameOf, nodesOnMonotonic, noiseOf, npmVersions, numbersOf, onMonotonic, openClusterLab, openFabric, openGroupLab, openKafkaLab, openLab, openReplicaSetLab, openStandby, pairArgs, pairOnce, pairingOf, pairingsOf, peersOf, pgClientImage, pingsOf, plainOf, planOf, programLines, programOf, quantileOf, readCpu, readHost, readOplog, reconnectFinding, reconnectOf, recordProblems, recoveryFindings, recoveryOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, restamped, routeLine, routerImage, routesOf, runCell, seedScript, sentinelConfig, sessionsOf, shippedClientRecord, shippedSource, sliceQuery, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, standaloneCommand, standbyCommand, statusViewOf, stealShare, stopScript, subjectClient, subjectProblems, subscriptionsOf, substrateHere, substrateKey, substrateOf, substrateOn, targetImageOf, targetSourceOf, templated, toolJsonOf, topicOf, topologyOf, toxicBody, unlockedManifests, unpinnedBases, unpoolable, verdictOf, versionsOf, viewOf, writesOf };
9006
+ export { AWS_FROM, CLIENT_CAPABILITIES, CLIENT_NAMES, CLOSED_TOLERANCE, CLUSTER_STAGES, CONVERGE_MS, DAEMONS, DEBUG, DIRTY, DROP_HANDLING, FABRIC_CLIENT, FABRIC_DEVICE, FABRIC_TARGET, FRR_IMAGE, GIVES_AWAY, HASH_SCRIPT, INJECT, INSERT, KAFKA_TAIL_MS, KEPT, LINGER_MS, LISTING, LOAD_FORMAT, LOAD_PATH, LOCK_NAMES, LOSS_MARGIN, MEMBER_MARKS, MONITOR_DOWN, NODE_MARKS, NOISE, PINNED, PUMP, PUMP_SLACK, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, REPOSITORY, RESTART, ROUTERS, SCHEDULE_PATH, SEED_DOCUMENT, SHIPPED_CLIENTS, SHIPPED_NAMES, STAGES, STANDBY_READY, STATUS_SCRIPT, STEPS, STEP_DOWN, amqpClientImage, basesOf, bootstrapOf, brokerEnv, buildOf, buildRecordOf, callOf, cancelsOf, cargoVersions, clockOffsetOf, closeOrExit, cloudOf, clusterArgs, clusterConf, clusterCutOf, clusterHostsOf, clusterOnce, clusterStagesOf, clusterViewOf, commitProblems, comparable, controllerEnv, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, drillOnce, drillSpan, dropFinding, dropOf, endpointAfter, envOf, errorsOf, estimateOf, estimateVerdict, exitOf, fieldOf, fieldsOf, firstUnanswered, frrConf, goHttpImage, goVersions, groupOnce, groupVerdictOf, grpcClientImage, harnessImage, harnessRecord, harnessVersion, hashOf, highestHeld, hikariClientImage, hzOf, imageOf, imageProblems, inContext, infoOf, initiateScript, isFenceLine, isHopPlace, jarVersions, jitterOf, jitterOfLines, judge, kafkaArgs, kafkaClientImage, kafkaCutOf, kafkaOnce, kafkaPauseOnce, kafkaViewOf, lagsOf, laterMove, latestEndOf, leafLacks, leftAfter, loadOf, loadPlanOf, loadVerdict, locksOf, logNameOf, logTimeOf, manifestProblems, matches, measure, measureConntrack, measureDriver, measureGroup, measurePause, measurePorts, measureQueue, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, monotonicAt, monotonicOf, namesGroup, nodeClientImage, nodeCommand, nodeHttpImage, nodeIdOf, nodeLineOf, nodeNameOf, nodesOnMonotonic, noiseOf, npmVersions, numbersOf, onMonotonic, openClusterLab, openFabric, openGroupLab, openKafkaLab, openLab, openReplicaSetLab, openStandby, pairArgs, pairOnce, pairingOf, pairingsOf, pauseReady, peersOf, pgClientImage, pingsOf, plainOf, planOf, poolOf, programLines, programOf, quantileOf, readCpu, readHost, readOplog, reconnectFinding, reconnectOf, recordProblems, recoveryFindings, recoveryOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, restamped, routeLine, routerImage, routesOf, runCell, seedScript, sentinelConfig, sessionsOf, shippedClientRecord, shippedSource, sliceQuery, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, standaloneCommand, standbyCommand, statusViewOf, stealShare, stopScript, streamsLoadOf, subjectClient, subjectProblems, subscriptionsOf, substrateHere, substrateKey, substrateOf, substrateOn, targetImageOf, targetSourceOf, templated, toolJsonOf, topicOf, topologyOf, toxicBody, unlockedManifests, unpinnedBases, unpoolable, verdictOf, versionsOf, viewOf, writesOf };
8144
9007
 
8145
9008
  //# sourceMappingURL=index.js.map