@mark1russell7/harness 0.11.0 → 0.11.2

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
@@ -107,10 +107,11 @@ A fault names a member by its role at the start of a run: `primary`, or `replica
107
107
  - or by itself, a gap after its first reply (`gap`), for the keep-alive race.
108
108
  - **The lines of an HTTP client.** It writes the lines of the TCP client, with the number `i` of the request on each line of a request. It also writes the connection of each try of a request (`request`, then `retry`), with its local address and a flag for a reused connection. Each connection gets a `conn` line, and each end of a connection a line (`eof`, `conn-error`, `closed`). The end of the second request is a `done` line.
109
109
  - **A target with a script.** A target can take files before it starts (`Target.files`), for example the script of a Node server on the stock Node image. A target that writes JSON lines (`Target.lines`) gives them to each run (`Run.server`). The manifest holds the sha256 of each file and the first line of the target.
110
- - **The WebSocket client** (`Cell.client` is `procedures-ws`) is the `WebSocketTransport` of `@procedures/client` 0.3.0. The cell gives each of its settings (`probe.ws`): the class of its sockets, the heartbeat, the reconnect, and the times of a call.
110
+ - **The WebSocket client** (`Cell.client` is `procedures-ws`) is the `WebSocketTransport` of `@procedures/client` 0.7.0. The cell gives each of its settings (`probe.ws`): the class of its sockets, the heartbeat, the reconnect, and the times of a call.
111
111
  - Its first request is `harness.echo`. In flight, its request into the fault is `harness.echo`, or `harness.hold` for a held reply.
112
112
  - It writes the lines of the TCP client. It also writes a line for each act of the program: `ping`, `drop`, `reconnecting` and `dial`.
113
- - The class of its sockets is the global `WebSocket` of Node, or the `WebSocket` of the `ws` package (the option `webSocket`). The transport gets a subclass of that class. The subclass writes a line, then gives the work to its class. Thus the transport, undici and ws operate as they do without it.
113
+ - The class of its sockets is the global `WebSocket` of Node, with the transport of the root entry. Or it is the `WebSocket` of the `ws` package, with the transport of the Node entry `@procedures/client/node`.
114
+ - The transport gets a subclass of that class. The subclass writes a line, then gives the work to its class. Thus the transport, undici and ws operate as they do without it.
114
115
  - The subclass has `terminate()` only when its class has it, because the transport drops a socket with `terminate()` when it exists.
115
116
  - **A target from an image of the harness** (`Target.harness`). The target operates an image of the harness in place of its stock image. The procedures server is a script in the image of the WebSocket client (`server-ws.mjs`). Thus the client and the server use one release of the package. `Target.image` is the stock base of that image, and the manifest names both.
116
117
  - **An agent** (`lab.agent(image, command, { pid })`) is a container in the network namespace of the target. With `pid`, it also shares the process namespace of the target, thus it can signal the server process. Its JSON lines and its `exec` stay available to the caller.
@@ -128,12 +129,13 @@ The harness builds six images. The tag of each image comes from the content of a
128
129
  - **The Node client image** (`image/node`) is a driver client: ioredis 6.0.0 on Node 24.21.0. The digest of the image and an npm lockfile pin them. `client-ioredis.mjs` prints the lines of the TCP client on the same clock, and it adds the status changes of ioredis as `status` lines. The same image holds the client of a group in Sentinel mode (`client-sentinel.mjs`) and the observer of its Sentinels (`observer.mjs`).
129
130
  - **The Go HTTP image** (`image/go-http`) is the `http.Transport` of Go 1.27.2 at its defaults (`client-go`). The program uses the standard library only, thus the digest of the Go builder pins all of its code. The build does `gofmt` and `go vet` first. A wrapper on each connection writes what the read loop of the transport reads. The trace of the transport writes each try of a request.
130
131
  - **The Node HTTP image** (`image/node-http`) holds the `http.Agent` of Node 24.21.0 (`client-agent.mjs`) and an HTTP/1.1 `Client` of undici 8.12.2 (`client-undici.mjs`). The digest of the image and an npm lockfile pin them. It is apart from the image of ioredis, thus the cells of ioredis keep their image.
132
+ - **The PostgreSQL client image** (`image/pg`) is node-postgres: pg 8.23.1 with pg-pool 3.14.0 on Node 24.21.0. The digest of the image and an npm lockfile pin them. `client-pg.mjs` makes one `pg.Pool` with the options of the cell (`Cell.driverOptions`), and it prints the lines of the TCP client on the same clock. The first query makes the connection, and the reply gives the client back to the pool, where it is idle. An error of the pool for that idle client is the detection of an idle cell. The probe is `{ driver: { name: "pg", query } }`, with `SELECT 1` as the query when the cell gives none.
131
133
  - **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:
132
134
  - Each socket of the driver is a `socket` line with its role: `monitor`, `rtt` or `pool`. The stack of the call that makes the socket gives the role.
133
135
  - The events of the driver are `sdam` lines (the heartbeats and the changes of each server), `cmap` lines (the pools) and `command` lines (each try of an operation).
134
136
  - Each operation is a `sent` line, then an `ack` line or a `fail` line. An `ack` line has the server that accepted it. A `fail` line has the name and the labels of its error.
135
137
  - The same image holds `stepdown-mongo.mjs`, the sender of `replSetStepDown`.
136
- - **The procedures image** (`image/procedures`) holds `@procedures/client` 0.3.0 and `ws` 8.22.0 on Node 24.21.0, whose WebSocket is undici 7.29.1. The digest of the image and an npm lockfile pin them. It holds the WebSocket client (`client-ws.mjs`) and the script of the procedures server (`server-ws.mjs`). Each takes each of its settings as a flag, and it has no default.
138
+ - **The procedures image** (`image/procedures`) holds `@procedures/client` 0.7.0 and `ws` 8.22.0 on Node 24.21.0, whose WebSocket is undici 7.29.1. The digest of the image and an npm lockfile pin them. It holds the WebSocket client (`client-ws.mjs`) and the script of the procedures server (`server-ws.mjs`). Each takes each of its settings as a flag, and it has no default. The lockfile holds one copy of `ws`, and the client stops when `@procedures/client/node` resolves another one.
137
139
  - **The router image** (`image/router`) is a digest-pinned Alpine with conntrack-tools, nftables, iproute2 and Python. `mbx.sh` sets the form of the router in one nftables table, `ctwatch.py` writes the conntrack events, and `resetter.py` sends the resets of the `reset-both` form. Each run records its packages in the manifest.
138
140
 
139
141
  The build does the `cargo test` of the two crates. Thus the record decoder passes its tests before an image exists. Each amount that the native programs print is a quantity in a canonical unit. For example, an RTO is in `jiffy`, a round trip in `us`, and a segment in `B`.
@@ -202,6 +204,8 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
202
204
  - A drop counts when it comes before each report of the kernel. A report of the kernel that comes first is the detection, and the drop after it is not.
203
205
  - The second view is the stamps of the program: from its `ping` line to its `drop` line. The steps are the later pings, each from the reference.
204
206
  - The lines of a client name the socket by its local address (`local`), and the lines of a server by its peer (`peer`). For the server, the lines are the lines of the target. A line whose address is null is about a try that did not open.
207
+ - The harness reads the peer of a server from the records of its socket. Behind a router, that peer is the address of the router, not the address of the client.
208
+ - A first ping without an answer can come late, for example at a beat of a server whose phase no document gives. Then the prediction gives its latest time after the reply (`startsBy`), and the run waits that much longer.
205
209
  - A prediction can say how the program drops (`drop`). A close frame (`frame`) is a write, and the socket stays ESTABLISHED after it. A close (`close`) closes the socket in δ_abs: a FIN or an abort. Another drop is a finding. A FIN of the peer that answers a close frame is no close by the program. The run gives the time from the `drop` line to the close by the program (`left`).
206
210
  - The client process ends at the end of each run, and its kernel then closes each socket that is still open. A prediction can say that the program is that client process (`exits`). Then a close in δ_abs of that end is no close by the program: `left` stays null, and `exited` gives the time.
207
211
  - The end of the process is the first change out of ESTABLISHED of another socket of the client after its last `dial` line (`exitOf`). The program dropped none of these sockets.
package/build/index.d.ts CHANGED
@@ -456,6 +456,12 @@ type Prediction = {
456
456
  readonly low: Quantity;
457
457
  /** This time is the closed form, plus the most that the timer can fire late, plus each proxy time. */
458
458
  readonly high: Quantity;
459
+ /**
460
+ * This time is the latest reference of the band after the reply of the client. It is for a
461
+ * reference that can come late, for example the first ping without an answer of a server
462
+ * heartbeat. The run then waits that much longer for the detection.
463
+ */
464
+ readonly startsBy?: Quantity;
459
465
  readonly rtt?: RttRange;
460
466
  readonly through?: readonly ProxyTime[];
461
467
  readonly group?: GroupPrediction;
@@ -560,10 +566,10 @@ type HarnessImage = "procedures";
560
566
  /**
561
567
  * A client is what the runs of a cell use: the TCP client of the harness, or a driver on its
562
568
  * socket. The HTTP clients are the `http.Transport` of Go (`go-http`), the `http.Agent` of Node
563
- * (`http-agent`) and an HTTP/1.1 `Client` of undici (`undici`). The MongoDB driver is `mongodb`, and the WebSocket client of
564
- * `@procedures/client` is `procedures-ws`.
569
+ * (`http-agent`) and an HTTP/1.1 `Client` of undici (`undici`). The MongoDB driver is `mongodb`, node-postgres is `pg`, and
570
+ * the WebSocket client of `@procedures/client` is `procedures-ws`.
565
571
  */
566
- type Client = "tcp" | "ioredis" | "go-http" | "http-agent" | "undici" | "mongodb" | "procedures-ws";
572
+ type Client = "tcp" | "ioredis" | "go-http" | "http-agent" | "undici" | "mongodb" | "pg" | "procedures-ws";
567
573
  /**
568
574
  * A WebSocket probe is the workload and the settings of the WebSocket client of
569
575
  * `@procedures/client` (`procedures-ws`). The cell gives each setting, thus the client takes
@@ -677,6 +683,8 @@ interface Cell {
677
683
  * - ioredis 6 on Node, whose keepAlive is the keepalive delay of the socket.
678
684
  * - the MongoDB driver on Node, against the replica set `set`. Its operation is a write, or
679
685
  * the read of a long document (`read-long`). `Cell.driverOptions` are its options.
686
+ * - node-postgres on Node, as a `pg.Pool` against one server. Its operation is `query`, or
687
+ * `SELECT 1`. `Cell.driverOptions` are the options of its pool.
680
688
  */
681
689
  readonly driver?: {
682
690
  readonly name: "ioredis";
@@ -685,6 +693,9 @@ interface Cell {
685
693
  readonly name: "mongodb";
686
694
  readonly set: string;
687
695
  readonly op?: "write" | "read-long";
696
+ } | {
697
+ readonly name: "pg";
698
+ readonly query?: string;
688
699
  };
689
700
  /** This workload is the workload of an HTTP client (`Cell.client` is an HTTP client). The client then ignores `keepalive` and `userTimeout`. */
690
701
  readonly http?: HttpProbe;
@@ -798,6 +809,13 @@ interface HopPlace {
798
809
  type FaultPlace = "target" | "client" | HopPlace | GroupPlace;
799
810
  /** This function tells if a fault place is a hop of a topology. */
800
811
  export declare const isHopPlace: (place: FaultPlace | undefined) => place is HopPlace;
812
+ /**
813
+ * This function gives the latest end of a band in seconds, from the reply of the client. It is
814
+ * the high bound, after the latest reference of the band (`startsBy`).
815
+ */
816
+ export declare const latestEndOf: (prediction: Extract<Prediction, {
817
+ readonly detects: true;
818
+ }>) => number;
801
819
  /** A middlebox event is one line of the router: a change of a conntrack entry, or a reset that it sent. `t` is CLOCK_MONOTONIC in nanoseconds. */
802
820
  interface MiddleboxEvent {
803
821
  readonly ev: string;
@@ -1561,6 +1579,8 @@ export declare const goHttpImage: () => Promise<string>;
1561
1579
  export declare const nodeHttpImage: () => Promise<string>;
1562
1580
  /** This function gives the image of the MongoDB client. */
1563
1581
  export declare const mongoClientImage: () => Promise<string>;
1582
+ /** This function gives the image of the PostgreSQL client. */
1583
+ export declare const pgClientImage: () => Promise<string>;
1564
1584
  /** This function gives the image of `@procedures/client`: its WebSocket client and its server. */
1565
1585
  export declare const proceduresImage: () => Promise<string>;
1566
1586
  /** This function gives the image that a target operates: its stock image, or an image of the harness (`Target.harness`). */
package/build/index.js CHANGED
@@ -8,6 +8,11 @@ import { fileURLToPath } from "node:url";
8
8
  //#region src/cell.ts
9
9
  /** This function tells if a fault place is a hop of a topology. */
10
10
  const isHopPlace = (place) => typeof place === "object" && "hop" in place;
11
+ /**
12
+ * This function gives the latest end of a band in seconds, from the reply of the client. It is
13
+ * the high bound, after the latest reference of the band (`startsBy`).
14
+ */
15
+ const latestEndOf = (prediction) => valueIn(prediction.high, "s") + (prediction.startsBy === void 0 ? 0 : valueIn(prediction.startsBy, "s"));
11
16
  //#endregion
12
17
  //#region src/verdict.ts
13
18
  /** This function gives the words of the outcome of a second request: its connection, a retry, and its end. */
@@ -1652,8 +1657,9 @@ const measureSocket = (cell, run) => {
1652
1657
  const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8);
1653
1658
  const own = cell.prediction.detects === true && cell.prediction.errno === "CLOSED" || cell.prediction.detects === null && cell.prediction.ends === "own-close" ? socket.find((record) => record.ev === "state" && oldState(record) === 1 && newState(record) === 4) : void 0;
1654
1659
  const lines = programLines(cell, run);
1660
+ const named = server ? socket.find((record) => record.dst !== void 0)?.dst ?? local : local;
1655
1661
  const kernelEnded = reported?.mono ?? closeWait?.mono ?? own?.mono ?? seen?.t;
1656
- const first = dropOf(lines, socket, local, noiseOf(cell));
1662
+ const first = dropOf(lines, socket, named, noiseOf(cell));
1657
1663
  const drop = first !== void 0 && (kernelEnded === void 0 || first.at <= kernelEnded) ? first : void 0;
1658
1664
  const dropped = drop !== void 0;
1659
1665
  const outcome = dropped ? { kind: "dropped" } : reported !== void 0 ? {
@@ -1666,7 +1672,7 @@ const measureSocket = (cell, run) => {
1666
1672
  const ended = drop?.at ?? kernelEnded;
1667
1673
  const noticed = reported !== void 0 && seen !== void 0 && !dropped ? between(reported.mono, seen.t) : null;
1668
1674
  const { prediction } = cell;
1669
- const pings = pingsOf(lines, socket, local, noiseOf(cell));
1675
+ const pings = pingsOf(lines, socket, named, noiseOf(cell));
1670
1676
  const before = run.fault?.t1 ?? ready.t;
1671
1677
  const referenceAt = (from) => {
1672
1678
  if (from === "request-sent") return request.t;
@@ -1947,7 +1953,13 @@ const MONGO = {
1947
1953
  dockerfile: "harness/image/mongo/Dockerfile",
1948
1954
  from: ["harness/image/mongo"]
1949
1955
  };
1950
- /** This image holds `@procedures/client` 0.3.0 on Node 24.21.0: its WebSocket client as a client of the harness, and the script of its server. */
1956
+ /** This image holds the PostgreSQL client: node-postgres (pg 8.23.1 with pg-pool 3.14.0) on Node 24.21.0. */
1957
+ const PG = {
1958
+ name: "loom-harness-pg",
1959
+ dockerfile: "harness/image/pg/Dockerfile",
1960
+ from: ["harness/image/pg"]
1961
+ };
1962
+ /** This image holds `@procedures/client` 0.7.0 on Node 24.21.0: its WebSocket client as a client of the harness, and the script of its server. */
1951
1963
  const PROCEDURES = {
1952
1964
  name: "loom-harness-procedures",
1953
1965
  dockerfile: "harness/image/procedures/Dockerfile",
@@ -2025,6 +2037,8 @@ const goHttpImage = () => build(GO_HTTP);
2025
2037
  const nodeHttpImage = () => build(NODE_HTTP);
2026
2038
  /** This function gives the image of the MongoDB client. */
2027
2039
  const mongoClientImage = () => build(MONGO);
2040
+ /** This function gives the image of the PostgreSQL client. */
2041
+ const pgClientImage = () => build(PG);
2028
2042
  /** This function gives the image of `@procedures/client`: its WebSocket client and its server. */
2029
2043
  const proceduresImage = () => build(PROCEDURES);
2030
2044
  /** This function gives the image that a target operates: its stock image, or an image of the harness (`Target.harness`). */
@@ -2222,6 +2236,11 @@ const clientOf = async (kind, image) => {
2222
2236
  command: ["node", "/opt/harness/mongo/client-mongo.mjs"],
2223
2237
  clientKind: kind
2224
2238
  };
2239
+ if (kind === "pg") return {
2240
+ clientImage: await pgClientImage(),
2241
+ command: ["node", "/opt/harness/pg/client-pg.mjs"],
2242
+ clientKind: kind
2243
+ };
2225
2244
  if (kind === "procedures-ws") return {
2226
2245
  clientImage: await proceduresImage(),
2227
2246
  command: ["node", "/opt/harness/procedures/client-ws.mjs"],
@@ -4185,7 +4204,7 @@ const droppedSoFar = (records) => {
4185
4204
  /** This function gives the argv of the client for a cell, with a deadline far after all that it waits for. */
4186
4205
  const probeArgs = (cell, lab) => {
4187
4206
  const { prediction, probe } = cell;
4188
- const longest = (prediction.detects === true && prediction.driver !== void 0 ? longestOf(cell) : prediction.detects ? valueIn(prediction.high, "s") : valueIn(prediction.window, "s")) + (cell.idleBefore === void 0 ? 0 : valueIn(cell.idleBefore, "s"));
4207
+ const longest = (prediction.detects === true && prediction.driver !== void 0 ? longestOf(cell) : prediction.detects ? latestEndOf(prediction) : valueIn(prediction.window, "s")) + (cell.idleBefore === void 0 ? 0 : valueIn(cell.idleBefore, "s"));
4189
4208
  const args = [
4190
4209
  lab.targetIp,
4191
4210
  String(lab.targetPort),
@@ -4230,6 +4249,13 @@ const probeArgs = (cell, lab) => {
4230
4249
  args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
4231
4250
  return args;
4232
4251
  }
4252
+ if (probe.driver?.name === "pg") {
4253
+ if (probe.driver.query !== void 0) args.push("--query", probe.driver.query);
4254
+ if (cell.before === true) args.push("--before");
4255
+ if (cell.driverOptions !== void 0) args.push("--options", JSON.stringify(cell.driverOptions));
4256
+ args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
4257
+ return args;
4258
+ }
4233
4259
  if (probe.driver !== void 0) {
4234
4260
  args.push("--keepalive-ms", String(valueIn(probe.driver.keepAlive, "ms")));
4235
4261
  args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
@@ -4555,7 +4581,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
4555
4581
  "deadline"
4556
4582
  ], window).catch(() => void 0);
4557
4583
  } else if (prediction.detects) {
4558
- const within = valueIn(prediction.high, "s") * 2e3 + 1e4;
4584
+ const within = latestEndOf(prediction) * 2e3 + 1e4;
4559
4585
  if (cell.observer !== void 0 && cell.observer !== "client") await observerError(cell, partial, within, () => lab.server.slice(serverFrom).some((line) => line.ev === "drop"));
4560
4586
  else if (http && cell.probe.mode !== "idle") await probe.until(HTTP_DONE, within).catch(() => void 0);
4561
4587
  else await probe.until([
@@ -4757,6 +4783,6 @@ const runCell = async (cell, say = () => {}) => {
4757
4783
  }
4758
4784
  };
4759
4785
  //#endregion
4760
- export { CONVERGE_MS, INJECT, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, NOISE, PUMP, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, callOf, clockOffsetOf, closeOrExit, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, dropFinding, dropOf, endpointAfter, errorsOf, exitOf, fieldOf, fieldsOf, firstUnanswered, goHttpImage, groupOnce, groupVerdictOf, harnessImage, hzOf, infoOf, initiateScript, isHopPlace, judge, lagsOf, leftAfter, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, nodeClientImage, nodeHttpImage, noiseOf, onMonotonic, openGroupLab, openLab, openReplicaSetLab, peersOf, pingsOf, proceduresImage, programLines, quantileOf, reconnectFinding, reconnectOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subscriptionsOf, targetImageOf, templated, toxicBody, verdictOf, viewOf, writesOf };
4786
+ export { CONVERGE_MS, INJECT, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, NOISE, PUMP, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, callOf, clockOffsetOf, closeOrExit, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, dropFinding, dropOf, endpointAfter, errorsOf, exitOf, fieldOf, fieldsOf, firstUnanswered, goHttpImage, groupOnce, groupVerdictOf, harnessImage, hzOf, infoOf, initiateScript, isHopPlace, judge, lagsOf, latestEndOf, leftAfter, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, nodeClientImage, nodeHttpImage, noiseOf, onMonotonic, openGroupLab, openLab, openReplicaSetLab, peersOf, pgClientImage, pingsOf, proceduresImage, programLines, quantileOf, reconnectFinding, reconnectOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subscriptionsOf, targetImageOf, templated, toxicBody, verdictOf, viewOf, writesOf };
4761
4787
 
4762
4788
  //# sourceMappingURL=index.js.map