@mark1russell7/harness 0.11.14 → 0.11.16

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
@@ -168,6 +168,10 @@ The harness ships ten images, each as a shipped source (`ImageSource`). The tag
168
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`.
169
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.
170
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.
171
+ - **The RabbitMQ client image** (`image/amqp`) is amqplib 2.2.0 on Node 24.21.0. The digest of the image and an npm lockfile pin them. The probe is `{ driver: { name: "amqp" } }`.
172
+ - `client-amqp.mjs` opens one connection and one channel. Each operation is an RPC of the channel: the first declares a queue that the broker names, and each later one declares it passively (checkQueue). The queue is exclusive, because RabbitMQ 4.3 refuses a transient queue that is not.
173
+ - The options of the cell (`Cell.driverOptions`) give `heartbeat` in seconds, the user and its password (guest at the defaults), and the socket options of amqplib.
174
+ - It prints the lines of the pg client on the same clock. A `connection` line gives each event of the connection, and a `channel` line each event of the channel. The errno of a failure is the error of the connection when it has one.
171
175
  - **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:
172
176
  - 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.
173
177
  - 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).
@@ -213,6 +217,10 @@ ioredis sets the keepalive of its socket itself, after it connects. Thus a cell
213
217
  - **A toxic** (`toxic`, `Cell.toxic`) is one toxic of Toxiproxy, added through its HTTP API by `inject http`, with monotonic stamps around the request. Its name is `fault`, and the harness removes it after the run. A cell whose fault is another one adds its toxic first: a slow close, then the close of the server.
214
218
  - **A close of the server** (`server-close`) makes the server close the connection of each client, with `Target.close`: `CLIENT KILL` for Redis.
215
219
  - **A stall of the application** (`app-stall`) injects nothing. The server process holds the reply, because the request asks for it (`HttpProbe.hold`), and its kernel stays up.
220
+ - **An event at a proxy** (`hop-event`, `Cell.event`) is a signal to the program of the proxy, or a query on its console. Examples are a reload of the configuration and PAUSE on the console of PgBouncer.
221
+ - A signal goes to each process of `ProxyHop.program`. A sidecar in the network and process namespaces of the proxy sends it (`inject signal NAME HUP`). The lab starts that sidecar at the first signal.
222
+ - A query goes from the injector of the proxy to its console on 127.0.0.1, as a user that the console trusts (`inject query`). The injector speaks the simple query of the PostgreSQL protocol. Its stamps bracket the query and its ReadyForQuery. After the run, the harness sends the `undo` of the query, for example RESUME after PAUSE.
223
+ - Without `inFlight`, the event comes first, and the request meets it. With `inFlight`, the request goes first, and the event comes while it is in flight. A run whose order is not that order is void.
216
224
  - **A fault on a group** acts on the part that `Cell.faultAt` names by its role:
217
225
  - A black hole (`blackhole`) drops each segment between the part and each peer. A reset (`rst`) answers each segment from a peer with a reset, thus the part refuses each connection.
218
226
  - A partial partition (`partition`) is a black hole between the part and some of its peers (`GroupPlace.cuts`). `group` cuts the part from the other members and the Sentinels, and `clients` cuts it from the client.
@@ -243,6 +251,7 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
243
251
  - **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.
244
252
  - **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.
245
253
  - **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.
254
+ - **A heartbeat of a driver.** The receive check of a heartbeat counts the bytes of the peer, not an ACK. Thus its band counts from the last segment with data that the socket received before the end of the band (`last-data`). An idle client of amqplib sends its own heartbeat frames. The ACK of the broker for one of them can come after the last data of the broker.
246
255
  - **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
256
  - A connect timer counts from the change of that socket to SYN_SENT (`syn-sent`).
248
257
  - 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.
@@ -30,6 +30,9 @@ import { KernelRecord } from "@mark1russell7/prober";
30
30
  * target (`Target.close`): `CLIENT KILL` in Redis.
31
31
  * - `none` injects nothing. A calibration cell measures the path itself: for example the time of a
32
32
  * proxy between its two sockets (`Cell.calibrates`).
33
+ * - `hop-event` is an event at a proxy of the topology (`Cell.event`): a signal to its program, or a
34
+ * query on its console. Examples are a reload of the configuration (SIGHUP) and PAUSE on the
35
+ * console of PgBouncer.
33
36
  *
34
37
  * A fault at a router of the topology (`Cell.faultAt`) acts on what the router forwards toward
35
38
  * one side. It is a black hole, a reset or an ICMP unreachable. It can also be a simplex drop, an
@@ -51,7 +54,7 @@ import { KernelRecord } from "@mark1russell7/prober";
51
54
  * This fault is of a replica set (`Cell.replicaSet`):
52
55
  * - `step-down` sends `replSetStepDown` to the primary, as a client sends a command.
53
56
  */
54
- type Fault = "blackhole" | "freeze-process" | "rst" | "stall" | "fin" | "unreachable" | "simplex" | "mtu-blackhole" | "netem" | "middlebox-forget" | "proxy-idle" | "toxic" | "server-close" | "partition" | "planned-failover" | "app-stall" | "step-down" | "none";
57
+ type Fault = "blackhole" | "freeze-process" | "rst" | "stall" | "fin" | "unreachable" | "simplex" | "mtu-blackhole" | "netem" | "middlebox-forget" | "proxy-idle" | "toxic" | "server-close" | "partition" | "planned-failover" | "app-stall" | "step-down" | "hop-event" | "none";
55
58
  /**
56
59
  * A middlebox form tells what a router does with a flow that it forgot, when a late packet comes:
57
60
  * - `reset-both` sends a reset to the two ends when it forgets the flow.
@@ -115,7 +118,31 @@ interface ProxyHop {
115
118
  readonly sysctls?: Readonly<Record<string, string>>;
116
119
  /** These are the settings of the proxy, for the manifest: for example its idle timeouts. */
117
120
  readonly settings?: Readonly<Record<string, string>>;
121
+ /** This name is the process of the proxy, as /proc/PID/comm holds it, for a signal of a `hop-event`. */
122
+ readonly program?: string;
118
123
  }
124
+ /**
125
+ * An event at a proxy hop (`hop-event`). A signal goes to each process of the program of the hop
126
+ * (`ProxyHop.program`). A query goes to the console of the hop from its own namespace, as a user
127
+ * that the console trusts. Its `undo` lifts it after the run. Without `inFlight`, the event
128
+ * comes first, and the request of the client meets it. With `inFlight`, the request goes first,
129
+ * and the event comes while it is in flight.
130
+ */
131
+ type HopEvent = {
132
+ readonly hop: string;
133
+ readonly signal: "HUP";
134
+ readonly inFlight?: true;
135
+ } | {
136
+ readonly hop: string;
137
+ readonly console: {
138
+ readonly port: number;
139
+ readonly user: string;
140
+ readonly database: string;
141
+ readonly query: string;
142
+ readonly undo?: string;
143
+ };
144
+ readonly inFlight?: true;
145
+ };
119
146
  /**
120
147
  * A hop is one box between the client and the target. Each hop is on two networks: the network
121
148
  * of the side of the client, and the network of the side of the target.
@@ -237,7 +264,7 @@ interface Toxic {
237
264
  * A connect timer counts from the change of its socket to SYN_SENT (`syn-sent`). A pooler connects
238
265
  * to the server by itself, thus the kernel stamps that connect, and no line of the client does.
239
266
  */
240
- type Reference = "last-received" | "first-retransmission" | "request-sent" | "peer-closed" | "fault" | "client-start" | "ping-sent" | "first-sent" | "syn-sent";
267
+ type Reference = "last-received" | "last-data" | "first-retransmission" | "request-sent" | "peer-closed" | "fault" | "client-start" | "ping-sent" | "first-sent" | "syn-sent";
241
268
  /**
242
269
  * This prediction tells when a connection moves again after a stall. That occurs at the first
243
270
  * firing of its retransmission timer after the stall stops, and its reply comes back one round
@@ -587,7 +614,7 @@ type HarnessImage = "procedures";
587
614
  * A shipped image is an image that the harness ships, by its name. Its build context is in the
588
615
  * package of the harness, and in the package of the prober for `harness`.
589
616
  */
590
- type Shipped = "harness" | "node" | "go-http" | "node-http" | "mongo" | "pg" | "hikari" | "procedures" | "router" | "pgbouncer" | "pgpool";
617
+ type Shipped = "harness" | "node" | "go-http" | "node-http" | "mongo" | "pg" | "hikari" | "amqp" | "procedures" | "router" | "pgbouncer" | "pgpool";
591
618
  /**
592
619
  * An image source tells where an image comes from, as data. The harness resolves each kind to an
593
620
  * image that it can start:
@@ -646,7 +673,7 @@ interface Subject {
646
673
  * (`http-agent`) and an HTTP/1.1 `Client` of undici (`undici`). The MongoDB driver is `mongodb`, node-postgres is `pg`, HikariCP over pgjdbc is `hikari`, and
647
674
  * the WebSocket client of `@procedures/client` is `procedures-ws`.
648
675
  */
649
- type Client = "tcp" | "ioredis" | "go-http" | "http-agent" | "undici" | "mongodb" | "pg" | "hikari" | "procedures-ws";
676
+ type Client = "tcp" | "ioredis" | "go-http" | "http-agent" | "undici" | "mongodb" | "pg" | "hikari" | "amqp" | "procedures-ws";
650
677
  /**
651
678
  * A WebSocket probe is the workload and the settings of the WebSocket client of
652
679
  * `@procedures/client` (`procedures-ws`). The cell gives each setting, thus the client takes
@@ -786,6 +813,8 @@ interface Cell {
786
813
  readonly name: "hikari";
787
814
  readonly query?: string;
788
815
  readonly inflight?: string;
816
+ } | {
817
+ readonly name: "amqp";
789
818
  };
790
819
  /** This workload is the workload of an HTTP client (`Cell.client` is an HTTP client). The client then ignores `keepalive` and `userTimeout`. */
791
820
  readonly http?: HttpProbe;
@@ -828,6 +857,8 @@ interface Cell {
828
857
  readonly idleBefore?: Quantity;
829
858
  /** This toxic is the toxic of a `toxic` fault, which the Toxiproxy hop gets. */
830
859
  readonly toxic?: Toxic;
860
+ /** This event is the event of a `hop-event` fault, at a proxy of the topology. */
861
+ readonly event?: HopEvent;
831
862
  /** This image, or this client of the harness, replaces the TCP client of the harness for each run. An HTTP client is one. */
832
863
  readonly client?: ClientImage | Client;
833
864
  /**
@@ -1022,5 +1053,5 @@ interface RunTopology {
1022
1053
  }>>;
1023
1054
  }
1024
1055
  //#endregion
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 };
1026
- //# sourceMappingURL=cell-COlAmUZ-.d.ts.map
1056
+ export { WsProbe as $, Reconnects as A, RunGroup as B, MiddleboxForm as C, ProbeEvent as D, Prediction as E, Replies as F, Shipped as G, RunTopology as H, RequestPrediction as I, StageBand as J, ShippedImage as K, RouterHop as L, Reference as M, ReplicaSet as N, ProxyHop as O, ReplicaSetStage as P, Toxic as Q, RttRange as R, MiddleboxExpectation as S, PinnedImage as T, SentinelEvent as U, RunPart as V, SentinelStage as W, Target as X, Subject as Y, Topology as Z, HttpProbe as _, DriverBand as a, LineMatch as b, FaultPlace as c, GroupPrediction as d, isHopPlace as et, GroupRole as f, HopPlace as g, HopEvent as h, ClientImage as i, Recovers as j, ProxyTime as k, Group as l, Hop as m, Cell as n, DriverStep as o, HarnessImage as p, Stage as q, Client as r, Fault as s, BuildImage as t, latestEndOf as tt, GroupPlace as u, ImageSource as v, Observer as w, MiddleboxEvent as x, KernelRecord as y, Run as z };
1057
+ //# sourceMappingURL=cell-B6YHtKFb.d.ts.map
@@ -72,6 +72,11 @@ const SHIPPED = {
72
72
  dockerfile: "harness/image/hikari/Dockerfile",
73
73
  from: ["harness/image/hikari"]
74
74
  },
75
+ amqp: {
76
+ name: "loom-harness-amqp",
77
+ dockerfile: "harness/image/amqp/Dockerfile",
78
+ from: ["harness/image/amqp"]
79
+ },
75
80
  procedures: {
76
81
  name: "loom-harness-procedures",
77
82
  dockerfile: "harness/image/procedures/Dockerfile",
@@ -160,6 +165,10 @@ const SHIPPED_CLIENTS = {
160
165
  "ClientHikari"
161
166
  ]
162
167
  },
168
+ amqp: {
169
+ image: shipped("amqp"),
170
+ command: ["node", "/opt/harness/amqp/client-amqp.mjs"]
171
+ },
163
172
  "procedures-ws": {
164
173
  image: SUBJECTS.procedures.image,
165
174
  command: SUBJECTS.procedures.programs["client"]
@@ -434,4 +443,4 @@ const targetImageOf = (target) => imageOf(targetSourceOf(target));
434
443
  //#endregion
435
444
  export { targetImageOf as C, unpinnedBases as E, subjectProblems as S, unlockedManifests as T, planOf as _, SHIPPED_NAMES as a, routerImage as b, harnessImage as c, imageProblems as d, inContext as f, pgClientImage as g, nodeHttpImage as h, SHIPPED_CLIENTS as i, hikariClientImage as l, nodeClientImage as m, PINNED as n, SUBJECTS as o, mongoClientImage as p, REPOSITORY as r, goHttpImage as s, CLIENT_NAMES as t, imageOf as u, proceduresImage as v, targetSourceOf as w, subjectClient as x, programOf as y };
436
445
 
437
- //# sourceMappingURL=image-DIJ1AFg_.js.map
446
+ //# sourceMappingURL=image-RM60Paqe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-RM60Paqe.js","names":[],"sources":["../src/image.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { copyFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, posix, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport type { BuildImage, Client, ClientImage, ImageSource, Shipped, ShippedImage, Subject, Target } from \"./cell.ts\";\n\n/**\n * This module resolves the source of each image (`ImageSource`) to an image that a lab can start.\n * A pinned image is its own name. A build context, or an image that the harness ships, becomes a\n * build with a tag from its content. Each image of the harness is a shipped source in one table.\n * Thus the procedures image of a subject is data, as the image of a caller is.\n */\n\n/**\n * These are the roots of the two packages that the shipped images read: this package, and the\n * prober that Node resolves for it. In a workspace, they are two folders side by side. From npm,\n * the prober is a link, and a Docker build context does not follow a link. Thus each image builds\n * from a context that the harness copies from the two roots.\n */\nconst ROOTS: Readonly<Record<string, string>> = {\n harness: fileURLToPath(new URL(\"..\", import.meta.url)),\n prober: dirname(realpathSync(createRequire(import.meta.url).resolve(\"@mark1russell7/prober/package.json\"))),\n};\n\n/** This function gives the path of a part of a shipped image, which is `<package>/<path>` in the context. */\nconst pathOf = (part: string): string => {\n const [root, ...rest] = part.split(\"/\");\n const base = ROOTS[root!];\n if (base === undefined) throw new Error(`${part} is in no package that the images read`);\n return join(base, ...rest);\n};\n\n/** A shipped context is the build context of a shipped image: its Dockerfile, and each file or folder that the Dockerfile reads, as `<package>/<path>`. */\ninterface ShippedContext {\n readonly name: string;\n readonly dockerfile: string;\n readonly from: readonly string[];\n}\n\n/** These are the build contexts of the images that the harness ships, by their names. */\nconst SHIPPED: Readonly<Record<Shipped, ShippedContext>> = {\n // the prober, the TCP client, the injector and the namespace admin\n harness: {\n name: \"loom-harness\",\n dockerfile: \"harness/image/Dockerfile\",\n from: [\"harness/image/Dockerfile\", \"harness/image/native\", \"prober/bpf\", \"prober/native\", \"prober/fixtures\"],\n },\n // the Node client: ioredis 6.0.0 on Node 24.21.0\n node: { name: \"loom-harness-node\", dockerfile: \"harness/image/node/Dockerfile\", from: [\"harness/image/node\"] },\n // the Go HTTP client: the http.Transport of Go 1.27.2\n \"go-http\": { name: \"loom-harness-go-http\", dockerfile: \"harness/image/go-http/Dockerfile\", from: [\"harness/image/go-http\"] },\n // the Node HTTP clients: the http.Agent of Node 24.21.0, and undici 8.12.2\n \"node-http\": { name: \"loom-harness-node-http\", dockerfile: \"harness/image/node-http/Dockerfile\", from: [\"harness/image/node-http\"] },\n // the MongoDB client: the driver mongodb 7.7.0 on Node 24.21.0, and its step-down script\n mongo: { name: \"loom-harness-mongo\", dockerfile: \"harness/image/mongo/Dockerfile\", from: [\"harness/image/mongo\"] },\n // the PostgreSQL client: node-postgres (pg 8.23.1 with pg-pool 3.14.0) on Node 24.21.0\n pg: { name: \"loom-harness-pg\", dockerfile: \"harness/image/pg/Dockerfile\", from: [\"harness/image/pg\"] },\n // the client of HikariCP 7.1.0 over pgjdbc 42.7.13 on Temurin 21.0.12.1: its jars by their SHA-256\n hikari: { name: \"loom-harness-hikari\", dockerfile: \"harness/image/hikari/Dockerfile\", from: [\"harness/image/hikari\"] },\n // the RabbitMQ client: amqplib 2.2.0 on Node 24.21.0\n amqp: { name: \"loom-harness-amqp\", dockerfile: \"harness/image/amqp/Dockerfile\", from: [\"harness/image/amqp\"] },\n // @procedures/client 0.7.0 on Node 24.21.0: its WebSocket client, and the script of its server\n procedures: { name: \"loom-harness-procedures\", dockerfile: \"harness/image/procedures/Dockerfile\", from: [\"harness/image/procedures\"] },\n // the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow\n router: { name: \"loom-harness-router\", dockerfile: \"harness/image/router/Dockerfile\", from: [\"harness/image/router\"] },\n // PgBouncer 1.26.1, a pooler hop: built from its release tarball, by its SHA-256\n pgbouncer: { name: \"loom-harness-pgbouncer\", dockerfile: \"harness/image/pgbouncer/Dockerfile\", from: [\"harness/image/pgbouncer\"] },\n // Pgpool-II 4.7.3, a pooler hop: built from the source of its tag, by its SHA-256\n pgpool: { name: \"loom-harness-pgpool\", dockerfile: \"harness/image/pgpool/Dockerfile\", from: [\"harness/image/pgpool\"] },\n};\n\n/** These are the names of the images that the harness ships. */\nexport const SHIPPED_NAMES: readonly [Shipped, ...Shipped[]] = Object.keys(SHIPPED) as [Shipped, ...Shipped[]];\n\n/** This image is the stock base of the Node images of the harness: Node 24.21.0 on Alpine, pinned by digest. */\nconst NODE_BASE = \"node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1\";\n\n/**\n * These are the subjects that the harness ships, by their names. `procedures` is\n * `@procedures/client` on Node: its WebSocket client (`client-ws.mjs`) and its server\n * (`server-ws.mjs`), from one image. Thus the client and the server use one release.\n */\nexport const SUBJECTS: Readonly<Record<\"procedures\", Subject>> = {\n procedures: {\n name: \"procedures\",\n image: { kind: \"shipped\", name: \"procedures\" },\n base: NODE_BASE,\n programs: {\n client: [\"node\", \"/opt/harness/procedures/client-ws.mjs\"],\n server: [\"node\", \"/opt/harness/procedures/server-ws.mjs\"],\n },\n },\n};\n\n/** This function gives the source of a shipped image. */\nconst shipped = (name: Shipped): ImageSource => ({ kind: \"shipped\", name });\n\n/**\n * These are the clients that the harness ships, by their names (`Client`): the image of each, and\n * its command. A cell that names a client gets this image and this command.\n */\nexport const SHIPPED_CLIENTS: Readonly<Record<Client, ClientImage>> = {\n tcp: { image: shipped(\"harness\"), command: [\"/opt/harness/client\"] },\n ioredis: { image: shipped(\"node\"), command: [\"node\", \"/opt/harness/node/client-ioredis.mjs\"] },\n \"go-http\": { image: shipped(\"go-http\"), command: [\"/opt/harness/client-go\"] },\n \"http-agent\": { image: shipped(\"node-http\"), command: [\"node\", \"/opt/harness/node-http/client-agent.mjs\"] },\n undici: { image: shipped(\"node-http\"), command: [\"node\", \"/opt/harness/node-http/client-undici.mjs\"] },\n mongodb: { image: shipped(\"mongo\"), command: [\"node\", \"/opt/harness/mongo/client-mongo.mjs\"] },\n pg: { image: shipped(\"pg\"), command: [\"node\", \"/opt/harness/pg/client-pg.mjs\"] },\n hikari: {\n image: shipped(\"hikari\"),\n command: [\"java\", \"-Dslf4j.internal.verbosity=ERROR\", \"-cp\", \"/opt/harness/hikari/classes:/opt/harness/hikari/lib/*\", \"ClientHikari\"],\n },\n amqp: { image: shipped(\"amqp\"), command: [\"node\", \"/opt/harness/amqp/client-amqp.mjs\"] },\n \"procedures-ws\": { image: SUBJECTS.procedures.image, command: SUBJECTS.procedures.programs[\"client\"]! },\n};\n\n/** These are the names of the clients that the harness ships. */\nexport const CLIENT_NAMES: readonly [Client, ...Client[]] = Object.keys(SHIPPED_CLIENTS) as [Client, ...Client[]];\n\n/** This function gives the command of a program of a subject by its role. It fails when the subject has no such program. */\nexport const programOf = (subject: Subject, role: string): readonly string[] => {\n const program = Object.hasOwn(subject.programs, role) ? subject.programs[role] : undefined;\n if (program === undefined) throw new Error(`the subject ${subject.name} has no program ${role}`);\n return program;\n};\n\n/**\n * This function gives the client of a subject for a cell (`Cell.client`): the image of the subject,\n * with the command of a program. A client that the harness ships goes by its name, thus its records\n * stay the same.\n */\nexport const subjectClient = (subject: Subject, role = \"client\"): Client | ClientImage => {\n const client: ClientImage = { image: subject.image, command: programOf(subject, role) };\n return CLIENT_NAMES.find((name) => isDeepStrictEqual(SHIPPED_CLIENTS[name], client)) ?? client;\n};\n\n/** This pattern matches an image pinned by digest: a name, a tag or none, then `@sha256:` and 64 hex digits. */\nexport const PINNED: RegExp = /^[a-z0-9][A-Za-z0-9._\\-/:]*@sha256:[0-9a-f]{64}$/;\n/** This pattern matches the repository of a tag that the harness builds. Its parts are lower case, with `/` between them. */\nexport const REPOSITORY: RegExp = /^[a-z0-9]+(?:[._-]+[a-z0-9]+)*(?:\\/[a-z0-9]+(?:[._-]+[a-z0-9]+)*)*$/;\n\n/** This function tells if a path stays in its context: relative, with `/` between its parts, and no `..`. */\nexport const inContext = (path: string): boolean =>\n typeof path === \"string\" &&\n path.length > 0 &&\n !path.startsWith(\"/\") &&\n !path.includes(\"\\\\\") &&\n !/^[A-Za-z]:/.test(path) &&\n !path.split(\"/\").includes(\"..\");\n\n/** This function tells if a value is a command: an array of one text or more. */\nconst isCommand = (value: unknown): boolean =>\n Array.isArray(value) && value.length > 0 && value.every((word) => typeof word === \"string\" && word.length > 0);\n\n/**\n * This function gives the problems of the source of an image, as data. A pinned image names a\n * digest. A build names a repository, a context, and paths that stay in the context. A shipped image\n * is one that the harness ships.\n */\nexport const imageProblems = (source: ImageSource): string[] => {\n const kind = (source as { readonly kind?: unknown } | null)?.kind;\n if (kind === \"pinned\") {\n const { image } = source as { readonly image?: unknown };\n return typeof image === \"string\" && PINNED.test(image) ? [] : [`${String(image)} is not pinned by digest: name@sha256:<64 hex digits>`];\n }\n if (kind === \"shipped\") {\n const { name } = source as { readonly name?: unknown };\n return typeof name === \"string\" && Object.hasOwn(SHIPPED, name) ? [] : [`the harness ships no image ${String(name)}: ${SHIPPED_NAMES.join(\", \")}`];\n }\n if (kind === \"build\") {\n const build = source as Partial<BuildImage>;\n const problems: string[] = [];\n if (typeof build.name !== \"string\" || !REPOSITORY.test(build.name)) problems.push(`${String(build.name)} is not the name of a repository`);\n if (typeof build.context !== \"string\" || build.context.length === 0) problems.push(\"the build names no context\");\n if (build.files !== undefined && (!Array.isArray(build.files) || build.files.length === 0)) problems.push(\"the build names no files\");\n for (const path of [build.dockerfile ?? \"Dockerfile\", ...(Array.isArray(build.files) ? build.files : [])])\n if (!inContext(path)) problems.push(`${String(path)} is not a path in the context`);\n return problems;\n }\n return [`${JSON.stringify(kind)} is not a kind of image source: pinned, build or shipped`];\n};\n\n/**\n * This function gives the problems of a subject, as data. A subject has a name and a base that a\n * digest pins. Its image source has no problems, and each of its programs is a command.\n */\nexport const subjectProblems = (subject: Subject): string[] => {\n const programs = (subject as { readonly programs?: unknown }).programs;\n const roles = typeof programs === \"object\" && programs !== null ? Object.entries(programs) : [];\n return [\n ...(typeof subject.name === \"string\" && subject.name.length > 0 ? [] : [\"the subject has no name\"]),\n ...(typeof subject.base === \"string\" && PINNED.test(subject.base) ? [] : [`the base ${String(subject.base)} is not pinned by digest`]),\n ...imageProblems(subject.image),\n ...(roles.length === 0 ? [\"the subject has no programs\"] : []),\n ...roles.flatMap(([role, command]) => (isCommand(command) ? [] : [`the program ${role} is not a command`])),\n ];\n};\n\n/**\n * This function gives each `FROM` of a Dockerfile that no digest pins. A `FROM` of an earlier stage\n * by its name, or of `scratch`, needs no digest. A base from an `ARG` has no digest that the harness\n * can read, thus it is in the list too.\n */\nexport const unpinnedBases = (dockerfile: string): string[] => {\n const stages = new Set<string>([\"scratch\"]);\n const found: string[] = [];\n for (const line of dockerfile.replace(/\\\\\\r?\\n/g, \" \").split(/\\r?\\n/)) {\n const from = /^\\s*FROM\\s+(.+)$/i.exec(line)?.[1];\n if (from === undefined) continue;\n const [image, as, stage] = from.split(/\\s+/).filter((word) => !word.startsWith(\"--\"));\n if (image === undefined) continue;\n if (!stages.has(image.toLowerCase()) && !PINNED.test(image)) found.push(image);\n if (as?.toLowerCase() === \"as\" && stage !== undefined) stages.add(stage.toLowerCase());\n }\n return found;\n};\n\n/** These are the lockfiles of each manifest of packages. A manifest without a lockfile installs other packages each time. */\nconst LOCKFILES: Readonly<Record<string, readonly string[]>> = {\n \"package.json\": [\"package-lock.json\", \"npm-lock.json\", \"npm-shrinkwrap.json\", \"pnpm-lock.yaml\", \"yarn.lock\", \"bun.lock\"],\n \"Cargo.toml\": [\"Cargo.lock\"],\n};\n\n/**\n * This function gives each manifest of packages in a context (`package.json`, `Cargo.toml`) that\n * has no lockfile in its folder or in a folder above it. The paths are the paths in the context.\n */\nexport const unlockedManifests = (paths: readonly string[]): string[] => {\n const present = new Set(paths);\n return paths.filter((path) => {\n const parts = path.split(\"/\");\n const locks = LOCKFILES[parts.at(-1)!];\n if (locks === undefined) return false;\n for (let depth = parts.length - 1; depth >= 0; depth -= 1) {\n const folder = parts.slice(0, depth).join(\"/\");\n if (locks.some((lock) => present.has(folder === \"\" ? lock : `${folder}/${lock}`))) return false;\n }\n return true;\n });\n};\n\n/**\n * An image plan tells how the harness gets the image of a source. It is a pull of an image by its\n * name, or a build of a context. A build has its tag, its Dockerfile, and each file with its path.\n */\nexport type ImagePlan =\n | { readonly kind: \"pull\"; readonly image: string }\n | {\n readonly kind: \"build\";\n readonly tag: string;\n readonly dockerfile: string;\n readonly files: ReadonlyArray<{ readonly file: string; readonly at: string }>;\n };\n\n/** This function gives each file under a path, in a stable sequence, but not the output of a local build. */\nconst sources = (path: string): string[] => {\n if (statSync(path).isFile()) return [path];\n return readdirSync(path, { withFileTypes: true })\n .filter((entry) => entry.name !== \"target\" && entry.name !== \"node_modules\")\n .flatMap((entry) => sources(join(path, entry.name)))\n .sort();\n};\n\n/** A part is a file or a folder of a context: its path on the machine, and its path in the context. */\ninterface Part {\n readonly path: string;\n readonly at: string;\n}\n\n/** This function gives the name, the Dockerfile and the parts of the context of a build or of a shipped image. */\nconst contextOf = (source: BuildImage | ShippedImage): { name: string; dockerfile: string; parts: Part[] } => {\n if (source.kind === \"shipped\") {\n const { name, dockerfile, from } = SHIPPED[source.name];\n return { name, dockerfile, parts: from.map((part) => ({ path: pathOf(part), at: part })) };\n }\n const root = resolve(source.context);\n return {\n name: source.name,\n dockerfile: posix.normalize(source.dockerfile ?? \"Dockerfile\"),\n parts: (source.files ?? [\".\"]).map((file) => ({ path: resolve(root, file), at: file === \".\" ? \"\" : file })),\n };\n};\n\n/** This function gives a text for a source in a message. */\nconst labelOf = (source: ImageSource): string =>\n source.kind === \"pinned\" ? source.image : `the ${source.kind === \"build\" ? \"build\" : \"shipped image\"} ${source.name}`;\n\nconst shippedPlans = new Map<Shipped, ImagePlan>();\n\n/**\n * This function gives the plan of the image of a source. A stock image by its name, or a pinned\n * image, is a pull. A build or a shipped image is a build. Its tag is its name, then 12 digits of a\n * sha256. The sha256 reads the path of the Dockerfile, and the path and the content of each file.\n * Thus a changed program cannot operate as an old one.\n *\n * A build fails before it starts on a problem of its source. It also fails on a `FROM` that no\n * digest pins, and on a manifest of packages without a lockfile.\n */\nexport const planOf = (source: string | ImageSource): ImagePlan => {\n if (typeof source === \"string\") return { kind: \"pull\", image: source };\n const problems = imageProblems(source);\n if (problems.length > 0) throw new Error(`${labelOf(source)}: ${problems.join(\", \")}`);\n if (source.kind === \"pinned\") return { kind: \"pull\", image: source.image };\n const known = source.kind === \"shipped\" ? shippedPlans.get(source.name) : undefined;\n if (known !== undefined) return known;\n const { name, dockerfile, parts } = contextOf(source);\n const files = parts.flatMap((part) =>\n sources(part.path).map((file) => ({ file, at: join(part.at, relative(part.path, file)).replaceAll(\"\\\\\", \"/\") })),\n );\n const docker = files.find((each) => each.at === dockerfile);\n if (docker === undefined) throw new Error(`${labelOf(source)}: the context has no ${dockerfile}`);\n const unpinned = unpinnedBases(readFileSync(docker.file, \"utf8\"));\n const unlocked = unlockedManifests(files.map((each) => each.at));\n if (unpinned.length > 0 || unlocked.length > 0)\n throw new Error(\n `${labelOf(source)}: ${[\n ...unpinned.map((image) => `the base ${image} is not pinned by digest`),\n ...unlocked.map((path) => `${path} has no lockfile`),\n ].join(\", \")}`,\n );\n const hash = createHash(\"sha256\");\n hash.update(dockerfile);\n for (const { file, at } of files) {\n hash.update(at);\n hash.update(readFileSync(file));\n }\n const plan: ImagePlan = { kind: \"build\", tag: `${name}:${hash.digest(\"hex\").slice(0, 12)}`, dockerfile, files };\n if (source.kind === \"shipped\") shippedPlans.set(source.name, plan);\n return plan;\n};\n\n/** This function builds a staged context with Docker, through Testcontainers. */\nconst dockerBuild = async (context: string, plan: Extract<ImagePlan, { readonly kind: \"build\" }>): Promise<void> => {\n const { GenericContainer } = await import(\"testcontainers\");\n await GenericContainer.fromDockerfile(context, plan.dockerfile).build(plan.tag, { deleteOnExit: false });\n};\n\nconst built = new Map<string, Promise<string>>();\n\n/**\n * This function builds the image of a plan one time for each process, from a new context that it\n * copies the files into. Docker keeps its layers between runs. A build that fails leaves the cache.\n * Thus a later cell builds again after a fault of Docker or of the network.\n */\nexport const buildOnce = (\n plan: Extract<ImagePlan, { readonly kind: \"build\" }>,\n builder: (context: string, plan: Extract<ImagePlan, { readonly kind: \"build\" }>) => Promise<void> = dockerBuild,\n): Promise<string> => {\n let tag = built.get(plan.tag);\n if (tag === undefined) {\n const building = (async () => {\n const context = mkdtempSync(join(tmpdir(), `${plan.tag.split(\":\")[0]!.replaceAll(\"/\", \"-\")}-`));\n try {\n for (const { file, at } of plan.files) {\n mkdirSync(dirname(join(context, at)), { recursive: true });\n copyFileSync(file, join(context, at));\n }\n await builder(context, plan);\n } finally {\n rmSync(context, { recursive: true, force: true });\n }\n return plan.tag;\n })();\n // a failed build does not stay: the next caller tries again\n building.catch(() => {\n if (built.get(plan.tag) === building) built.delete(plan.tag);\n });\n built.set(plan.tag, building);\n tag = building;\n }\n return tag;\n};\n\n/**\n * This function gives an image that a lab can start, from its source. It is the name of a stock\n * image or of a pinned image, which the lab pulls, or the tag of a build. This function loads\n * Testcontainers only for a build.\n */\nexport const imageOf = async (source: string | ImageSource): Promise<string> => {\n const plan = planOf(source);\n return plan.kind === \"pull\" ? plan.image : buildOnce(plan);\n};\n\n/** This function gives the image of the harness: the prober, the TCP client, the injector and the namespace admin. */\nexport const harnessImage = (): Promise<string> => imageOf(shipped(\"harness\"));\n/** This function gives the image of the Node client. */\nexport const nodeClientImage = (): Promise<string> => imageOf(shipped(\"node\"));\n/** This function gives the image of the router. */\nexport const routerImage = (): Promise<string> => imageOf(shipped(\"router\"));\n/** This function gives the image of the Go HTTP client. */\nexport const goHttpImage = (): Promise<string> => imageOf(shipped(\"go-http\"));\n/** This function gives the image of the Node HTTP clients. */\nexport const nodeHttpImage = (): Promise<string> => imageOf(shipped(\"node-http\"));\n/** This function gives the image of the MongoDB client. */\nexport const mongoClientImage = (): Promise<string> => imageOf(shipped(\"mongo\"));\n/** This function gives the image of the PostgreSQL client. */\nexport const pgClientImage = (): Promise<string> => imageOf(shipped(\"pg\"));\n/** This function gives the image of the client of HikariCP over pgjdbc. */\nexport const hikariClientImage = (): Promise<string> => imageOf(shipped(\"hikari\"));\n/** This function gives the image of the procedures subject: the WebSocket client of `@procedures/client`, and its server. */\nexport const proceduresImage = (): Promise<string> => imageOf(SUBJECTS.procedures.image);\n\n/**\n * This function gives the source of the image that a target operates. It is the image of its\n * subject (`Target.subject`), a shipped image by its short form (`Target.harness`), or its stock image.\n */\nexport const targetSourceOf = (target: Target): string | ImageSource =>\n target.subject ?? (target.harness === undefined ? target.image : shipped(target.harness));\n\n/** This function gives the image that a target operates: the image of its subject, or its stock image. */\nexport const targetImageOf = (target: Target): Promise<string> => imageOf(targetSourceOf(target));\n"],"mappings":"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"}
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-COlAmUZ-.js";
1
+ import { $ as WsProbe, A as Reconnects, B as RunGroup, C as MiddleboxForm, D as ProbeEvent, E as Prediction, F as Replies, G as Shipped, H as RunTopology, I as RequestPrediction, J as StageBand, K as ShippedImage, L as RouterHop, M as Reference, N as ReplicaSet, O as ProxyHop, P as ReplicaSetStage, Q as Toxic, R as RttRange, S as MiddleboxExpectation, T as PinnedImage, U as SentinelEvent, V as RunPart, W as SentinelStage, X as Target, Y as Subject, Z as Topology, _ as HttpProbe, a as DriverBand, b as LineMatch, c as FaultPlace, d as GroupPrediction, et as isHopPlace, f as GroupRole, g as HopPlace, h as HopEvent, i as ClientImage, j as Recovers, k as ProxyTime, l as Group, m as Hop, n as Cell, o as DriverStep, p as HarnessImage, q as Stage, r as Client, s as Fault, t as BuildImage, tt as latestEndOf, u as GroupPlace, v as ImageSource, w as Observer, x as MiddleboxEvent, y as KernelRecord, z as Run } from "./cell-B6YHtKFb.js";
2
2
  import { Quantity } from "@mark1russell7/quantity";
