@mark1russell7/harness 0.14.4 → 0.15.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/build/index.js CHANGED
@@ -1,25 +1,251 @@
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 { 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
+ };
18
92
  /**
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`).
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.
21
97
  */
22
- const latestEndOf = (prediction) => valueIn(prediction.high, "s") + (prediction.startsBy === void 0 ? 0 : valueIn(prediction.startsBy, "s"));
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
+ const measures = each.map(([, measure]) => measure);
103
+ const sum = (count) => measures.reduce((total, measure) => total + count(measure), 0);
104
+ const join = (times) => measures.flatMap(times);
105
+ return {
106
+ pool: {
107
+ planned: sum((measure) => measure.planned),
108
+ unsent: sum((measure) => measure.unsent),
109
+ unfinished: sum((measure) => measure.unfinished),
110
+ failed: sum((measure) => measure.failed ?? 0),
111
+ latency: join((measure) => measure.latency),
112
+ service: join((measure) => measure.service),
113
+ lag: join((measure) => measure.lag),
114
+ censored: join((measure) => measure.censored)
115
+ },
116
+ streams: Object.fromEntries(each)
117
+ };
118
+ };
119
+ /** This function gives the spread of a list of times in ms, or null for no time. */
120
+ const spreadOf$1 = (values) => values.length === 0 ? null : {
121
+ p50: quantity(quantile(values, .5, "nearest-rank"), "ms"),
122
+ p99: quantity(quantile(values, .99, "nearest-rank"), "ms"),
123
+ max: quantity(values.reduce((most, value) => Math.max(most, value), 0), "ms")
124
+ };
125
+ /** 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. */
126
+ const NOW = "2026-01-01T00:00:00.000Z";
127
+ /**
128
+ * This function judges one band of the latency over the pooled arrivals of the counted runs. The
129
+ * top of the band reads the interval of rigor-bench: the band holds only if the interval is under its top.
130
+ * The latencies of one run can follow each other, thus the width of the interval is an estimate.
131
+ * The bottom of the band reads the quantile itself. It gives the text of the verdict, and if it holds.
132
+ */
133
+ const bandVerdict = (band, values, censored) => {
134
+ const high = valueIn(band.high, "ms");
135
+ const rung = {
136
+ name: "band",
137
+ at: {},
138
+ budgets: [{
139
+ kind: "quantile",
140
+ measure: "latency",
141
+ unit: "ms",
142
+ quantile: band.quantile,
143
+ limit: high
144
+ }],
145
+ test: "the cell"
146
+ };
147
+ const top = rungVerdict(rung, {
148
+ rung: "band",
149
+ commit: "the cell",
150
+ at: NOW,
151
+ real: ["the counted runs"],
152
+ synthetic: [],
153
+ evidence: [{
154
+ kind: "quantile",
155
+ measure: "latency",
156
+ values,
157
+ censored
158
+ }]
159
+ }, { now: NOW });
160
+ const read = values.length + censored.length === 0 ? null : quantile([...values, ...censored], band.quantile, "nearest-rank");
161
+ const low = band.low === void 0 ? null : valueIn(band.low, "ms");
162
+ const above = low === null || read !== null && read >= low;
163
+ const place = low === null ? `under ${high} ms` : `in [${low}, ${high}] ms`;
164
+ const outcome = top.outcome === "within" && !above ? "under the bottom of the band" : top.outcome;
165
+ return {
166
+ holds: top.outcome === "within" && above,
167
+ words: `p${band.quantile * 100} of the latency ${read === null ? "has no value" : `is ${read} ms`}, wanted ${place}: ${outcome}. ${top.reason}`
168
+ };
169
+ };
170
+ /**
171
+ * This function judges the share of failed arrivals over the counted runs against a limit. Each
172
+ * planned arrival is a trial. An arrival fails if the client did not send it. It also fails with no
173
+ * reply at the end of its run, or with the status of a failure. The rate budget of rigor-bench
174
+ * reads the upper end of the Wilson interval at 0.95. Thus 381 arrivals with no failure hold a limit
175
+ * of 1 %, and fewer arrivals cannot decide.
176
+ */
177
+ const failuresVerdict = (limit, failures, trials) => {
178
+ const verdict = rungVerdict({
179
+ name: "failures",
180
+ at: {},
181
+ budgets: [{
182
+ kind: "rate",
183
+ measure: "failures",
184
+ limit
185
+ }],
186
+ test: "the cell"
187
+ }, {
188
+ rung: "failures",
189
+ commit: "the cell",
190
+ at: NOW,
191
+ real: ["the counted runs"],
192
+ synthetic: [],
193
+ evidence: [{
194
+ kind: "rate",
195
+ measure: "failures",
196
+ failures,
197
+ trials
198
+ }]
199
+ }, { now: NOW });
200
+ return {
201
+ holds: verdict.outcome === "within",
202
+ words: `${failures} of ${trials} arrivals failed, wanted a share under ${limit * 100} %: ${verdict.outcome}. ${verdict.reason}`
203
+ };
204
+ };
205
+ /**
206
+ * This function judges the load of an open-loop cell over its counted runs. A run that did not keep
207
+ * its schedule measures its driver, not the target, thus it is a finding of that run. Each
208
+ * band reads all the arrivals of the counted runs.
209
+ */
210
+ const loadVerdict = (prediction, runs) => {
211
+ const limit = valueIn(prediction.lag, "ms");
212
+ const outside = runs.flatMap((run) => {
213
+ if (run.load === void 0) return [{
214
+ run: run.name,
215
+ why: "the client never started its schedule"
216
+ }];
217
+ const check = scheduleHeld(run.load, limit);
218
+ return check.held ? [] : [{
219
+ run: run.name,
220
+ why: check.reason
221
+ }];
222
+ });
223
+ const measures = runs.flatMap((run) => run.load === void 0 ? [] : [run.load]);
224
+ const latency = measures.flatMap((measure) => measure.latency);
225
+ const censored = measures.flatMap((measure) => measure.censored);
226
+ const bands = prediction.bands.map((band) => bandVerdict(band, latency, censored));
227
+ const total = (count) => measures.reduce((sum, measure) => sum + count(measure), 0);
228
+ const planned = total((measure) => measure.planned);
229
+ const unsent = total((measure) => measure.unsent);
230
+ const unfinished = total((measure) => measure.unfinished);
231
+ const failed = total((measure) => measure.failed ?? 0);
232
+ const failures = prediction.failures === void 0 ? void 0 : failuresVerdict(prediction.failures, unsent + unfinished + failed, planned);
233
+ return {
234
+ holds: runs.length > 0 && outside.length === 0 && bands.every((band) => band.holds) && (failures?.holds ?? true),
235
+ outside,
236
+ load: {
237
+ planned,
238
+ unsent,
239
+ unfinished,
240
+ failed,
241
+ latency: spreadOf$1([...latency, ...censored]),
242
+ service: spreadOf$1(measures.flatMap((measure) => measure.service)),
243
+ lag: spreadOf$1(measures.flatMap((measure) => measure.lag.map((lag) => Math.abs(lag)))),
244
+ bands: bands.map((band) => band.words),
245
+ ...failures === void 0 ? {} : { failures: failures.words }
246
+ }
247
+ };
248
+ };
23
249
  //#endregion
