@mark1russell7/harness 0.11.11 → 0.11.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,8 @@ A topology (`Topology`) is the chain of hops from the client to the target, as d
45
45
  | `recreate` | nothing | passes, and the flow starts again | dropped |
46
46
  | `none` | nothing | passes, and the flow starts again | a reset from the stack of the router |
47
47
 
48
- - **A proxy** (`ProxyHop`) is a stock image, pinned by digest, that ends each TCP connection and opens its own to the next hop. Its files go into it before it starts, and `{{next}}` in them is the address and port of the next hop. An injector sidecar shares its namespace. A privileged sidecar sets its sysctls when the hop names them.
48
+ - **A proxy** (`ProxyHop`) is a stock image, pinned by digest, that ends each TCP connection and opens its own to the next hop. Its files go into it before it starts, and `{{next}}` in them is the address and port of the next hop. Its `command` and its `environment` replace those of its image. An injector sidecar shares its namespace. A privileged sidecar sets its sysctls when the hop names them.
49
+ - The lab keeps the last 60 lines of the output of each proxy. A proxy that does not start gives them in its error.
49
50
 
50
51
  The element before a router reaches the networks after the router through it. Its privileged sidecar, or the router itself, adds each route. A router translates the address of the side of the client, thus the target and each later hop see the router as the peer.
51
52
 
@@ -164,6 +165,7 @@ The harness ships ten images, each as a shipped source (`ImageSource`). The tag
164
165
  - `inflight` is the text of the query in flight: the second query of an in-flight run. The first query then makes the pool ready at once, and only the query in flight is slow, for example `SELECT pg_sleep(20)`. The HikariCP client reads it too.
165
166
  - **The HikariCP client image** (`image/hikari`) is HikariCP 7.1.0 over pgjdbc 42.7.13 on Temurin 21.0.12.1. The digests of the two stages and the SHA-256 of each jar pin them. The probe is `{ driver: { name: "hikari", query, inflight } }`.
166
167
  - `ClientHikari.java` makes one `HikariDataSource` with the properties of the cell (`Cell.driverOptions`). A name with the prefix `dataSource.` is a property of pgjdbc.
168
+ - The user is `postgres`, unless the cell gives `username` (and `password`). Supavisor finds the tenant of a client in its user, for example `postgres.harness`.
167
169
  - It prints the lines of the pg client on the same clock. Each socket comes from the `PGStream` of its connection, and a `pool` line gives each change of the counts of the pool. The errno of a failure is the SQLState of pgjdbc.
168
170
  - Before its first line, it loads the classes of a connect and of a pool on the loopback of its namespace. It also starts the SecureRandom that HikariCP reads for the UUID of each new connection. Then it parses an ERROR of the server, as for the timeout of a statement. A new JVM loads each class at its first use. Thus a band does not time that load.
169
171
  - **The MongoDB client image** (`image/mongo`) is the driver mongodb 7.7.0 on Node 24.21.0. The digest of the image and an npm lockfile pin them. `client-mongo.mjs` prints the lines of the TCP client on the same clock:
@@ -241,6 +243,9 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
241
243
  - **Recovery.** After a stall (`prediction.recovers`), it times the send to the first segment back after the stall stopped. That segment is the answer of the server to the retransmission that got through. A run whose stall stopped outside the window of its prediction is void. A run where nothing came back is a finding.
242
244
  - **An end of file.** A FIN counts from the change of state of the socket of the peer when it closes (`peer-closed`). It ends at the change of the socket of the observer to CLOSE_WAIT. The kernel stamps the two changes.
243
245
  - **A check of a pool.** A check on an idle connection counts from the first segment with data that the socket sends after the fault (`first-sent`). The keepalive of HikariCP comes at a phase from the creation of its connection. Thus no line of the client gives its time, and the kernel stamps it. The prediction gives the latest time of that send after the reply (`startsBy`), and the run waits that much longer.