3
3
  import "testcontainers";
4
4
  //#region src/application.d.ts
@@ -911,6 +911,16 @@ interface Lab {
911
911
  t0: number;
912
912
  t1: number;
913
913
  }>;
914
+ /**
915
+ * This method applies an event at a proxy hop (`HopEvent`), or its undo. A signal goes from a
916
+ * sidecar in the network and process namespaces of the hop. The lab starts that sidecar at the
917
+ * first signal of the hop. A query goes from the injector of the hop to its console on 127.0.0.1. The
918
+ * injector stamps each one. An event with no undo gives null for its undo.
919
+ */
920
+ event(event: HopEvent, undo?: boolean): Promise<{
921
+ t0: number;
922
+ t1: number;
923
+ } | null>;
914
924
  probe(args: readonly string[]): Promise<Probe>;
915
925
  /**
916
926
  * This method waits for the next clock line of the prober, which gives the count of lost
@@ -1422,5 +1432,5 @@ export declare const spread: (runs: number, environments: number) => number[];
1422
1432
  */
1423
1433
  export declare const runCell: (cell: Cell, say?: (line: string) => void) => Promise<CellResult>;
1424
1434
  //#endregion
1425
- export { type Agent, type BuildImage, type CallReading, type Cell, type CellResult, type Client, type ClientImage, type Crossings, type Cut, type Decision, type DriverBand, type DriverSocket, type DriverStep, type DriverStepTime, type ErrorOnTheWay, type Fault, type FaultPlace, type FaultSide, type Group, type GroupClient, type GroupLab, type GroupMeasured, type GroupNetworkFault, type GroupPlace, type GroupPrediction, type GroupReading, type GroupRole, type GroupVerdict, type GroupView, type HarnessImage, type Hop, type HopPlace, type HttpProbe, type ImagePlan, type ImageSource, type Interval, type KernelRecord, type Lab, type LineMatch, type Manifest, type ManifestHop, type Measured, type MemberLine, type MiddleboxEvent, type MiddleboxExpectation, type MiddleboxForm, type NetworkFault, type Observer, type Outcome, type PinnedImage, type Prediction, type Probe, type ProbeEvent, type ProgramEvent, type ProxyHop, type ProxyTime, type ReconnectReading, type ReconnectVerdict, type Reconnects, type Recovers, type Reference, type Reordering, type ReplicaSet, type ReplicaSetLab, type ReplicaSetStage, type Replies, type RequestPrediction, type RequestReading, type RouterHop, type RttRange, type Run, type RunGroup, type RunPart, type RunTopology, type SentinelEvent, type SentinelStage, type Shipped, type ShippedImage, type SocketPlace, type Spread, type Stage, type StageBand, type StageVerdict, type Stamp, type Status, type Subject, type Target, type Topology, type Toxic, type Verdict, type Writes, type WsProbe, isHopPlace, latestEndOf };
1435
+ export { type Agent, type BuildImage, type CallReading, type Cell, type CellResult, type Client, type ClientImage, type Crossings, type Cut, type Decision, type DriverBand, type DriverSocket, type DriverStep, type DriverStepTime, type ErrorOnTheWay, type Fault, type FaultPlace, type FaultSide, type Group, type GroupClient, type GroupLab, type GroupMeasured, type GroupNetworkFault, type GroupPlace, type GroupPrediction, type GroupReading, type GroupRole, type GroupVerdict, type GroupView, type HarnessImage, type Hop, type HopEvent, type HopPlace, type HttpProbe, type ImagePlan, type ImageSource, type Interval, type KernelRecord, type Lab, type LineMatch, type Manifest, type ManifestHop, type Measured, type MemberLine, type MiddleboxEvent, type MiddleboxExpectation, type MiddleboxForm, type NetworkFault, type Observer, type Outcome, type PinnedImage, type Prediction, type Probe, type ProbeEvent, type ProgramEvent, type ProxyHop, type ProxyTime, type ReconnectReading, type ReconnectVerdict, type Reconnects, type Recovers, type Reference, type Reordering, type ReplicaSet, type ReplicaSetLab, type ReplicaSetStage, type Replies, type RequestPrediction, type RequestReading, type RouterHop, type RttRange, type Run, type RunGroup, type RunPart, type RunTopology, type SentinelEvent, type SentinelStage, type Shipped, type ShippedImage, type SocketPlace, type Spread, type Stage, type StageBand, type StageVerdict, type Stamp, type Status, type Subject, type Target, type Topology, type Toxic, type Verdict, type Writes, type WsProbe, isHopPlace, latestEndOf };
1426
1436
  //# sourceMappingURL=index.d.ts.map