24
250
  //#region src/verdict.ts
25
251
  /** This function gives the words of the outcome of a second request: its connection, a retry, and its end. */
@@ -345,14 +571,15 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
345
571
  why: "no run counted"
346
572
  };
347
573
  if (prediction.detects === null) {
348
- if (prediction.queue !== void 0 || prediction.ports !== void 0) {
574
+ 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;
575
+ if (law !== void 0) {
349
576
  const off = new Set(outside.map((each) => each.run)).size;
350
577
  return {
351
578
  ...empty,
352
579
  decision: off === 0 ? "holds" : "fails",
353
580
  band: null,
354
581
  outside,
355
- why: `${counted.length - off} of ${counted.length} runs hold the law of the ${prediction.queue === void 0 ? "port range" : "queue"}; ${prediction.why}`
582
+ why: `${counted.length - off} of ${counted.length} runs hold the law of the ${law}; ${prediction.why}`
356
583
  };
357
584
  }
358
585
  const crossed = counted.flatMap((run) => run.crossings === void 0 ? [] : [run.crossings]);
@@ -420,6 +647,33 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
420
647
  why: `${counted.length} runs measured: ${outcomes}; ${timed}.${late} ${prediction.why}`
421
648
  };
422
649
  }
650
+ if (counted.some((run) => run.load !== void 0)) {
651
+ const judged = loadVerdict(prediction.load ?? {
652
+ lag: quantity(Number.MAX_SAFE_INTEGER, "ms"),
653
+ bands: []
654
+ }, counted.map((run) => ({
655
+ name: name(run),
656
+ load: run.load
657
+ })));
658
+ const middleOfLoad = judged.load.latency?.p50;
659
+ const failed = judged.load.failed === 0 ? "" : `, ${judged.load.failed} with the status of a failure`;
660
+ const read = [
661
+ `${counted.length} runs on a schedule, ${judged.load.planned} arrivals, ${judged.load.unsent} not sent, ${judged.load.unfinished} with no reply${failed}.`,
662
+ ...judged.load.bands,
663
+ ...judged.load.failures === void 0 ? [] : [judged.load.failures],
664
+ prediction.why
665
+ ].join(" ");
666
+ return {
667
+ ...empty,
668
+ decision: prediction.load === void 0 ? "measured" : judged.holds ? "holds" : "fails",
669
+ band: null,
670
+ outside: judged.outside,
671
+ median: middleOfLoad === void 0 ? null : s(valueIn(middleOfLoad, "s")),
672
+ ...judged.load.latency === null ? {} : { slowest: s(valueIn(judged.load.latency.max, "s")) },
673
+ load: judged.load,
674
+ why: read
675
+ };
676
+ }
423
677
  const samples = counted.flatMap((run) => (run.samples ?? []).map((sample) => valueIn(sample, "s"))).sort((a, b) => a - b);
424
678
  const retransmissions = counted.reduce((sum, run) => sum + run.retransmissions, 0);
425
679
  const ended = counted.filter((run) => run.outcome.kind === "error" || run.outcome.kind === "eof");
@@ -1351,6 +1605,285 @@ const measureQueue = (run, expected) => {
1351
1605
  };
1352
1606
  };
1353
1607
  //#endregion
