@mark1russell7/harness 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,9 @@
1
- # The procedures image of the harness: @procedures/client 0.3.0 and ws 8.22.0 on Node 24.21.0. Its WebSocket
1
+ # The procedures image of the harness: @procedures/client 0.7.0 and ws 8.22.0 on Node 24.21.0. Its WebSocket
2
2
  # client runs as a container in the client's network namespace, as the harness's own TCP client
3
3
  # does, and prints the same JSON lines (client-ws.mjs). A target runs the same image with the
4
4
  # script of the procedures server (server-ws.mjs), thus the client and the server share one
5
- # release of the package. The digest of the image and an npm lockfile pin them.
5
+ # release of the package. The digest of the image and an npm lockfile pin them. The lockfile
6
+ # holds one copy of ws, thus @procedures/client/node and the scripts use one ws class.
6
7
  # lab.ts stages the build context: this folder, under harness/image/procedures. The lockfile is
7
8
  # npm-lock.json here, because npm leaves each package-lock.json out of a published tarball.
8
9
  FROM node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1
@@ -4,13 +4,18 @@
4
4
  // --reconnect-attempts N --reconnect-jitter X]
5
5
  // --request-timeout-ms N --connection-timeout-ms N [--hold-ms N] [--go PATH] [--max-s N]
6
6
  //
7
- // A cell's client: the WebSocket client of @procedures/client 0.3.0 (WebSocketTransport) on Node
8
- // 24.21.0. Its sockets are of the global WebSocket of Node, which is undici 7.29.1, or of the
9
- // WebSocket of the ws package (the option `webSocket`): --websocket. Each setting of the transport
10
- // is a flag, and the client has no default: a missing flag is a failure. 0.3.0 takes each time as a
11
- // duration, not a number. Both modes call `harness.echo` first; its reply is `ready`. idle: then
12
- // hold the connection. inflight: then wait for the file --go, and call `harness.echo` (or
13
- // `harness.hold` for --hold-ms) into the fault.
7
+ // A cell's client: the WebSocket client of @procedures/client 0.7.0 (WebSocketTransport) on Node
8
+ // 24.21.0. --websocket gives the class of its sockets:
9
+ // global the WebSocketTransport of the root entry, whose default class is the global WebSocket
10
+ // of Node: undici 7.29.1.
11
+ // ws the WebSocketTransport of the Node entry @procedures/client/node, whose default class
12
+ // is the WebSocket of the ws package (since 0.4.0). The client gives the transport a
13
+ // subclass of that class, from the one copy of ws in the image: it stops when the Node
14
+ // entry resolves another copy.
15
+ // Each setting of the transport is a flag, and the client has no default: a missing flag is a
16
+ // failure. 0.7.0 takes each time as a duration, not a number. Both modes call `harness.echo`
17
+ // first, and its reply is `ready`. idle: then hold the connection. inflight: then wait for the file
18
+ // --go, and call `harness.echo` (or `harness.hold` for --hold-ms) into the fault.
14
19
  //
15
20
  // The lines are those of the harness's TCP client, on CLOCK_MONOTONIC, and the program's own acts:
16
21
  // dial {conn} before each new socket of the transport (conn counts from 1)
@@ -27,9 +32,13 @@
27
32
  // The client gives the transport a subclass of the class of its sockets. The subclass writes a
28
33
  // line, then calls its class: the transport, undici and ws do what they do without it. Its
29
34
  // listeners of `open` and `close` are its own, thus the transport cannot remove them. A drop line
30
- // with no local address is a socket that never opened: a try that failed.
35
+ // with no local address is a socket that never opened: a try that failed. A WebSocket of ws emits
36
+ // no `error` for an error of its socket (lib/websocket.js:1411-1421), thus the subclass reads that
37
+ // error on the socket itself.
31
38
 
32
39
  import { subscribe } from "node:diagnostics_channel";
40
+ import { createRequire } from "node:module";
41
+ import { fileURLToPath } from "node:url";
33
42
  import { WebSocket as WsWebSocket } from "ws";
34
43
  import { bootId, commandLine, emit, now, waitFor } from "./lines.mjs";
35
44
 
@@ -41,7 +50,7 @@ const mode = line.required("--mode");
41
50
  if (mode !== "idle" && mode !== "inflight") line.fail("--mode is idle or inflight");
42
51
  const socketClass = line.required("--websocket");
43
52
  if (socketClass !== "global" && socketClass !== "ws") line.fail("--websocket is global or ws");
44
- /** This function gives a duration of milliseconds, as 0.3.0 takes each time. */
53
+ /** This function gives a duration of milliseconds, as 0.7.0 takes each time. */
45
54
  const ms = (value) => ({ value, unit: "ms" });
46
55
  const heartbeat = line.toggle("--heartbeat")