package/build/index.js CHANGED
@@ -1,10 +1,15 @@
1
- import { C as targetImageOf, E as unpinnedBases, S as subjectProblems, T as unlockedManifests, _ as planOf, a as SHIPPED_NAMES, b as routerImage, c as harnessImage, d as imageProblems, f as inContext, g as pgClientImage, h as nodeHttpImage, i as SHIPPED_CLIENTS, l as hikariClientImage, m as nodeClientImage, n as PINNED, o as SUBJECTS, p as mongoClientImage, r as REPOSITORY, s as goHttpImage, t as CLIENT_NAMES, u as imageOf, v as proceduresImage, w as targetSourceOf, x as subjectClient, y as programOf } from "./image-DIJ1AFg_.js";
1
+ import { C as targetImageOf, E as unpinnedBases, S as subjectProblems, T as unlockedManifests, _ as planOf, a as SHIPPED_NAMES, b as routerImage, c as harnessImage, d as imageProblems, f as inContext, g as pgClientImage, h as nodeHttpImage, i as SHIPPED_CLIENTS, l as hikariClientImage, m as nodeClientImage, n as PINNED, o as SUBJECTS, p as mongoClientImage, r as REPOSITORY, s as goHttpImage, t as CLIENT_NAMES, u as imageOf, v as proceduresImage, w as targetSourceOf, x as subjectClient, y as programOf } from "./image-RM60Paqe.js";
2
2
  import { quantity, valueIn } from "@mark1russell7/quantity";