1608
+ //#region src/conntrack.ts
1609
+ /** This errno tells that a connect got a slot. */
1610
+ const NONE = "none";
1611
+ const numberOr = (line, field) => {
1612
+ const value = line?.[field];
1613
+ return typeof value === "number" ? value : null;
1614
+ };
1615
+ /** This function gives the time from one stamp to a stamp of a field of a line, when the line has it. */
1616
+ const untilField = (from, line, field) => {
1617
+ const stamp = numberOr(line, field);
1618
+ return from === void 0 || stamp === null ? null : between$2(from.t, stamp);
1619
+ };
1620
+ const stallOf = (line) => line === void 0 ? null : {
1621
+ errno: String(line["errno"]),
1622
+ time: quantity(Math.round((numberOr(line, "elapsed") ?? 0) / 1e3) / 1e6, "s"),
1623
+ drops: numberOr(line, "drops") ?? 0,
1624
+ retransFail: numberOr(line, "retrans_fail") ?? 0
1625
+ };
1626
+ /** This function tells where a reading differs from the law of its cell. */
1627
+ const findingsOf$1 = (read, failed, expected) => {
1628
+ const found = [];
1629
+ const { max, neighbor, refused, drained, pressure } = expected;
1630
+ if (typeof failed === "string") found.push(`a connect failed with ${failed} before the client held its flows`);
1631
+ if (read.flows !== max) found.push(`the client held ${read.flows} flows, not ${max}`);
1632
+ if (read.count !== max) found.push(`the count read ${read.count} at the fill, not ${max}`);
1633
+ if (neighbor !== void 0 && read.neighbor !== neighbor) found.push(`the namespace of the target read ${read.neighbor ?? "nothing"}, not ${neighbor}`);
1634
+ if (refused !== void 0) {
1635
+ const stall = read.stall;
1636
+ if (stall === null) found.push("no connect after the fill ended");
1637
+ else {
1638
+ if (stall.errno !== refused.errno) found.push(`the connect after the fill ended with ${stall.errno}, not ${refused.errno}`);
1639
+ if (!inside(stall.time, refused.within)) found.push(`the connect after the fill took ${valueIn(stall.time, "s")} s, outside ${bandWords(refused.within)}`);
1640
+ if (stall.drops !== refused.drops) found.push(`the field drop rose by ${stall.drops}, not ${refused.drops}`);
1641
+ if (stall.retransFail !== refused.retransFail) found.push(`TCPRetransFail rose by ${stall.retransFail}, not ${refused.retransFail}`);
1642
+ }
1643
+ }
1644
+ if (drained !== void 0) {
1645
+ 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)}`);
1646
+ }
1647
+ if (pressure !== void 0) {
1648
+ const stall = read.stall;
1649
+ if (read.closed !== max) found.push(`the count read ${read.closed ?? "nothing"} after each side closed, not ${max}`);
1650
+ const { highest } = pressure;
1651
+ if (highest === void 0) {
1652
+ if (read.lowest !== pressure.lowest) found.push(`the lowest count was ${read.lowest ?? "nothing"}, not ${pressure.lowest}`);
1653
+ if (read.after !== pressure.after) found.push(`the count read ${read.after ?? "nothing"} after the connect, not ${pressure.after}`);
1654
+ } 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}`);
1655
+ else if (read.after !== read.lowest + 1) found.push(`the count read ${read.after ?? "nothing"} after the connect, not ${read.lowest + 1}`);
1656
+ if (stall === null) found.push("no connect after the close ended");
1657
+ else {
1658
+ if (stall.errno !== NONE) found.push(`the connect after the close ended with ${stall.errno}`);
1659
+ 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`);
1660
+ }
1661
+ }
1662
+ return found;
1663
+ };
1664
+ /**
1665
+ * This function reads one run of a conntrack cell from the lines of its client and its target. An
1666
+ * `unsettled` line makes the run void. The entries of the run before it were still in the table, thus
1667
+ * the run did not start from an empty table.
1668
+ */
1669
+ const measureConntrack = (run, expected) => {
1670
+ const { events, kernel } = run;
1671
+ const retransmissions = kernel.filter((record) => record.netns === run.netns && record.ev === "retrans").length;
1672
+ const base = {
1673
+ environment: run.environment,
1674
+ index: run.index,
1675
+ outcome: { kind: "answered" },
1676
+ elapsed: null,
1677
+ socket: null,
1678
+ noticed: null,
1679
+ steps: [],
1680
+ probes: 0,
1681
+ retransmissions,
1682
+ acks: 0
1683
+ };
1684
+ const voided = (why) => ({
1685
+ ...base,
1686
+ void: why,
1687
+ finding: null
1688
+ });
1689
+ const unsettled = events.find((event) => event.ev === "unsettled");
1690
+ if (unsettled !== void 0) return voided(`the table held ${String(unsettled["count"])} entries at the end of the wait before the run`);
1691
+ if (events.find((event) => event.ev === "done") === void 0) {
1692
+ const error = events.find((event) => event.ev === "error");
1693
+ return voided(error === void 0 ? "the client did not reach its end" : `the client did not reach its end: ${String(error["what"])}`);
1694
+ }
1695
+ if (run.lost === null) return voided("the prober wrote no clock line after the run, so nothing says what it dropped");
1696
+ if (run.lost > 0) return voided(`the prober dropped ${run.lost} records`);
1697
+ const full = events.find((event) => event.ev === "full");
1698
+ if (full === void 0) return voided("the client wrote no line for its fill");
1699
+ const stall = events.find((event) => event.ev === "stall");
1700
+ const closing = events.find((event) => event.ev === "closing");
1701
+ const drained = events.find((event) => event.ev === "drained");
1702
+ const closed = events.find((event) => event.ev === "closed");
1703
+ const neighbor = (run.server ?? []).filter((line) => line.ev === "count" && line.t >= full.t && (stall === void 0 || line.t <= stall.t)).at(-1);
1704
+ const conntrack = {
1705
+ flows: numberOr(full, "flows") ?? 0,
1706
+ count: numberOr(full, "count") ?? 0,
1707
+ neighbor: numberOr(neighbor, "count"),
1708
+ stall: stallOf(stall),
1709
+ firstFall: untilField(closing, drained, "first_fall"),
1710
+ zero: untilField(closing, drained, "zero"),
1711
+ closed: numberOr(closed, "count"),
1712
+ lowest: numberOr(drained ?? stall, "lowest"),
1713
+ after: numberOr(stall, "after")
1714
+ };
1715
+ const found = findingsOf$1(conntrack, full["errno"], expected);
1716
+ return {
1717
+ ...base,
1718
+ void: null,
1719
+ finding: found.length === 0 ? null : found.join(". "),
1720
+ conntrack
1721
+ };
1722
+ };
1723
+ //#endregion
1724
+ //#region src/pause.ts
1725
+ /** This expression finds the stamp of a log line of a broker, as its console writes it, on UTC. */
1726
+ const LOG_TIME = /\[(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d),(\d{3})\]/;
1727
+ /** 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. */
1728
+ const logTimeOf = (line, start) => {
1729
+ const found = LOG_TIME.exec(line);
1730
+ if (found === null) return null;
1731
+ 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]));
1732
+ return start.t + (wall - Number(start["wall"])) * 1e6;
1733
+ };
1734
+ /** This function gives the time between two instants of the monotonic clock, in s, to 1 microsecond. */
1735
+ const secondsBetween = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
1736
+ const isInside = (time, band) => {
1737
+ const seconds = valueIn(time, "s");
1738
+ return seconds >= valueIn(band.low, "s") && seconds <= valueIn(band.high, "s");
1739
+ };
1740
+ const bandText = (band) => `[${valueIn(band.low, "s")}, ${valueIn(band.high, "s")}] s`;
1741
+ const isChange = (event) => event.ev === "assigned" || event.ev === "revoked" || event.ev === "lost";
1742
+ /** This function gives the partitions that a line of the rebalance listener names, in its field `partitions` or `held`. */
1743
+ const partitionsOf = (event, field) => event[field] ?? [];
1744
+ /** This function gives the last line of the rebalance listener in a list of lines. */
1745
+ const lastChange = (events) => events.findLast(isChange);
1746
+ /** This function gives the offset that a commit names for a partition, or null. */
1747
+ const offsetOf = (event, partition) => {
1748
+ const value = event["offsets"]?.[String(partition)];
1749
+ return typeof value === "number" ? value : null;
1750
+ };
1751
+ /**
1752
+ * This function tells if the two members can take the fault. Each member holds one partition, the
1753
+ * two partitions differ, and the first member has two acknowledged commits on its partition after its
1754
+ * last change. Thus the first member reads and commits at a steady rate.
1755
+ */
1756
+ const pauseReady = (first, other) => {
1757
+ const mine = lastChange(first);
1758
+ const theirs = lastChange(other);
1759
+ if (mine === void 0 || theirs === void 0) return false;
1760
+ const held = partitionsOf(mine, "held");
1761
+ const others = partitionsOf(theirs, "held");
1762
+ if (held.length !== 1 || others.length !== 1 || held[0] === others[0]) return false;
1763
+ return first.filter((event) => event.ev === "commit" && event.t > mine.t && offsetOf(event, held[0]) !== null).length >= 2;
1764
+ };
1765
+ /** This function tells if a log line of a broker tells that a member left a group, under either protocol. */
1766
+ const isFenceLine = (line) => /Member \S+ fenced from the group because|Member \S+ in group \S+ has failed, removing it from the group/.test(line);
1767
+ /** This function tells if a fence line names a group. The two protocols write the name in different words. */
1768
+ const namesGroup = (line, group) => line.includes(`[GroupId ${group}]`) || line.includes(`in group ${group} has`);
1769
+ /** This function compares a reading to the law, and gives each disagreement. */
1770
+ const findingsOf = (reading, prediction, fences, otherReports) => {
1771
+ const found = [];
1772
+ const { commit, fence, takeover } = reading;
1773
+ if (prediction.fence === null) {
1774
+ if (fences.length > 0) found.push(`the group removed a member: ${fences[0]}`);
1775
+ if (otherReports) found.push("the other member reported the partition");
1776
+ } else {
1777
+ if (fence === null) found.push("no log line tells of a fence, and the other member did not report the partition");
1778
+ else if (!isInside(fence, prediction.fence)) found.push(`the fence is ${valueIn(fence, "s")} s, outside ${bandText(prediction.fence)}`);
1779
+ if (reading.otherFirst !== reading.lastAck) found.push(`the other member started at offset ${reading.otherFirst}, and the last commit is ${reading.lastAck}`);
1780
+ }
1781
+ if (prediction.takeover !== null) {
1782
+ if (takeover === null) found.push("the other member did not report the partition");
1783
+ else if (!isInside(takeover, prediction.takeover)) found.push(`the other member reported the partition at ${valueIn(takeover, "s")} s, outside ${bandText(prediction.takeover)}`);
1784
+ }
1785
+ if (commit === null) {
1786
+ found.push("the member made no commit after the stop");
1787
+ return found;
1788
+ }
1789
+ switch (prediction.thaw) {
1790
+ case "fails":
1791
+ if (commit.name !== "CommitFailedException") found.push(`the commit after the stop gave ${commit.name ?? "an acknowledgment"}, not CommitFailedException`);
1792
+ break;
1793
+ case "acknowledged":
1794
+ if (!commit.acked) found.push(`the commit after the stop failed with ${commit.name}`);
1795
+ if (commit.epoch !== reading.epochBefore) found.push(`the epoch changed from ${reading.epochBefore} to ${commit.epoch}`);
1796
+ break;
1797
+ case "rejoin": {
1798
+ const within = prediction.rejoinWithin;
1799
+ if (reading.rejoin === null) found.push("the member did not get a new epoch");
1800
+ 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`);
1801
+ if (!commit.acked) found.push(`the stale commit failed with ${commit.name}`);
1802
+ if (reading.reassigned) found.push("the member held the partition again before its commit");
1803
+ if (reading.committed !== reading.stale) found.push(`the committed offset is ${reading.committed}, and the stale commit named ${reading.stale}`);
1804
+ 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`);
1805
+ break;
1806
+ }
1807
+ }
1808
+ return found;
1809
+ };
1810
+ /** This function reads one run of a paused consumer against its law. */
1811
+ const measurePause = (run, prediction) => {
1812
+ const retransmissions = run.kernel.filter((record) => record.netns === run.netns && record.ev === "retrans").length;
1813
+ const base = {
1814
+ environment: run.environment,
1815
+ index: run.index,
1816
+ outcome: { kind: "answered" },
1817
+ elapsed: null,
1818
+ socket: null,
1819
+ noticed: null,
1820
+ steps: [],
1821
+ probes: 0,
1822
+ retransmissions,
1823
+ acks: 0
1824
+ };
1825
+ const voided = (why) => ({
1826
+ ...base,
1827
+ void: why,
1828
+ finding: null
1829
+ });
1830
+ const { fault, heal, pause } = run;
1831
+ if (fault === null || pause === void 0) return voided("the partitions were not one for each member before the stop");
1832
+ if (heal === void 0 || heal === null) return voided("the stop did not end");
1833
+ const stop = fault.t0;
1834
+ const thaw = heal.t0;
1835
+ const before = run.events.filter((event) => event.t < stop);
1836
+ const after = run.events.filter((event) => event.t >= stop);
1837
+ const start = run.events.find((event) => event.ev === "start");
1838
+ if (start === void 0) return voided("the member wrote no start line");
1839
+ const mine = lastChange(before);
1840
+ const theirs = lastChange(pause.other.filter((event) => event.t < stop));
1841
+ const held = mine === void 0 ? [] : partitionsOf(mine, "held");
1842
+ const others = theirs === void 0 ? [] : partitionsOf(theirs, "held");
1843
+ const partition = held[0];
1844
+ 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");
1845
+ if (after.find((event) => event.ev === "pause")?.["in"] !== "process") return voided("the stop landed outside the processing of a batch");
1846
+ if (before.findLast((event) => event.ev === "read")?.["partition"] !== partition) return voided("the last read before the stop was on another partition");
1847
+ const lastCommit = before.findLast((event) => event.ev === "commit" && offsetOf(event, partition) !== null);
1848
+ if (lastCommit === void 0) return voided("the member had no acknowledged commit on its partition before the stop");
1849
+ const member = String(mine?.["member"]);
1850
+ const fenceAt = pause.fences.filter((line) => line.includes(member)).map((line) => logTimeOf(line, start)).find((time) => time !== null && time >= stop) ?? null;
1851
+ const taken = pause.other.find((event) => event.ev === "assigned" && event.t >= stop && partitionsOf(event, "partitions").includes(partition));
1852
+ if (prediction.fence !== null && fenceAt === null && taken !== void 0) return voided("the broker log lost the line of the fence");
1853
+ const thawed = after.find((event) => event.ev === "commit" || event.ev === "commit-fail");
1854
+ const rejoined = after.find((event) => event.ev === "rejoined");
1855
+ const ackAt = thawed?.t ?? Number.POSITIVE_INFINITY;
1856
+ const otherCommit = pause.other.findLast((event) => event.ev === "commit" && event.t < ackAt && offsetOf(event, partition) !== null);
1857
+ const committed = after.find((event) => event.ev === "committed");
1858
+ const epochOf = (event) => typeof event?.["epoch"] === "number" ? event["epoch"] : null;
1859
+ const pauseReading = {
1860
+ partition,
1861
+ fence: fenceAt === null ? null : secondsBetween(stop, fenceAt),
1862
+ takeover: taken === void 0 ? null : secondsBetween(stop, taken.t),
1863
+ commit: thawed === void 0 ? null : {
1864
+ acked: thawed.ev === "commit",
1865
+ name: typeof thawed["name"] === "string" ? thawed["name"] : null,
1866
+ message: typeof thawed["message"] === "string" ? thawed["message"] : null,
1867
+ epoch: epochOf(thawed)
1868
+ },
1869
+ epochBefore: epochOf(lastCommit),
1870
+ lastAck: offsetOf(lastCommit, partition),
1871
+ otherFirst: pause.other.find((event) => event.ev === "read" && event.t >= stop && event["partition"] === partition)?.["first"] ?? null,
1872
+ otherLast: otherCommit === void 0 ? null : offsetOf(otherCommit, partition),
1873
+ rejoin: rejoined === void 0 ? null : secondsBetween(thaw, rejoined.t),
1874
+ reassigned: after.some((event) => event.ev === "assigned" && event.t >= thaw && event.t <= ackAt && partitionsOf(event, "partitions").includes(partition)),
1875
+ committed: committed === void 0 ? null : offsetOf(committed, partition),
1876
+ stale: thawed === void 0 ? null : offsetOf(thawed, partition)
1877
+ };
1878
+ const findings = findingsOf(pauseReading, prediction, pause.fences, taken !== void 0);
1879
+ return {
1880
+ ...base,
1881
+ void: null,
1882
+ finding: findings.length === 0 ? null : findings.join(". "),
1883
+ pause: pauseReading
1884
+ };
1885
+ };
1886
+ //#endregion
1354
1887
  //#region src/ports.ts
1355
1888
  /** This errno ends a search that found no port. */
1356
1889
  const NO_PORT = "EADDRNOTAVAIL";
@@ -2312,6 +2845,21 @@ const requestOf = (events) => {
2312
2845
  };
2313
2846
  /** This function reads the socket of one run against its cell, as `measure` does without the second request of an HTTP client. */
2314
2847
  const measureSocket = (cell, run) => {
2848
+ if (run.missing !== void 0) return {
2849
+ environment: run.environment,
2850
+ index: run.index,
2851
+ outcome: { kind: "answered" },
2852
+ elapsed: null,
2853
+ socket: null,
2854
+ noticed: null,
2855
+ steps: [],
2856
+ probes: 0,
2857
+ retransmissions: 0,
2858
+ acks: 0,
2859
+ void: `the environment cannot do this cell: ${run.missing}`,
2860
+ finding: null
2861
+ };
2862
+ if (cell.prediction.detects === null && cell.prediction.pause !== void 0) return measurePause(run, cell.prediction.pause);
2315
2863
  if (cell.group !== void 0 || cell.replicaSet !== void 0 || cell.cluster !== void 0 || cell.pair !== void 0 || cell.kafka !== void 0) {
2316
2864
  const read = measureGroup(cell, run);
2317
2865
  return {
@@ -2332,6 +2880,7 @@ const measureSocket = (cell, run) => {
2332
2880
  }
2333
2881
  if (cell.prediction.detects === null && cell.prediction.queue !== void 0) return measureQueue(run, cell.prediction.queue);
2334
2882
  if (cell.prediction.detects === null && cell.prediction.ports !== void 0) return measurePorts(run, cell.prediction.ports);
2883
+ if (cell.prediction.detects === null && cell.prediction.conntrack !== void 0) return measureConntrack(run, cell.prediction.conntrack);
2335
2884
  if (cell.prediction.detects !== null && cell.prediction.driver !== void 0) return measureDriver(cell, run);
2336
2885
  const { events, kernel } = run;
2337
2886
  const ready = events.find((event) => event.ev === "ready");
@@ -2462,7 +3011,13 @@ const measureSocket = (cell, run) => {
2462
3011
  forgot: forgotten === void 0 ? null : between(lastRx, forgotten.t)
2463
3012
  };
2464
3013
  if (prediction.detects === null) {
2465
- const sends = events.filter((event) => event.ev === "sent").slice(1);
3014
+ const load = run.streams !== void 0 ? streamsLoadOf(events, run.streams)?.pool ?? null : run.schedule === void 0 ? void 0 : loadOf(events, run.schedule);
3015
+ if (load === null) return {
3016
+ ...base,
3017
+ ...nothing,
3018
+ void: "the client never started its schedule"
3019
+ };
3020
+ const sends = load === void 0 ? events.filter((event) => event.ev === "sent").slice(1) : [];
2466
3021
  const answers = events.some((event) => event.ev === "replied") ? "replied" : "data";
2467
3022
  const samples = sends.flatMap((send, n) => {
2468
3023
  const next = sends[n + 1]?.t ?? Infinity;
@@ -2500,6 +3055,7 @@ const measureSocket = (cell, run) => {
2500
3055
  noticed: prediction.from === void 0 ? null : noticed,
2501
3056
  steps: retransmitted.filter((record) => record.mono > request.t).map((record) => between(request.t, record.mono)),
2502
3057
  samples,
3058
+ ...load === void 0 ? {} : { load },
2503
3059
  reordering: reorderingOf(socket.filter((record) => record.mono > request.t), peerRecords.filter((record) => record.mono > request.t)),
2504
3060
  ...crossings,
2505
3061
  void: voided
@@ -3446,6 +4002,7 @@ const clientOf = async (kind) => {
3446
4002
  clientImage: await imageOf(client.image),
3447
4003
  command: client.command,
3448
4004
  clientKind: typeof kind === "string" ? kind : "image",
4005
+ ...client.capabilities === void 0 || client.capabilities.length === 0 ? {} : { capabilities: client.capabilities },
3449
4006
  ...typeof client.image === "string" ? {} : { source: client.image }
3450
4007
  };
3451
4008
  };
@@ -3477,18 +4034,21 @@ const deviceOf = (route) => {
3477
4034
  */
3478
4035
  const openLab = async (target, kind = "tcp", topology) => {
3479
4036
  const image = await harnessImage();
3480
- const { clientImage, command, clientKind, source } = await clientOf(kind);
4037
+ const { clientImage, command, clientKind, capabilities, source } = await clientOf(kind);
3481
4038
  const hops = topology?.hops ?? [];
3482
4039
  const { GenericContainer, Network, Wait, getContainerRuntimeClient } = await containers();
3483
4040
  const runtime = await getContainerRuntimeClient();
3484
4041
  const opened = [];
3485
4042
  const networks = [];
3486
4043
  const serverLines = lines();
4044
+ let restoreHost = () => Promise.resolve([]);
3487
4045
  let fabric;
3488
4046
  const close = async () => {
4047
+ const restored = await restoreHost().then(() => null, (error) => error);
3489
4048
  await fabric?.close();
3490
4049
  for (const container of opened.reverse()) await container.stop(STOP).catch(() => void 0);
3491
4050
  for (const network of networks.reverse()) await network.stop().catch(() => void 0);
4051
+ if (restored !== null) throw restored;
3492
4052
  };
3493
4053
  /** 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
4054
  const ipOn = async (container, network) => {
@@ -3793,6 +4353,33 @@ const openLab = async (target, kind = "tcp", topology) => {
3793
4353
  };
3794
4354
  };
3795
4355
  const proxies = inChain.filter((hop) => hop.hop.kind === "proxy");
4356
+ let hostAdmin;
4357
+ const hostChanges = [];
4358
+ let hostDone;
4359
+ restoreHost = async () => {
4360
+ if (hostDone !== void 0) return hostDone;
4361
+ const restored = [];
4362
+ for (const change of [...hostChanges].reverse()) {
4363
+ await output(hostAdmin, [
4364
+ "sysctl",
4365
+ "-q",
4366
+ "-w",
4367
+ `${change.name}=${change.before}`
4368
+ ]);
4369
+ const read = await output(hostAdmin, [
4370
+ "sysctl",
4371
+ "-n",
4372
+ change.name
4373
+ ]);
4374
+ if (read !== change.before) throw new Error(`${change.name} reads ${read} after the restore, not ${change.before}`);
4375
+ restored.unshift({
4376
+ ...change,
4377
+ restored: read
4378
+ });
4379
+ }
4380
+ hostDone = restored;
4381
+ return restored;
4382
+ };
3796
4383
  return {
3797
4384
  serverIp,
3798
4385
  targetIp,
@@ -3839,6 +4426,53 @@ const openLab = async (target, kind = "tcp", topology) => {
3839
4426
  ]);
3840
4427
  return read;
3841
4428
  },
4429
+ async namespaceSetup(commands) {
4430
+ for (const command of commands) {
4431
+ await output(admin, [...command]);
4432
+ await output(injector, [...command]);
4433
+ }
4434
+ },
4435
+ async hostSysctl(settings) {
4436
+ if (settings.length === 0) return;
4437
+ if (hostAdmin === void 0) {
4438
+ hostAdmin = await new GenericContainer(image).withNetworkMode("host").withPrivilegedMode().start();
4439
+ opened.push(hostAdmin);
4440
+ }
4441
+ for (const { name, value, guard } of settings) {
4442
+ if (guard !== void 0) {
4443
+ const reads = Number(await output(hostAdmin, [
4444
+ "sysctl",
4445
+ "-n",
4446
+ guard.name
4447
+ ]));
4448
+ if (!(reads <= guard.atMost)) throw new Error(`${guard.name} of the host reads ${reads}, above ${guard.atMost}`);
4449
+ }
4450
+ const before = await output(hostAdmin, [
4451
+ "sysctl",
4452
+ "-n",
4453
+ name
4454
+ ]);
4455
+ hostChanges.push({
4456
+ name,
4457
+ before,
4458
+ set: value,
4459
+ restored: null
4460
+ });
4461
+ await output(hostAdmin, [
4462
+ "sysctl",
4463
+ "-q",
4464
+ "-w",
4465
+ `${name}=${value}`
4466
+ ]);
4467
+ const set = await output(hostAdmin, [
4468
+ "sysctl",
4469
+ "-n",
4470
+ name
4471
+ ]);
4472
+ if (set !== value) throw new Error(`${name} of the host reads ${set} after the write, not ${value}`);
4473
+ }
4474
+ },
4475
+ restoreHost: () => restoreHost(),
3842
4476
  async configure(values) {
3843
4477
  const read = {};
3844
4478
  if (Object.keys(values).length === 0) return read;
@@ -4090,9 +4724,10 @@ const openLab = async (target, kind = "tcp", topology) => {
4090
4724
  query
4091
4725
  ]);
4092
4726
  },
4093
- async probe(args) {
4727
+ async probe(args, files = []) {
4094
4728
  const events = lines();
4095
- const container = await new GenericContainer(clientImage).withNetworkMode(`container:${client.getId()}`).withCommand([...command, ...args]).withLogConsumer(events.consume).start();
4729
+ const prober = new GenericContainer(clientImage).withNetworkMode(`container:${client.getId()}`).withCommand([...command, ...args]).withCopyContentToContainer(files.map((file) => ({ ...file }))).withLogConsumer(events.consume);
4730
+ const container = await (capabilities === void 0 ? prober : prober.withAddedCapabilities(...capabilities)).start();
4096
4731
  return {
4097
4732
  events: events.seen,
4098
4733
  until: (kinds, within) => events.until(kinds, within, "client"),
@@ -4193,6 +4828,7 @@ const openLab = async (target, kind = "tcp", topology) => {
4193
4828
  client: {
4194
4829
  kind: clientKind,
4195
4830
  image: clientImage,
4831
+ ...capabilities === void 0 ? {} : { capabilities },
4196
4832
  ...source === void 0 ? {} : {
4197
4833
  source,
4198
4834
  ...buildOf(source)
@@ -6978,6 +7614,7 @@ const openKafkaLab = async (kafka) => {
6978
7614
  const injectors = /* @__PURE__ */ new Map();
6979
7615
  const parts = {};
6980
7616
  const raw = {};
7617
+ const fences = [];
6981
7618
  const openNode = async (name, environment) => {
6982
7619
  let partial = "";
6983
7620
  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 +7625,7 @@ const openKafkaLab = async (kafka) => {
6988
7625
  const tail = raw[name] ??= [];
6989
7626
  tail.push(text);
6990
7627
  if (tail.length > 400) tail.shift();
7628
+ if (isFenceLine(text)) fences.push(text);
6991
7629
  }
6992
7630
  })).start();
6993
7631
  opened.push(box);
@@ -7059,6 +7697,7 @@ const openKafkaLab = async (kafka) => {
7059
7697
  netns: { client: clientNetns },
7060
7698
  kernel: records.seen,
7061
7699
  raw,
7700
+ fences,
7062
7701
  async sysctl(values) {
7063
7702
  const settings = Object.entries(values).map(([key, value]) => `${key}=${value}`);
7064
7703
  if (settings.length > 0) await output(admin, [
@@ -7086,11 +7725,46 @@ const openKafkaLab = async (kafka) => {
7086
7725
  }
7087
7726
  throw new Error(`the brokers did not come up in ${within} ms: ${last}`);
7088
7727
  },
7089
- async create(topic) {
7728
+ async create(topic, partitions = 1) {
7090
7729
  const ids = kafka.brokers.map((name) => nodeIdOf(kafka, name)).join(",");
7091
- const reply = await tool("create", topic, String(kafka.minInsync), ids);
7730
+ const reply = await tool("create", topic, String(kafka.minInsync), ids, String(partitions));
7092
7731
  if (!reply.includes("\"ok\":true")) throw new Error(`the create of the topic failed: ${reply}`);
7093
7732
  },
7733
+ async fill(topic, partitions, count) {
7734
+ const reply = await tool("fill", topic, String(partitions), String(count));
7735
+ if (!reply.includes("\"ok\":true")) throw new Error(`the fill of the topic failed: ${reply}`);
7736
+ },
7737
+ async consumer(args) {
7738
+ const events = lines();
7739
+ const container = await new GenericContainer(clientImage).withNetworkMode(`container:${holder.getId()}`).withCommand([
7740
+ "sh",
7741
+ "-c",
7742
+ "java \"$@\"; :",
7743
+ "consumer",
7744
+ CLIENT_HEAP,
7745
+ "-cp",
7746
+ CLASS_PATH,
7747
+ "ConsumerKafka",
7748
+ bootstrap,
7749
+ ...args
7750
+ ]).withLogConsumer(events.consume).start();
7751
+ const signaller = await withPid(new GenericContainer(image).withNetworkMode(`container:${holder.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${container.getId()}`).start();
7752
+ return {
7753
+ events: events.seen,
7754
+ until: (kinds, within) => events.until(kinds, within, "client"),
7755
+ signal: (signal, at) => stamped(signaller, [
7756
+ "/opt/harness/inject",
7757
+ "signal",
7758
+ "java",
7759
+ signal,
7760
+ ...after(at)
7761
+ ]),
7762
+ stop: async () => {
7763
+ await container.stop({ timeout: 2 }).catch(() => void 0);
7764
+ await signaller.stop(STOP).catch(() => void 0);
7765
+ }
7766
+ };
7767
+ },
7094
7768
  async converge(topic, within = CONVERGE_MS) {
7095
7769
  const begin = Date.now();
7096
7770
  let calm = 0;
@@ -7342,6 +8016,105 @@ const kafkaOnce = async (lab, cell, environment, index) => {
7342
8016
  }
7343
8017
  };
7344
8018
  };
8019
+ /** This count is the partitions of the topic of a run of a paused consumer: one for each member. */
8020
+ const PAUSE_PARTITIONS = 2;
8021
+ /** 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. */
8022
+ const PAUSE_READY_MS = 9e4;
8023
+ /** 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. */
8024
+ const PAUSE_BATCH_MS = 3e4;
8025
+ /**
8026
+ * This function does one run of a paused consumer:
8027
+ * - The topic of the run has two partitions, each with records that a run does not finish.
8028
+ * - Two members join one group, and the run waits until each member holds one partition and the
8029
+ * first member has two commits on its partition.
8030
+ * - A SIGSTOP stops the first member while it holds its next batch, and a SIGCONT ends the stop
8031
+ * after the hold of the cell. The run then waits for the tail.
8032
+ *
8033
+ * The fault is null when the members did not get one partition each. The measure then voids the run.
8034
+ * The law reads no kernel record, thus a lost record does not void this run.
8035
+ */
8036
+ const kafkaPauseOnce = async (lab, cell, environment, index) => {
8037
+ const pause = cell.kafka?.pause;
8038
+ if (pause === void 0) throw new Error("a run of a paused consumer needs `Kafka.pause`");
8039
+ if (cell.fault !== "freeze-process") throw new Error(`a paused consumer has no fault ${cell.fault}`);
8040
+ if (cell.hold === void 0) throw new Error("a paused consumer needs a hold: its law reads it");
8041
+ const hold = valueIn(cell.hold, "s");
8042
+ const topic = topicOf(environment, index);
8043
+ const group = `pause-e${environment}-r${index + 1}`;
8044
+ await lab.brokersUp();
8045
+ await lab.create(topic, PAUSE_PARTITIONS);
8046
+ const leader = await lab.converge(topic);
8047
+ await lab.fill(topic, PAUSE_PARTITIONS, pause.fill);
8048
+ await lab.forget();
8049
+ const from = lab.kernel.length;
8050
+ const lostBefore = droppedSoFar$1(lab.kernel);
8051
+ const common = [
8052
+ "--topic",
8053
+ topic,
8054
+ "--group",
8055
+ group,
8056
+ "--protocol",
8057
+ pause.protocol,
8058
+ "--max-s",
8059
+ String(Math.ceil(hold + 180)),
8060
+ ...Object.entries(cell.driverOptions ?? {}).flatMap(([name, value]) => ["--set", `${name}=${String(value)}`])
8061
+ ];
8062
+ const members = [];
8063
+ let fault = null;
8064
+ let heal = null;
8065
+ let otherEvents = [];
8066
+ let firstEvents = [];
8067
+ try {
8068
+ const first = await lab.consumer([
8069
+ ...common,
8070
+ "--process-ms",
8071
+ String(valueIn(pause.process, "ms")),
8072
+ "--after-pause",
8073
+ pause.afterPause
8074
+ ]);
8075
+ members.push(first);
8076
+ const other = await lab.consumer([
8077
+ ...common,
8078
+ "--process-ms",
8079
+ "0",
8080
+ "--after-pause",
8081
+ "commit"
8082
+ ]);
8083
+ members.push(other);
8084
+ firstEvents = first.events;
8085
+ otherEvents = other.events;
8086
+ const begun = Date.now();
8087
+ while (Date.now() - begun < PAUSE_READY_MS && !pauseReady(first.events, other.events)) await sleep$1(100);
8088
+ if (pauseReady(first.events, other.events)) {
8089
+ const seen = first.events.at(-1)?.t ?? 0;
8090
+ const batch = await waitFor(first, (event) => event.ev === "processing" && event.t > seen, PAUSE_BATCH_MS);
8091
+ if (batch !== void 0) {
8092
+ fault = await first.signal("STOP", batch.t + valueIn(pause.stopAfter, "s") * 1e9);
8093
+ heal = await first.signal("CONT", fault.t0 + hold * 1e9);
8094
+ await sleep$1(KAFKA_TAIL_MS);
8095
+ }
8096
+ }
8097
+ } finally {
8098
+ for (const member of members) await member.stop();
8099
+ }
8100
+ const settled = await lab.settle();
8101
+ return {
8102
+ environment,
8103
+ index,
8104
+ events: [...firstEvents],
8105
+ fault,
8106
+ ...heal === null ? {} : { heal },
8107
+ kernel: lab.kernel.slice(from),
8108
+ netns: lab.netns.client,
8109
+ targetNetns: lab.members[leader].netns,
8110
+ lost: settled === null ? null : settled - lostBefore,
8111
+ pause: {
8112
+ other: [...otherEvents],
8113
+ fences: lab.fences.filter((line) => namesGroup(line, group)),
8114
+ group
8115
+ }
8116
+ };
8117
+ };
7345
8118
  //#endregion
7346
8119
  //#region src/run.ts
7347
8120
  /**
@@ -7433,6 +8206,8 @@ const probeArgs = (cell, lab) => {
7433
8206
  }
7434
8207
  if (probe.keepalive) args.push("--keepalive");
7435
8208
  if (probe.userTimeout !== void 0) args.push("--user-timeout-ms", String(valueIn(probe.userTimeout, "ms")));
8209
+ if (probe.plan !== void 0) args.push("--schedule", SCHEDULE_PATH);
8210
+ if (probe.streams !== void 0) args.push("--plan", LOAD_PATH);
7436
8211
  args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
7437
8212
  return args;
7438
8213
  };
@@ -7615,7 +8390,16 @@ const once = async (lab, cell, environment, index, dry = false) => {
7615
8390
  const middleboxFrom = lab.middlebox.length;
7616
8391
  const serverFrom = lab.server.length;
7617
8392
  const lostBefore = droppedSoFar(lab.kernel);
7618
- const probe = await lab.probe(probeArgs(cell, lab));
8393
+ const plan = cell.probe.plan === void 0 ? void 0 : schedule(cell.probe.plan.profile, cell.probe.plan.options);
8394
+ const load = cell.probe.streams === void 0 ? void 0 : loadPlanOf(cell.probe.streams);
8395
+ const files = [...plan === void 0 ? [] : [{
8396
+ content: JSON.stringify(plan),
8397
+ target: SCHEDULE_PATH
8398
+ }], ...load === void 0 ? [] : [{
8399
+ content: load.content,
8400
+ target: LOAD_PATH
8401
+ }]];
8402
+ const probe = await lab.probe(probeArgs(cell, lab), files.length === 0 ? void 0 : files);
7619
8403
  const server = cell.observer === "server";
7620
8404
  const peer = server ? {
7621
8405
  freeze: () => probe.freeze(),
@@ -7628,7 +8412,11 @@ const once = async (lab, cell, environment, index, dry = false) => {
7628
8412
  const last = probe.events.slice(-3).map((event) => JSON.stringify(event));
7629
8413
  throw new Error(`${error.message}; the client's last lines: ${last.join(" ")}`);
7630
8414
  });