47
56
  ? {
@@ -67,11 +76,22 @@ const hold = line.flag("--hold-ms") === undefined ? undefined : line.number("--h
67
76
  const go = line.flag("--go") ?? "/tmp/go";
68
77
  const max = Number(line.flag("--max-s") ?? 60) * 1000;
69
78
 
79
+ /** The entry of the transport for each class: the root for the global WebSocket, the Node entry for ws. */
80
+ const ENTRY = socketClass === "ws" ? "@procedures/client/node" : "@procedures/client";
81
+ // the class of the Node entry is the class of the ws that it resolves: it must be the ws of this client
82
+ if (socketClass === "ws") {
83
+ const nodeEntry = fileURLToPath(import.meta.resolve(ENTRY));
84
+ const theirs = createRequire(nodeEntry).resolve("ws");
85
+ const ours = createRequire(import.meta.url).resolve("ws");
86
+ if (theirs !== ours) line.fail(`${ENTRY} resolves ws at ${theirs}, and this client at ${ours}`);
87
+ }
88
+
70
89
  emit("start", {
71
90
  boot_id: bootId(),
72
91
  mode,
73
92
  client: "procedures-ws",
74
- package: "@procedures/client 0.3.0",
93
+ package: "@procedures/client 0.7.0",
94
+ entry: ENTRY,
75
95
  websocket: socketClass,
76
96
  node: process.version,
77
97
  undici: process.versions.undici ?? null,
@@ -120,10 +140,14 @@ const stamped = (Base) => {
120
140
  super(...args);
121
141
  const socket = { conn, local: undefined, dropped: false };
122
142
  sockets.set(this, socket);
123
- // a socket of ws gives its local address at its open; undici gives it in a diagnostics channel
143
+ // a socket of ws gives its local address at its open, and undici gives it in a diagnostics
144
+ // channel. ws keeps the error of its socket to itself, thus the client reads it there
124
145
  this.addEventListener("open", () => {
125
146
  const raw = this._socket;
126
147
  if (raw?.localAddress !== undefined) opened(socket, `${raw.localAddress}:${raw.localPort}`);
148
+ raw?.on?.("error", (error) => {
149
+ pending = { errno: error?.code ?? error?.name ?? "unknown", at: now() };
150
+ });
127
151
  });
128
152
  this.addEventListener("error", (event) => {
129
153
  const code = event?.error?.code;
@@ -174,7 +198,8 @@ setTimeout(() => {
174
198
  process.exit(0);
175
199
  }, max);
176
200
 
177
- const { Client, WebSocketTransport } = await import("@procedures/client");
201
+ const { Client } = await import("@procedures/client");
202
+ const { WebSocketTransport } = await import(ENTRY);
178
203
 
179
204
  let first;
180
205
  const firstOpen = new Promise((resolve) => {
@@ -6,14 +6,14 @@
6
6
  "": {
7
7
  "name": "loom-harness-procedures",
8
8
  "dependencies": {
9
- "@procedures/client": "0.3.0",
9
+ "@procedures/client": "0.7.0",
10
10
  "ws": "8.22.0"
11
11
  }
12
12
  },
13
13
  "node_modules/@mark1russell7/algebra": {
14
- "version": "0.5.0",
15
- "resolved": "https://registry.npmjs.org/@mark1russell7/algebra/-/algebra-0.5.0.tgz",
16
- "integrity": "sha512-rNKCWvZBGDUYtA1BOPftPY7+zG6LWdISWrjkZwsHf3IS4bfz6JCeWVpJ0LV/vEvNRz1OnVjh9WgwoEUn1Y4cCQ==",
14
+ "version": "0.7.1",
15
+ "resolved": "https://registry.npmjs.org/@mark1russell7/algebra/-/algebra-0.7.1.tgz",
16
+ "integrity": "sha512-+nimxDO9pEOwNxVJ7/F4vOAba8CnytN/DbRJKN7FxF/Q+g5Yy2u6e+3bbjiuq0eH9vCWwc3WlIuyDJhSFfuNow==",
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
19
  "@mark1russell7/quantity": "^0.2.0"
@@ -43,12 +43,12 @@
43
43
  }
44
44
  },
45
45
  "node_modules/@mark1russell7/service": {
46
- "version": "0.4.0",
47
- "resolved": "https://registry.npmjs.org/@mark1russell7/service/-/service-0.4.0.tgz",
48
- "integrity": "sha512-Zby/XIGWTWax927/pkQgeY9ljlzLmBkUnTlDcpGDKnNpEmXhM0WpnK948hnDqvUd2bwXM3/CcM2cYQDAaTA+2A==",
46
+ "version": "0.4.1",
47
+ "resolved": "https://registry.npmjs.org/@mark1russell7/service/-/service-0.4.1.tgz",
48
+ "integrity": "sha512-9lxVPWJ/oZ1wfyl0NhZ7WX0w53L2HwG++78XmWr3gh0IhCiMZSJi3cuvgiRfF/uS2uf3u7TGXmb2MctEZDtF3g==",
49
49
  "license": "MIT",
50
50
  "dependencies": {
51
- "@mark1russell7/algebra": "^0.5.0",
51
+ "@mark1russell7/algebra": "^0.7.0",
52
52
  "@mark1russell7/quantity": "^0.2.0"
53
53
  },
54
54
  "engines": {
@@ -65,20 +65,17 @@
65
65
  }
66
66
  },
67
67
  "node_modules/@procedures/client": {
68
- "version": "0.3.0",
69
- "resolved": "https://registry.npmjs.org/@procedures/client/-/client-0.3.0.tgz",
70
- "integrity": "sha512-DzOOZIhv1DH87zv3Je/D4gg+Jou386i69hPkQU5bz8Gs1vRzZoTrBcb0kojNMS4Ex1cwDxXYr0GDxVmlpVuEnA==",
68
+ "version": "0.7.0",
69
+ "resolved": "https://registry.npmjs.org/@procedures/client/-/client-0.7.0.tgz",
70
+ "integrity": "sha512-eBsvnfEYBJ7yrNpeX3Wi847Q9VlZMc8K92ydFfYIoZDwdfmj/Fxzu/IWQMfpCq7Fvof7Xt0m0Wk6YbcFZx8Frg==",
71
71
  "license": "MIT",
72
72
  "dependencies": {
73
- "@mark1russell7/algebra": "^0.5.0",
73
+ "@mark1russell7/algebra": "^0.7.0",
74
74
  "@mark1russell7/quantity": "^0.2.0",
75
- "@mark1russell7/service": "^0.4.0",
75
+ "@mark1russell7/service": "^0.4.1",
76
76
  "ws": "^8.18.3",
77
77
  "zod": "^4.6.5"
78
78
  },
79
- "bin": {
80
- "client-registry": "dist/procedures/cli/index.js"
81
- },
82
79
  "engines": {
83
80
  "node": ">=22"
84
81
  },
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@procedures/client": "0.3.0",
6
+ "@procedures/client": "0.7.0",
7
7
  "ws": "8.22.0"
8
8
  }
9
9
  }
@@ -1,6 +1,6 @@
1
1
  // server-ws --port N --tcp-keepalive off|N --heartbeat off|on [--heartbeat-interval-ms N --heartbeat-misses N]
2
2
  //
3
- // A cell's target: the procedures server of @procedures/client 0.3.0 (Server and
3
+ // A cell's target: the procedures server of @procedures/client 0.7.0 (Server and
4
4
  // WebSocketServerTransport) on the http.Server of Node 24.21.0, at the path /ws. It has two
5
5
  // procedures: `harness.echo` answers at once, and `harness.hold` answers after `ms` milliseconds
6
6
  // (a held reply, as BLPOP is for Redis).
@@ -10,7 +10,9 @@
10
10
  //
11
11
  // --heartbeat is the heartbeat of the server (roadmap R3): a WebSocket ping to each connection at
12
12
  // each interval, and terminate() for a connection that missed `misses` pongs. Each setting is a
13
- // flag, and the server has no default. 0.3.0 takes each time as a duration, not a number.
13
+ // flag, and the server has no default. 0.7.0 takes each time as a duration, not a number. One
14
+ // setInterval of the transport, from its start, beats for all connections: the first ping of a
15
+ // connection comes at the next beat after its accept (transport.js:94-112, :147-151).
14
16
  //
15
17
  // Each event is a JSON line on the clock of the clients: a connection (`accepted`), a request,
16
18
  // a reply, and the close of a connection. The `ws` package does the WebSocket of the server. Its
@@ -87,7 +89,7 @@ httpServer.listen(port, "0.0.0.0", () =>
87
89
  port,
88
90
  boot_id: bootId(),
89
91
  node: process.version,
90
- package: "@procedures/client 0.3.0",
92
+ package: "@procedures/client 0.7.0",
91
93
  tcpKeepalive: keepAliveMs,
92
94
  heartbeat,
93
95
  }),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/harness",
4
- "version": "0.10.0",
4
+ "version": "0.11.1",
5
5
  "description": "A fault-injection harness for connection timers: a cell runs a client against a stock target in Docker, through a topology of routers and proxies, a replicated group under Sentinels or a replica set, injects a fault, measures the kernel's own records and judges a prediction. HTTP clients of Go, Node and undici race the keep-alive of a Node server, and the MongoDB driver meets the faults of a replica set. The WebSocket client of @procedures/client drops a silent connection on its own heartbeat, and the harness times that detection and the reconnect from the program's lines and the kernel's stamps.",
6
6
  "keywords": [
7
7
  "fault-injection",