3
3
  import { createHash } from "node:crypto";
4
4
  //#region src/cell.ts
5
5
  /** This function tells if a fault place is a hop of a topology. */
6
6
  const isHopPlace = (place) => typeof place === "object" && "hop" in place;
7
7
  /**
8
+ * This function tells if the request of a client goes before the fault of its cell. That is so for a
9
+ * hop event that comes while the request is in flight (`HopEvent.inFlight`). Then a request after the fault is void.
10
+ */
11
+ const requestFirst = (cell) => cell.fault === "hop-event" && cell.event?.inFlight === true;
12
+ /**
8
13
  * This function gives the latest end of a band in seconds, from the reply of the client. It is
9
14
  * the high bound, after the latest reference of the band (`startsBy`).
10
15
  */
@@ -898,7 +903,9 @@ const measureDriver = (cell, run) => {
898
903
  let voided = null;
899
904
  if (cell.probe.mode !== "idle" && cell.before !== true) {
900
905
  if (request === void 0) voided = "the operation was never sent into the fault";
901
- else if (run.fault !== null && run.fault.t1 > request.t) voided = "the operation went out before the fault held";
906
+ else if (requestFirst(cell)) {
907
+ if (run.fault !== null && run.fault.t0 < request.t) voided = "the event came before the operation";
908
+ } else if (run.fault !== null && run.fault.t1 > request.t) voided = "the operation went out before the fault held";
902
909
  }
903
910
  const keepalive = records.find((record) => record.ev === "probe" && record.what === "keepalive");
904
911
  if (voided === null && run.fault !== null && keepalive !== void 0 && keepalive.mono < run.fault.t1) voided = "a keepalive probe went out before the fault held";
@@ -1403,7 +1410,7 @@ const count = (event, field) => event === void 0 ? 0 : Number(event[field] ?? 0)
1403
1410
  /** This function gives the view of the client on the same interval: TCP_INFO, with a sample each 2 ms. */
1404
1411
  const socketVantage = (cell, events, ready, request) => {
1405
1412
  const { from } = cell.prediction.detects === true ? cell.prediction : { from: void 0 };
1406
- if (from === void 0 || from === "peer-closed" || from === "fault" || from === "ping-sent" || from === "first-sent") return null;
1413
+ if (from === void 0 || from === "peer-closed" || from === "fault" || from === "ping-sent" || from === "first-sent" || from === "last-data") return null;
1407
1414
  const ended = events.find((event) => event.ev === "error" || event.ev === "eof");
1408
1415
  const reference = from === "last-received" ? ready : from === "request-sent" ? request : events.find((event) => event.ev === "kernel" && event.t >= request.t && count(event, "retransmits") > 0);
1409
1416
  return ended === void 0 || reference === void 0 ? null : between(reference.t, ended.t);
@@ -1705,7 +1712,7 @@ const measureSocket = (cell, run) => {
1705
1712
  };
1706
1713
  let voided = null;
1707
1714
  if (run.fault !== null) {
1708
- const replyFirst = server && inflight && cell.fault !== "mtu-blackhole";
1715
+ const replyFirst = server && inflight && cell.fault !== "mtu-blackhole" || requestFirst(cell);
1709
1716
  const replied = events.some((event) => event.ev === "data" && event.t > request.t && event.t < run.fault.t1);
1710
1717
  if (inflight && !replyFirst && run.fault.t1 > request.t) voided = "the request went out before the fault held";
1711
1718
  else if (replyFirst && run.fault.t1 < request.t) voided = "the request went out into the fault, so the server never had it";
@@ -1715,10 +1722,12 @@ const measureSocket = (cell, run) => {
1715
1722
  const assumed = cell.prediction.detects === null ? void 0 : cell.prediction.rtt;
1716
1723
  if (voided === null && assumed !== void 0 && rtt !== null && valueIn(rtt, "ms") > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt.value} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
1717
1724
  const remote = cell.observer !== void 0 && cell.observer !== "client";
1718
- const reported = socket.find((record) => record.ev === "error" && (record.err ?? 0) !== 0);
1719
- const seen = remote ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
1720
- const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8);
1721
- 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;
1725
+ const stop = events.find((event) => event.ev === "done")?.t ?? Number.POSITIVE_INFINITY;
1726
+ const anError = socket.find((record) => record.ev === "error" && (record.err ?? 0) !== 0 && record.mono < stop);
1727
+ const seen = remote ? void 0 : events.find((event) => (event.ev === "error" || event.ev === "eof") && event.t < stop);
1728
+ const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8 && record.mono < stop);
1729
+ 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 && record.mono < stop) : void 0;
1730
+ const reported = anError !== void 0 && own !== void 0 && own.mono < anError.mono ? void 0 : anError;
1722
1731
  const lines = programLines(cell, run);
1723
1732
  const named = server ? socket.find((record) => record.dst !== void 0)?.dst ?? local : local;
1724
1733
  const kernelEnded = reported?.mono ?? closeWait?.mono ?? own?.mono ?? seen?.t;
@@ -1742,7 +1751,7 @@ const measureSocket = (cell, run) => {
1742
1751
  if (from === "fault") return run.fault?.t0;
1743
1752
  if (from === "client-start") return (events.find((event) => event.ev === "connecting") ?? events[0])?.t;
1744
1753
  if (from === "ping-sent") return firstUnanswered(pings, socket, ended)?.at;
1745
- 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);
1754
+ const record = from === "last-received" ? socket.filter((each) => each.ev === "rx" && each.mono <= before).at(-1) : from === "last-data" ? socket.filter((each) => each.ev === "rx" && (each.seg?.len.value ?? 0) > 0 && each.mono <= (ended ?? 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);
1746
1755
  if (record === void 0 && from === "peer-closed" && peerCookie === void 0) return proxiedClose(run);
1747
1756
  return record?.mono;
1748
1757
  };
@@ -2040,8 +2049,10 @@ const lines = (also) => {
2040
2049
  if (found !== void 0) return Promise.resolve(found);
2041
2050
  return new Promise((resolve, reject) => {
2042
2051
  const timer = setTimeout(() => {
2043
- const said = other.length === 0 ? "" : `; it wrote: ${other.slice(-3).join(" | ")}`;
2044
- reject(/* @__PURE__ */ new Error(`no ${kinds.join(" or ")} from the ${who} in ${within} ms${said}`));
2052
+ const failed = seen.filter((line) => line.ev === "fail" || line.ev === "error").slice(0, 3);
2053
+ const last = [.../* @__PURE__ */ new Set([...failed, ...seen.slice(-3)])].map((line) => JSON.stringify(line));
2054
+ const said = [...last.length === 0 ? [] : [`its last lines: ${last.join(" | ")}`], ...other.length === 0 ? [] : [`it wrote: ${other.slice(-3).join(" | ")}`]];
2055
+ reject(/* @__PURE__ */ new Error(`no ${kinds.join(" or ")} from the ${who} in ${within} ms${said.length === 0 ? "" : `; ${said.join("; ")}`}`));
2045
2056
  }, within);
2046
2057
  waiting.push({
2047
2058
  kinds,
@@ -2429,6 +2440,7 @@ const openLab = async (target, kind = "tcp", topology) => {
2429
2440
  const paused = runtime.container.getById(server.getId());
2430
2441
  let targetAdmin;
2431
2442
  let signaller;
2443
+ const hopSignallers = /* @__PURE__ */ new Map();
2432
2444
  const hopNamed = (name) => {
2433
2445
  const found = inChain.find((hop) => hop.hop.name === name);
2434
2446
  if (found === void 0) throw new Error(`the topology has no hop ${name}`);
@@ -2704,6 +2716,38 @@ const openLab = async (target, kind = "tcp", topology) => {
2704
2716
  ...when
2705
2717
  ]);
2706
2718
  },
2719
+ async event(event, undo = false) {
2720
+ const hop = hopNamed(event.hop);
2721
+ if (hop.hop.kind !== "proxy") throw new Error(`the hop ${event.hop} is a router: an event goes to a proxy`);
2722
+ if ("signal" in event) {
2723
+ if (undo) return null;
2724
+ const program = hop.hop.program;
2725
+ if (program === void 0) throw new Error(`the hop ${event.hop} names no program to signal`);
2726
+ let sidecar = hopSignallers.get(event.hop);
2727
+ if (sidecar === void 0) {
2728
+ sidecar = await withPid(new GenericContainer(image).withNetworkMode(`container:${hop.box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${hop.box.getId()}`).start();
2729
+ hopSignallers.set(event.hop, sidecar);
2730
+ opened.push(sidecar);
2731
+ }
2732
+ return stamped(sidecar, [
2733
+ "/opt/harness/inject",
2734
+ "signal",
2735
+ program,
2736
+ event.signal
2737
+ ]);
2738
+ }
2739
+ const query = undo ? event.console.undo : event.console.query;
2740
+ if (query === void 0) return null;
2741
+ const { port, user, database } = event.console;
2742
+ return stamped(hop.injector, [
2743
+ "/opt/harness/inject",
2744
+ "query",
2745
+ `127.0.0.1:${port}`,
2746
+ user,
2747
+ database,
2748
+ query
2749
+ ]);
2750
+ },
2707
2751
  async probe(args) {
2708
2752
  const events = lines();
2709
2753
  const container = await new GenericContainer(clientImage).withNetworkMode(`container:${client.getId()}`).withCommand([...command, ...args]).withLogConsumer(events.consume).start();
@@ -4181,9 +4225,9 @@ const probeArgs = (cell, lab) => {
4181
4225
  args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
4182
4226
  return args;
4183
4227
  }
4184
- if (probe.driver?.name === "pg" || probe.driver?.name === "hikari") {
4185
- if (probe.driver.query !== void 0) args.push("--query", probe.driver.query);
4186
- if (probe.driver.inflight !== void 0) args.push("--inflight-query", probe.driver.inflight);
4228
+ if (probe.driver?.name === "pg" || probe.driver?.name === "hikari" || probe.driver?.name === "amqp") {
4229
+ if (probe.driver.name !== "amqp" && probe.driver.query !== void 0) args.push("--query", probe.driver.query);
4230
+ if (probe.driver.name !== "amqp" && probe.driver.inflight !== void 0) args.push("--inflight-query", probe.driver.inflight);
4187
4231
  if (cell.before === true) args.push("--before");
4188
4232
  if (cell.driverOptions !== void 0) args.push("--options", JSON.stringify(cell.driverOptions));
4189
4233
  args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
@@ -4396,7 +4440,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
4396
4440
  if (cell.probe.mode !== "idle") await go();
4397
4441
  await sleep(500);
4398
4442
  } else {
4399
- const replyInFlight = cell.probe.mode === "inflight" && (server && cell.fault !== "mtu-blackhole" || cell.fault === "fin");
4443
+ const replyInFlight = cell.probe.mode === "inflight" && (server && cell.fault !== "mtu-blackhole" || cell.fault === "fin" || requestFirst(cell));
4400
4444
  if (replyInFlight) {
4401
4445
  await go();
4402
4446
  await sleep(200);
@@ -4447,6 +4491,9 @@ const once = async (lab, cell, environment, index, dry = false) => {
4447
4491
  case "server-close":
4448
4492
  await lab.serverClose();
4449
4493
  return null;
4494
+ case "hop-event":
4495
+ if (cell.event === void 0) throw new Error(`${cell.name} names no event`);
4496
+ return lab.event(cell.event);
4450
4497
  default: return lab.fault("blackhole", faultAt);
4451
4498
  }
4452
4499
  };
@@ -4465,6 +4512,9 @@ const once = async (lab, cell, environment, index, dry = false) => {
4465
4512
  case "server-close":
4466
4513
  case "app-stall":
4467
4514
  case "none": return;
4515
+ case "hop-event":
4516
+ if (cell.event !== void 0) await lab.event(cell.event, true);
4517
+ return;
4468
4518
  case "unreachable": return lab.lift("reject", faultAt, { code: cell.icmp ?? "host" });
4469
4519
  case "simplex": return lab.lift("blackhole", faultAt, options);
4470
4520
  case "mtu-blackhole": return lab.lift("mtu", cell.faultAt ?? "client", { above: cell.mtu ?? 1200 });