246
+ - **The connect of a pooler.** A band of a driver can be on the socket that a hop opens toward the target (`socket: "upstream"`). It is the first socket of a hop that moves to SYN_SENT after the outset. A pooler connects to the server by itself, thus no line of the client names that socket.
247
+ - A connect timer counts from the change of that socket to SYN_SENT (`syn-sent`).
248
+ - The band ends at the error that the kernel reports, for example ETIMEDOUT after the SYNs. Without an error, it ends when the program closes its socket in SYN_SENT (`closed`). That close has no error and no FIN, only the change to CLOSE. A refused connect also leaves SYN_SENT, but it has its error.
244
249
  - **A detection by the program** (`application.ts`). The kernel has no report of it. Thus the program of the observer writes a line before each of its own acts, and the kernel stamps each act on the socket.
245
250
  - The program writes `ping` before each ping of its heartbeat. The kernel stamps the write of the ping (`tx`).
246
251
  - The reference is the first ping that got no answer (`ping-sent`): no segment with data reached the socket after its write, up to the detection. Its write starts the timer of the heartbeat that the fault met. The fault can come at any time, and its stamps are not necessary.
@@ -97,6 +97,8 @@ interface ProxyHop {
97
97
  /** This port is the port that the proxy listens on for the side of the client. */
98
98
  readonly port: number;
99
99
  readonly command?: readonly string[];
100
+ /** These variables are the environment of the proxy, over the environment of its image. */
101
+ readonly environment?: Readonly<Record<string, string>>;
100
102
  /**
101
103
  * These files go into the proxy before it starts, by their path. In their content, {{next}} is
102
104
  * the address and port of the next hop, and {{next.host}} and {{next.port}} are each part.
@@ -231,8 +233,11 @@ interface Toxic {
231
233
  * A check of a pool on an idle connection counts from the first segment with data that the socket
232
234
  * sends after the fault (`first-sent`). The keepalive of HikariCP comes at a phase from the
233
235
  * creation of its connection, thus no line of the client gives its time. The kernel stamps it.
236
+ *
237
+ * A connect timer counts from the change of its socket to SYN_SENT (`syn-sent`). A pooler connects
238
+ * to the server by itself, thus the kernel stamps that connect, and no line of the client does.
234
239
  */
235
- type Reference = "last-received" | "first-retransmission" | "request-sent" | "peer-closed" | "fault" | "client-start" | "ping-sent" | "first-sent";
240
+ type Reference = "last-received" | "first-retransmission" | "request-sent" | "peer-closed" | "fault" | "client-start" | "ping-sent" | "first-sent" | "syn-sent";
236
241
  /**
237
242
  * This prediction tells when a connection moves again after a stall. That occurs at the first
238
243
  * firing of its retransmission timer after the stall stops, and its reply comes back one round
@@ -1018,4 +1023,4 @@ interface RunTopology {
1018
1023
  }
1019
1024
  //#endregion
1020
1025
  export { isHopPlace as $, Recovers as A, RunPart as B, Observer as C, ProxyHop as D, ProbeEvent as E, RequestPrediction as F, ShippedImage as G, SentinelEvent as H, RouterHop as I, Subject as J, Stage as K, RttRange as L, ReplicaSet as M, ReplicaSetStage as N, ProxyTime as O, Replies as P, WsProbe as Q, Run as R, MiddleboxForm as S, Prediction as T, SentinelStage as U, RunTopology as V, Shipped as W, Topology as X, Target as Y, Toxic as Z, ImageSource as _, DriverBand as a, MiddleboxEvent as b, FaultPlace as c, GroupPrediction as d, latestEndOf as et, GroupRole as f, HttpProbe as g, HopPlace as h, ClientImage as i, Reference as j, Reconnects as k, Group as l, Hop as m, Cell as n, DriverStep as o, HarnessImage as p, StageBand as q, Client as r, Fault as s, BuildImage as t, GroupPlace as u, KernelRecord as v, PinnedImage as w, MiddleboxExpectation as x, LineMatch as y, RunGroup as z };
1021
- //# sourceMappingURL=cell-6DQfSaJP.d.ts.map
1026
+ //# sourceMappingURL=cell-COlAmUZ-.d.ts.map
package/build/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as isHopPlace, A as Recovers, B as RunPart, C as Observer, D as ProxyHop, E as ProbeEvent, F as RequestPrediction, G as ShippedImage, H as SentinelEvent, I as RouterHop, J as Subject, K as Stage, L as RttRange, M as ReplicaSet, N as ReplicaSetStage, O as ProxyTime, P as Replies, Q as WsProbe, R as Run, S as MiddleboxForm, T as Prediction, U as SentinelStage, V as RunTopology, W as Shipped, X as Topology, Y as Target, Z as Toxic, _ as ImageSource, a as DriverBand, b as MiddleboxEvent, c as FaultPlace, d as GroupPrediction, et as latestEndOf, f as GroupRole, g as HttpProbe, h as HopPlace, i as ClientImage, j as Reference, k as Reconnects, l as Group, m as Hop, n as Cell, o as DriverStep, p as HarnessImage, q as StageBand, r as Client, s as Fault, t as BuildImage, u as GroupPlace, v as KernelRecord, w as PinnedImage, x as MiddleboxExpectation, y as LineMatch, z as RunGroup } from "./cell-6DQfSaJP.js";
1
+ import { $ as isHopPlace, A as Recovers, B as RunPart, C as Observer, D as ProxyHop, E as ProbeEvent, F as RequestPrediction, G as ShippedImage, H as SentinelEvent, I as RouterHop, J as Subject, K as Stage, L as RttRange, M as ReplicaSet, N as ReplicaSetStage, O as ProxyTime, P as Replies, Q as WsProbe, R as Run, S as MiddleboxForm, T as Prediction, U as SentinelStage, V as RunTopology, W as Shipped, X as Topology, Y as Target, Z as Toxic, _ as ImageSource, a as DriverBand, b as MiddleboxEvent, c as FaultPlace, d as GroupPrediction, et as latestEndOf, f as GroupRole, g as HttpProbe, h as HopPlace, i as ClientImage, j as Reference, k as Reconnects, l as Group, m as Hop, n as Cell, o as DriverStep, p as HarnessImage, q as StageBand, r as Client, s as Fault, t as BuildImage, u as GroupPlace, v as KernelRecord, w as PinnedImage, x as MiddleboxExpectation, y as LineMatch, z as RunGroup } from "./cell-COlAmUZ-.js";
2
2
  import { Quantity } from "@mark1russell7/quantity";
3
3
  import "testcontainers";
4
4
  //#region src/application.d.ts
package/build/index.js CHANGED
@@ -682,8 +682,28 @@ const peerPlace = (run, local) => {
682
682
  const peer = run.topology === void 0 ? local : `${run.topology.targetPeer}:${portOf(local)}`;
683
683
  return placeOf(run, run.targetNetns, (record) => record.dst === peer);
684
684
  };
685
- /** This function gives the kernel place of a socket of a role at an instant. The role `server` is a socket of the target. */
685
+ /**
686
+ * This function gives the socket that a hop of the topology opens toward its target in the run. It is
687
+ * the first one that moves to SYN_SENT after the outset. A pooler connects to the server by itself,
688
+ * thus no line of the client names that socket. The last hop goes first, because it faces the target.
689
+ */
690
+ const upstreamOf = (run) => {
691
+ if (run.topology === void 0) return void 0;
692
+ const outset = outsetOf(run);
693
+ for (const hop of Object.values(run.topology.hops).reverse()) {
694
+ const place = placeOf(run, hop.netns, (record) => record.ev === "state" && newState$1(record) === 2 && record.mono >= outset && record.dst === hop.target);
695
+ if (place !== void 0) return place;
696
+ }
697
+ };
698
+ /**
699
+ * This function gives the kernel place of a socket of a role at an instant. The role `server` is a
700
+ * socket of the target, and the role `upstream` is the socket that a hop opens toward the target.
701
+ */
686
702
  const kernelOf = (run, role, at) => {
703
+ if (role === "upstream") return {
704
+ socket: void 0,
705
+ place: upstreamOf(run)
706
+ };
687
707
  if (role === "server") {
688
708
  const pool = socketAt(run.events, "pool", at);
689
709
  return {
@@ -728,6 +748,7 @@ const referenceOf = (run, from, role, data) => {
728
748
  return place.records.filter((record) => record.mono <= before && counts(record)).at(-1)?.mono;
729
749
  }
730
750
  if (from === "first-sent") return place.records.find((record) => record.ev === "tx" && record.mono > before && (record.seg?.len.value ?? 0) > 0)?.mono;
751
+ if (from === "syn-sent") return place.records.find((record) => record.ev === "state" && newState$1(record) === 2)?.mono;
731
752
  if (from === "first-retransmission") {
732
753
  const request = run.events.filter((event) => event.ev === "sent")[1]?.t ?? before;
733
754
  return place.records.find((record) => record.ev === "retrans" && record.why === "rto" && record.mono > request)?.mono;
@@ -755,10 +776,21 @@ const ipOf$1 = (address) => address.slice(0, address.lastIndexOf(":"));
755
776
  const lineAfter = (events, match, after) => events.find((event) => event.t > after && matches(event, match));
756
777
  /** This function gives the name of the error of a line: the field that the match names, or `name`. */
757
778
  const nameOf = (line, match) => String(fieldOf(line, match.name ?? "name") ?? line.ev);
758
- /** This function gives the report of the kernel on a socket after an instant: an error, or the end of the file (CLOSE_WAIT). */
779
+ /**
780
+ * This function gives the report of the kernel on a socket after an instant: an error, or the end of
781
+ * the file (CLOSE_WAIT). With neither, a close of the socket in SYN_SENT is the end. The program gave
782
+ * up on its connect, and the kernel reports no error and sends no FIN. A refused connect also leaves
783
+ * SYN_SENT, but it has its error.
784
+ */
759
785
  const reportOf = (place, after) => {
760
786
  const report = place.records.find((record) => record.mono > after && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && newState$1(record) === 8));
761
- if (report === void 0) return void 0;
787
+ if (report === void 0) {
788
+ const given = place.records.find((record) => record.mono > after && record.ev === "state" && oldState$1(record) === 2 && newState$1(record) === 7);
789
+ return given === void 0 ? void 0 : {
790
+ at: given.mono,
791
+ outcome: { kind: "closed" }
792
+ };
793
+ }
762
794
  return report.ev === "error" ? {
763
795
  at: report.mono,
764
796
  outcome: {
@@ -972,7 +1004,7 @@ const measureDriver = (cell, run) => {
972
1004
  finding: `it ended without its reference event (${prediction.from} on the ${driver.socket} socket)`
973
1005
  };
974
1006
  if (driver.ends === void 0 && prediction.from === "last-received" && place !== void 0) voided ??= delayed(place, reference, ended, run.hz, noiseOf(cell));
975
- const name = outcome.kind === "error" ? outcome.errno : outcome.kind === "eof" ? "EOF" : "silent";
1007
+ const name = outcome.kind === "error" ? outcome.errno : outcome.kind === "eof" ? "EOF" : outcome.kind === "closed" ? "CLOSED" : "silent";
976
1008
  if (prediction.errno !== "OK" && name !== prediction.errno) findings.unshift(`it ended with ${name}, not ${prediction.errno}`);
977
1009
  const seen = report === void 0 ? void 0 : events.find((event) => event.t >= report.at && (event.ev === "error" || event.ev === "socket-error"));
978
1010
  return {
@@ -1694,7 +1726,7 @@ const measureSocket = (cell, run) => {
1694
1726
  if (from === "fault") return run.fault?.t0;
1695
1727
  if (from === "client-start") return (events.find((event) => event.ev === "connecting") ?? events[0])?.t;
1696
1728
  if (from === "ping-sent") return firstUnanswered(pings, socket, ended)?.at;
1697
- const record = from === "last-received" ? socket.filter((each) => each.ev === "rx" && each.mono <= before).at(-1) : from === "first-retransmission" ? retransmitted.find((each) => each.why === "rto" && each.mono > before) : from === "first-sent" ? socket.find((each) => each.ev === "tx" && each.mono > before && (each.seg?.len.value ?? 0) > 0) : kernel.find((each) => each.netns === peerNetns && each.cookie === peerCookie && each.ev === "state" && oldState(each) === 1);
1729
+ const record = from === "last-received" ? socket.filter((each) => each.ev === "rx" && each.mono <= before).at(-1) : from === "first-retransmission" ? retransmitted.find((each) => each.why === "rto" && each.mono > before) : from === "first-sent" ? socket.find((each) => each.ev === "tx" && each.mono > before && (each.seg?.len.value ?? 0) > 0) : from === "syn-sent" ? socket.find((each) => each.ev === "state" && newState(each) === 2) : kernel.find((each) => each.netns === peerNetns && each.cookie === peerCookie && each.ev === "state" && oldState(each) === 1);
1698
1730
  if (record === void 0 && from === "peer-closed" && peerCookie === void 0) return proxiedClose(run);
1699
1731
  return record?.mono;
1700
1732
  };
@@ -1946,6 +1978,8 @@ const PUMP_SLACK = {
1946
1978
  value: 5,
1947
1979
  unit: "ms"
1948
1980
  };
1981
+ /** This count is how many of the last lines of a proxy hop the lab keeps. The error of a hop that does not start gives them. */
1982
+ const TAIL_LINES = 60;
1949
1983
  /** This function gives the arguments of the injector for the options of a fault. */
1950
1984
  const optionArgs = (options) => [
1951
1985
  ...options.direction === void 0 ? [] : ["--direction", options.direction],
@@ -2231,12 +2265,27 @@ const openLab = async (target, kind = "tcp", topology) => {
2231
2265
  const next = endpointOf(at + 1);
2232
2266
  let proxy = new GenericContainer(await imageOf(hop.image)).withNetwork(targetSide);
2233
2267
  if (hop.command !== void 0) proxy = proxy.withCommand([...hop.command]);
2268
+ if (hop.environment !== void 0) proxy = proxy.withEnvironment({ ...hop.environment });
2234
2269
  if (hop.files !== void 0) proxy = proxy.withCopyContentToContainer(Object.entries(hop.files).map(([path, content]) => ({
2235
2270
  content: templated(content, next),
2236
2271
  target: path
2237
2272
  })));
2238
2273
  if (hop.ready !== void 0) proxy = proxy.withWaitStrategy(Wait.forLogMessage(hop.ready));
2239
- const box = await proxy.start();
2274
+ const tail = [];
2275
+ proxy = proxy.withLogConsumer((stream) => {
2276
+ let rest = "";
2277
+ stream.on("data", (chunk) => {
2278
+ const lines = (rest + chunk.toString()).split("\n");
2279
+ rest = lines.pop() ?? "";
2280
+ for (const line of lines) {
2281
+ tail.push(line);
2282
+ if (tail.length > TAIL_LINES) tail.shift();
2283
+ }
2284
+ });
2285
+ });
2286
+ const box = await proxy.start().catch((error) => {
2287
+ throw new Error(`the hop ${hop.name} did not start: ${error instanceof Error ? error.message : String(error)}. Its last lines:\n${tail.join("\n")}`);
2288
+ });
2240
2289
  opened.push(box);
2241
2290
  await join(box, clientSide);
2242
2291
  const injector = await new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW").start();
@@ -4220,7 +4269,8 @@ const beforeOnce = async (lab, cell, environment, index) => {
4220
4269
  kernel: lab.kernel.slice(from),
4221
4270
  netns: lab.netns.client,
4222
4271
  targetNetns: lab.netns.target,
4223
- lost: 0
4272
+ lost: 0,
4273
+ ...lab.topology === null ? {} : { topology: lab.topology }
4224
4274
  });
4225
4275
  await driverOutcome(cell, current, driverWithin(cell));
4226
4276
  await sleep(TAIL_MS);