7631
- const placed = lab.topology === null ? {} : { topology: lab.topology };
8415
+ const placed = {
8416
+ ...lab.topology === null ? {} : { topology: lab.topology },
8417
+ ...plan === void 0 ? {} : { schedule: plan },
8418
+ ...load === void 0 ? {} : { streams: load.streams }
8419
+ };
7632
8420
  const fabricOf = () => {
7633
8421
  if (lab.fabric === void 0) throw new Error(`${cell.name} silences a router of a lab with no fabric`);
7634
8422
  return lab.fabric;
@@ -7773,7 +8561,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
7773
8561
  fault = await apply();
7774
8562
  try {
7775
8563
  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();
8564
+ if ((cell.probe.mode === "inflight" || cell.probe.mode === "stream" || cell.probe.mode === "schedule") && !replyInFlight) await go();
7777
8565
  const { prediction } = cell;
7778
8566
  const http = cell.probe.http !== void 0;
7779
8567
  if (prediction.detects !== null && prediction.driver !== void 0) {
@@ -7838,7 +8626,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
7838
8626
  await lift();
7839
8627
  if (toxicFirst) await lab.toxic(null);
7840
8628
  }
7841
- if (cell.fault === "freeze-process" && !server && cell.probe.mode !== "idle") await probe.until(["data"], 5e3).catch(() => void 0);
8629
+ if (cell.fault === "freeze-process" && !server && cell.probe.mode !== "idle" && cell.probe.mode !== "schedule") await probe.until(["data"], 5e3).catch(() => void 0);
7842
8630
  const expected = cell.prediction.detects !== false ? cell.prediction.reconnects : void 0;
7843
8631
  if (expected !== void 0) {
7844
8632
  const waits = expected.waits.reduce((sum, wait) => sum + valueIn(wait.high, "s"), 0);
@@ -7918,9 +8706,11 @@ const inEnvironment = async (cell, environment, count, say) => {
7918
8706
  if (latency !== null) await lab.netem(latency, "target");
7919
8707
  await lab.warm();
7920
8708
  const manifest = await lab.manifest(await lab.sysctl(cell.sysctls), settings, targetSysctls);
8709
+ const missing = await lab.namespaceSetup(cell.namespaceSetup ?? []).then(() => lab.hostSysctl(cell.hostSysctls ?? [])).then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
7921
8710
  const timed = (run) => ({
7922
8711
  ...run,
7923
- jitter: jitterOf(cell, run)
8712
+ jitter: jitterOf(cell, run),
8713
+ ...missing === void 0 ? {} : { missing }
7924
8714
  });
7925
8715
  const warmup = {
7926
8716
  ...timed(await withSteal(() => once(lab, cell, environment, -1, endsEnvironment(cell)))),
@@ -7933,8 +8723,12 @@ const inEnvironment = async (cell, environment, count, say) => {
7933
8723
  runs.push(run);
7934
8724
  say(line(cell, run, measure(cell, run)));
7935
8725
  }
8726
+ const host = await lab.restoreHost();
7936
8727
  return {
7937
- manifest,
8728
+ manifest: host.length === 0 ? manifest : {
8729
+ ...manifest,
8730
+ host
8731
+ },
7938
8732
  runs,
7939
8733
  warmup
7940
8734
  };
@@ -8064,16 +8858,17 @@ const inClusterEnvironment = async (cell, environment, count, say) => {
8064
8858
  */
8065
8859
  const inKafkaEnvironment = async (cell, environment, count, say) => {
8066
8860
  const lab = await openKafkaLab(cell.kafka);
8861
+ const once = cell.kafka.pause === void 0 ? kafkaOnce : kafkaPauseOnce;
8067
8862
  try {
8068
8863
  const manifest = await lab.manifest(await lab.sysctl(cell.sysctls));
8069
8864
  const warmup = {
8070
- ...await kafkaOnce(lab, cell, environment, -1),
8865
+ ...await once(lab, cell, environment, -1),
8071
8866
  warmup: true
8072
8867
  };
8073
8868
  say(line(cell, warmup, measure(cell, warmup)));
8074
8869
  const runs = [];
8075
8870
  for (let index = 0; index < count; index += 1) {
8076
- const run = await kafkaOnce(lab, cell, environment, index);
8871
+ const run = await once(lab, cell, environment, index);
8077
8872
  runs.push(run);
8078
8873
  say(line(cell, run, measure(cell, run)));
8079
8874
  }
@@ -8140,6 +8935,6 @@ const runCell = async (cell, say = () => {}) => {
8140
8935
  }
8141
8936
  };
8142
8937
  //#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 };
8938
+ export { AWS_FROM, CLIENT_CAPABILITIES, 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, 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, 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, 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
8939
 
8145
8940
  //# sourceMappingURL=index.js.map