@mark1russell7/harness 0.14.3 → 0.14.4
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 +20 -5
- package/build/{cell-B3-BSTit.d.ts → cell-DZm8zXN4.d.ts} +239 -8
- package/build/{image-BMdV7yWD.js → image-EZ8ioZ3L.js} +9 -2
- package/build/image-EZ8ioZ3L.js.map +1 -0
- package/build/index.d.ts +269 -4
- package/build/index.js +1284 -116
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +1 -1
- package/build/schema.js +1 -1
- package/image/Dockerfile +1 -1
- package/image/amqp/client-cluster.mjs +68 -31
- package/image/hikari/ClientHikari.java +81 -6
- package/image/kafka/AdminKafka.java +117 -0
- package/image/kafka/ClientKafka.java +234 -0
- package/image/kafka/Dockerfile +23 -0
- package/image/kafka/jars.sha256 +5 -0
- package/image/native/src/bin/ports.rs +374 -0
- package/image/native/src/bin/queue.rs +553 -0
- package/image/node/client-sentinel.mjs +28 -12
- package/image/pg/client-pg.mjs +31 -4
- package/package.json +5 -1
- package/build/image-BMdV7yWD.js.map +0 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The harness does not compute predictions. The author of a cell computes the pred
|
|
|
8
8
|
|
|
9
9
|
A **cell** (`cell.ts`) is all that one measurement needs, as data:
|
|
10
10
|
|
|
11
|
-
- **The topology.** A stock target image, pinned by digest, and the hops between the client and the target (`Cell.topology`). Without hops, the client and the target are a direct pair. A replicated group under its Sentinels can replace the target (`Cell.group`).
|
|
11
|
+
- **The topology.** A stock target image, pinned by digest, and the hops between the client and the target (`Cell.topology`). Without hops, the client and the target are a direct pair. A replicated group under its Sentinels can replace the target (`Cell.group`). A PostgreSQL target can have one standby, with a final read of each server (`Cell.pair`).
|
|
12
12
|
- **The subject.** The program under test, as data. Its image is an image pinned by digest, a build context, or an image that the harness ships (`Cell.client`, `Target.subject`).
|
|
13
13
|
- **The settings.** The sysctls of the client namespace, and the socket options of the client.
|
|
14
14
|
- **The workload.** A connection that stays idle, a connection with a request in flight, or a stream of requests. A stream of `GET` requests for a long value keeps many segments in flight.
|
|
@@ -94,6 +94,8 @@ Each run of a group cell starts from a group that converged:
|
|
|
94
94
|
|
|
95
95
|
The harness then removes each key from the group. After the run, the group converges again, and the keys that it holds tell which writes it kept.
|
|
96
96
|
|
|
97
|
+
A driver with `op: "rpush"` appends each write to one list, and the entries tell how many copies of each write the group kept. `window` sets the count of writers. A band of extra copies (`GroupPrediction.duplicates`) judges the cell as a band of lost writes does.
|
|
98
|
+
|
|
97
99
|
A Sentinel that aborted a failover does not try again for 2 × `failover-timeout`. Thus after a run with an aborted failover, each Sentinel forgets the state of the group (`SENTINEL RESET`). The next run then starts from a group that each Sentinel knows again.
|
|
98
100
|
|
|
99
101
|
## The replica sets
|
|
@@ -103,7 +105,7 @@ A replica set (`ReplicaSet`, `Cell.replicaSet`) is a replicated group with no ex
|
|
|
103
105
|
- **The members.** Each member is a container from one stock image, pinned by digest, in its own network namespace. Its server is the child of a shell, with the name of the set and the options of the set (`args`).
|
|
104
106
|
- **The start.** The first member starts the set (`rs.initiate`) with the address of each member, as the client reaches it, and with the settings of the set. An example of a setting is `electionTimeoutMillis`.
|
|
105
107
|
- **The sidecars.** Each member has an injector sidecar in its network namespace and in its process namespace, as a member of a group has.
|
|
106
|
-
- **The client.** The client of each run is the MongoDB driver (`image/mongo`). It knows the address of each member and the name of the set. It writes a stream of sequence numbers with the write concern of the cell (`Cell.writeConcern`).
|
|
108
|
+
- **The client.** The client of each run is the MongoDB driver (`image/mongo`). It knows the address of each member and the name of the set. It writes a stream of sequence numbers with the write concern of the cell (`Cell.writeConcern`). A run continues 2 s after the lift, and for `Cell.watch` more. A prediction of no failover after a short hold needs that time.
|
|
107
109
|
- **The timeline.** A log consumer reads the JSON log of each member. A run keeps the lines of replication, of its heartbeats, of the elections, of the rollbacks and of the commands (`KEPT`).
|
|
108
110
|
- **The clocks.** The log of mongod has the wall clock in ms. The client writes the wall clock beside the monotonic clock each second (`clocks`). Thus the run puts each line of a member on the monotonic clock (`clockOffsetOf`).
|
|
109
111
|
- **The prober** watches the namespace of the client.
|
|
@@ -131,7 +133,7 @@ A cluster (`Cluster`, `Cell.cluster`) is a cluster of RabbitMQ nodes with one qu
|
|
|
131
133
|
- **The nodes.** Each node is a container from one stock image, pinned by digest, in its own network namespace. Its hostname is the name of its node (`rabbit@<name>`), and all nodes share one Erlang cookie. Its server is the child of a shell.
|
|
132
134
|
- **The start.** The nodes form the cluster from the list of their names (`classic_config`), and the lab waits until each node runs in it. Then the admin (`image/amqp/admin-cluster.mjs`) declares the queue on the first node, with the leader locator `client-local`.
|
|
133
135
|
- **The sidecars.** Each node has an injector sidecar in its network namespace and in its process namespace.
|
|
134
|
-
- **The client.** The client of each run is amqplib, in `image/amqp/client-cluster.mjs`. The client publishes a stream of sequence numbers on a confirm channel through one node. That node is the last follower, or the node of the leader (`Cell.clientAt`). The library does not connect again by itself. When the connection ends, the client connects to the next node after 1 s, as an application does.
|
|
136
|
+
- **The client.** The client of each run is amqplib, in `image/amqp/client-cluster.mjs`. The client publishes a stream of sequence numbers on a confirm channel through one node. That node is the last follower, or the node of the leader (`Cell.clientAt`). The library does not connect again by itself. When the connection ends, the client connects to the next node after 1 s, as an application does. `driverOptions.window` sets the count of writers on the one channel. Each option that the client does not read goes to the socket of amqplib, for example `noDelay`.
|
|
135
137
|
- **The timeline.** A log consumer reads the console log of each node. A run keeps the lines of the queue, each warning and error, and the lines of the maintenance mode. Ra writes a NOTICE for each change from or to the leader, and an INFO for each new leader that a follower detects.
|
|
136
138
|
- **The clocks.** The log has the wall clock in µs. The run puts each line on the monotonic clock with the clock lines of the client.
|
|
137
139
|
|
|
@@ -139,6 +141,14 @@ Each run starts from a queue that converged (`rabbitmq-queues quorum_status`): o
|
|
|
139
141
|
|
|
140
142
|
A step-down (`step-down`) moves the leadership to the first follower (`rabbitmq-queues transfer_leadership`). A drain (`drain`) puts the node in maintenance mode (`rabbitmq-upgrade drain`), and the run revives the node after it. Each of them starts an Erlang VM for the tools first. Thus its stamp is a line of the log, and not the command. For a step-down it is the NOTICE of the old leader that gives the leadership away (`GIVES_AWAY`). A drain stops the member of the leader, thus its stamp is the first follower that sees its monitor of the leader go down (`MONITOR_DOWN`).
|
|
141
143
|
|
|
144
|
+
A Kafka group (`Kafka`, `Cell.kafka`) is a group of Kafka brokers in KRaft mode with one controller, as data. The group replaces the target of its cell.
|
|
145
|
+
|
|
146
|
+
- **The nodes.** Each node is a container from one stock image, pinned by digest, in its own network namespace. The image starts by its own command, and each setting comes from the environment (`envOf`). The controller is a node of its own. A fault does not go on it.
|
|
147
|
+
- **The topic.** Each run makes one new topic with one partition, a replica on each broker, and `Kafka.minInsync`. The first broker in the replica list leads. A run starts when each broker is not fenced and the partition is whole.
|
|
148
|
+
- **The sidecars.** Each node has an injector sidecar in its network namespace and in its process namespace. A `partition` with `cuts: "group"` cuts the leader from the other brokers and the controller. The client keeps its route to the leader.
|
|
149
|
+
- **The client.** The client of each run is the producer of kafka-clients, in `image/kafka/ClientKafka.java`. Each of `Kafka.writers` threads has one write at a time. Each entry of `Cell.driverOptions` is a producer setting.
|
|
150
|
+
- **The record.** A Kafka group has no log lines in its record, and `Run.group.sentinels` holds the controller. After the run the partition converges again, and the admin (`image/kafka/AdminKafka.java`) reads the whole partition to its high watermark. The leader after the run tells if a failover came.
|
|
151
|
+
|
|
142
152
|
## The clients and the agents
|
|
143
153
|
|
|
144
154
|
- **A client image** (`ClientImage`, `Cell.client`) replaces the TCP client of the harness in each run. It operates in the namespace of the client. Its command gets the arguments of the TCP client, and it prints the same JSON lines. Its image is a stock image by its name, or the source of the image of a subject (`ImageSource`).
|
|
@@ -204,14 +214,18 @@ From 0.11.20, the harness builds each image with the CLI of Docker (`docker buil
|
|
|
204
214
|
- **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, inflight } }`, with `SELECT 1` as the query when the cell gives none.
|
|
205
215
|
- `inflight` is the text of the query in flight: the second query of an in-flight run. The first query then makes the pool ready at once, and only the query in flight is slow, for example `SELECT pg_sleep(20)`. The HikariCP client reads it too.
|
|
206
216
|
- **The HikariCP client image** (`image/hikari`) is HikariCP 7.1.0 over pgjdbc 42.7.13 on Temurin 21.0.12.1. The digests of the two stages and the SHA-256 of each jar pin them. The probe is `{ driver: { name: "hikari", query, inflight } }`.
|
|
207
|
-
- `ClientHikari.java` makes one `HikariDataSource` with the properties of the cell (`Cell.driverOptions`). A name with the prefix `dataSource.` is a property of pgjdbc.
|
|
217
|
+
- `ClientHikari.java` makes one `HikariDataSource` with the properties of the cell (`Cell.driverOptions`). A name with the prefix `dataSource.` is a property of pgjdbc. A name with the prefix `system.` is a system property of the JVM, which the client sets before its pool.
|
|
208
218
|
- 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`.
|
|
209
219
|
- 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.
|
|
210
220
|
- 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.
|
|
221
|
+
- With `burst` in the probe (`{ count, gap }`), it sends `count` queries in place of one query in flight. Query 2 and the next ones each start on their own thread, `gap` apart, when each connection of the pool is idle. A `burst` line gives the counts of replies and failures, then the counts of the pool. Before its first line, a client with a burst also makes one borrow that times out. Thus the band of a failure does not time that load.
|
|
211
222
|
- **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" } }`.
|
|
212
223
|
- `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.
|
|
213
224
|
- 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.
|
|
214
225
|
- 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.
|
|
226
|
+
- **The Kafka client image** (`image/kafka`) is kafka-clients 4.3.2 on Java 21, on the base image of the HikariCP client. The digest of the image and the hash of each jar (`jars.sha256`) pin them. A Kafka lab starts it (`Cell.kafka`), not a probe of a driver.
|
|
227
|
+
- `ClientKafka.java` writes the sequence numbers to partition 0 of the topic of the run, with the settings of the cell. It prints `sent`, `ack` and `fail` lines on the monotonic clock.
|
|
228
|
+
- `AdminKafka.java` makes a topic, gives a view of the partition, and reads the whole partition.
|
|
215
229
|
- **The gRPC client image** (`image/grpc`) is grpc-js 1.14.6 on Node 24.21.0. The digest of the image and an npm lockfile pin them. The probe is `{ driver: { name: "grpc", hold } }`.
|
|
216
230
|
- `client-grpc.mjs` opens one channel with no TLS. Each operation is a unary call of `grpc.health.v1.Health/Check`, with the messages encoded in the script. The first call makes the connection.
|
|
217
231
|
- The options of the cell (`Cell.driverOptions`) are the channel options of grpc-js (`grpc.*`), and `deadline_ms` for the deadline of each call. With `hold`, the call in flight asks the server of the harness to hold its reply that long.
|
|
@@ -280,7 +294,7 @@ ioredis sets the keepalive of its socket itself, after it connects. Thus a cell
|
|
|
280
294
|
- 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.
|
|
281
295
|
- **A fault on a group** acts on the part that `Cell.faultAt` names by its role:
|
|
282
296
|
- 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.
|
|
283
|
-
- 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.
|
|
297
|
+
- 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. `to-clients` drops only the segments from the part to the client, thus each write reaches the part and no reply reaches the client.
|
|
284
298
|
- The rules for each peer go into one commit. The injector takes a list of peers, `A,B,C`.
|
|
285
299
|
- A frozen process (`freeze-process`) is SIGSTOP to the server of the part, and SIGCONT thaws it. A FIN (`fin`) is SIGKILL, and the server starts again after the run.
|
|
286
300
|
- A planned failover (`planned-failover`) is `SENTINEL FAILOVER <group>` to the first Sentinel, or `SENTINEL FAILOVER <group> COORDINATED` (`Cell.coordinated`). The injector stamps the write of the command and its reply.
|
|
@@ -415,6 +429,7 @@ The distribution test needs a simulator, which does not exist at this time.
|
|
|
415
429
|
- The manifest holds the kernel release, the architecture, the boot id and the Docker version. It also holds the image ids, the packages of the image, the client and the sysctls as read back.
|
|
416
430
|
- Each image that the harness builds has a build record in the manifest (`BuildRecord`). It gives the tag, the bases by digest and the sha256 of each lockfile. It also gives the versions of the packages that the manifests of the image name. The harness, the client and the image of a subject have one. The harness record also has the version of the package.
|
|
417
431
|
- `manifestProblems(manifest)` gives each image of a manifest that does not pin its programs. Such an image is a pulled image without a digest, or a build without its build record. `commitProblems(commit)` examines the commit of a record. A record without a pin for each program is not a record, because nobody can reproduce its numbers.
|
|
432
|
+
- `recordProblems(record)` gives each problem of a record: its commit, its manifests, a checkout with changes (`DIRTY`) and the mark of a debug run (`DEBUG`). The writer of the cells and the read of a schedule use this one function.
|
|
418
433
|
- A cell whose probe names a `driver` opens its labs with the client of that driver (`openLab(target, "ioredis")`).
|
|
419
434
|
- The harness sets the `targetSettings` of a cell in the target before a client connects, with the `settings` commands of the target (`redis-cli CONFIG SET`, then `CONFIG GET`).
|
|
420
435
|
- A group cell opens a group environment (`openGroupLab`), and each run is `groupOnce`. Its warm-up is a whole run with its fault, because no fault of a group ends its environment. The manifest also holds the settings of each member and of each Sentinel, as each part read them.
|
|
@@ -331,6 +331,36 @@ interface Cluster {
|
|
|
331
331
|
readonly start: string;
|
|
332
332
|
};
|
|
333
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* A Kafka group is a group of brokers of Apache Kafka in KRaft mode with one controller, as data.
|
|
336
|
+
* Each node is a container from the same stock image, pinned by digest, in its own network namespace.
|
|
337
|
+
* Each run makes one new topic with one partition, with a replica on each broker. The client is the
|
|
338
|
+
* producer of kafka-clients, and it writes a stream of sequence numbers to that partition.
|
|
339
|
+
* The one controller keeps a Raft election out of each run. A fault on a broker cuts it from the
|
|
340
|
+
* controller too, and no fault acts on the controller.
|
|
341
|
+
*/
|
|
342
|
+
interface Kafka {
|
|
343
|
+
/** This image is the stock image of each node, pinned by digest. */
|
|
344
|
+
readonly image: string;
|
|
345
|
+
/** These are the names of the brokers. A broker's node id is its place in this list, from 1. The first broker is the first leader. */
|
|
346
|
+
readonly brokers: readonly string[];
|
|
347
|
+
/** This name is the name of the controller. Its node id comes after the ids of the brokers. */
|
|
348
|
+
readonly controller: string;
|
|
349
|
+
readonly port: number;
|
|
350
|
+
readonly controllerPort: number;
|
|
351
|
+
/** This text is the id of the cluster: 16 bytes in base64url without padding. */
|
|
352
|
+
readonly clusterId: string;
|
|
353
|
+
/** This text is the value of KAFKA_HEAP_OPTS of each node. */
|
|
354
|
+
readonly heap: string;
|
|
355
|
+
/** These are server properties of each node, by their names in Kafka: for example `broker.session.timeout.ms`. */
|
|
356
|
+
readonly settings: Readonly<Record<string, string>>;
|
|
357
|
+
/** This line tells that a node serves. */
|
|
358
|
+
readonly ready: string;
|
|
359
|
+
/** This count is the setting `min.insync.replicas` of the topic of each run. */
|
|
360
|
+
readonly minInsync: number;
|
|
361
|
+
/** This count is the number of writers of the client, each with one write at a time. */
|
|
362
|
+
readonly writers: number;
|
|
363
|
+
}
|
|
334
364
|
/**
|
|
335
365
|
* A cluster stage is one step of the timeline of a cluster after a fault. A line of the log of a
|
|
336
366
|
* node marks it, or a line of the client. These are the stages:
|
|
@@ -352,10 +382,12 @@ type GroupRole = "primary" | "replica" | "sentinel";
|
|
|
352
382
|
* - `clients`: the clients, but not the group.
|
|
353
383
|
* - `one`: one other member, in both directions.
|
|
354
384
|
* - `one-way`: one other member, for the segments from the part only.
|
|
385
|
+
* - `to-clients`: the clients, for the segments from the part only. Each write of a client reaches
|
|
386
|
+
* the part, and no reply reaches the client.
|
|
355
387
|
*/
|
|
356
388
|
interface GroupPlace {
|
|
357
389
|
readonly role: GroupRole;
|
|
358
|
-
readonly cuts?: "all" | "group" | "clients" | "one" | "one-way";
|
|
390
|
+
readonly cuts?: "all" | "group" | "clients" | "one" | "one-way" | "to-clients";
|
|
359
391
|
}
|
|
360
392
|
/** A toxic is one toxic of Toxiproxy, as its API takes it. Its toxicity is 1. */
|
|
361
393
|
interface Toxic {
|
|
@@ -479,6 +511,9 @@ interface StageBand {
|
|
|
479
511
|
* - `lost` is the band of the count of accepted writes that the group did not keep, in each run. A
|
|
480
512
|
* count outside it is a finding. A measurement-only cell with this band gets its decision from
|
|
481
513
|
* the findings of its runs, thus the law of the count judges the cell.
|
|
514
|
+
* - `duplicates` is the band of the count of extra copies that the group holds, in each run. It
|
|
515
|
+
* judges a cell as `lost` does. Only a list or a log can show a copy, thus a Redis cell needs
|
|
516
|
+
* `op: "rpush"`.
|
|
482
517
|
*/
|
|
483
518
|
interface GroupPrediction {
|
|
484
519
|
readonly lands: "new" | "same";
|
|
@@ -495,6 +530,51 @@ interface GroupPrediction {
|
|
|
495
530
|
readonly low: number;
|
|
496
531
|
readonly high: number;
|
|
497
532
|
};
|
|
533
|
+
readonly duplicates?: {
|
|
534
|
+
readonly low: number;
|
|
535
|
+
readonly high: number;
|
|
536
|
+
};
|
|
537
|
+
/** These are the bands of the commits of a pair (`Cell.pair`), each by its set. A set with no band is a measurement. */
|
|
538
|
+
readonly outcomes?: Outcomes;
|
|
539
|
+
}
|
|
540
|
+
/** A count band is the least and the most of a count, both included. */
|
|
541
|
+
interface CountBand {
|
|
542
|
+
readonly low: number;
|
|
543
|
+
readonly high: number;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* These are the bands of the sets of a pair. A commit with no reply is in one set, by the two final reads:
|
|
547
|
+
* - `kept`: the new primary holds it.
|
|
548
|
+
* - `stranded`: only the old primary holds it, after its crash recovery.
|
|
549
|
+
* - `absent`: no server holds it.
|
|
550
|
+
*/
|
|
551
|
+
interface Outcomes {
|
|
552
|
+
/** This band is the count of the writes with no reply, of each set. */
|
|
553
|
+
readonly unknown?: CountBand;
|
|
554
|
+
readonly kept?: CountBand;
|
|
555
|
+
readonly stranded?: CountBand;
|
|
556
|
+
readonly absent?: CountBand;
|
|
557
|
+
/** This band is the count of the acknowledged writes that the old primary does not hold after its crash recovery. */
|
|
558
|
+
readonly missing?: CountBand;
|
|
559
|
+
/** This band is the count of the WARNING notices that the client reads. */
|
|
560
|
+
readonly warnings?: CountBand;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* A pair is a PostgreSQL primary, the target of a cell, and one standby on its network. The topology
|
|
564
|
+
* of the cell stays in front of the primary. Nothing in the pair fails over by itself: the harness
|
|
565
|
+
* does each step, in the order of `pairOnce`.
|
|
566
|
+
*/
|
|
567
|
+
interface Pair {
|
|
568
|
+
/** This flag names the standby in `synchronous_standby_names`. Without it, the standby is asynchronous. */
|
|
569
|
+
readonly synchronous: boolean;
|
|
570
|
+
/** This count is the writers of the client, each with its own connection of the pool. */
|
|
571
|
+
readonly writers: number;
|
|
572
|
+
/** This time is the pause of a writer after each reply. */
|
|
573
|
+
readonly interval: Quantity;
|
|
574
|
+
/** This place is what the black hole cuts the primary from: the standby, or each peer. */
|
|
575
|
+
readonly cuts: "standby" | "all";
|
|
576
|
+
/** This count is the cancels of the waits of the primary, spread across the hold. */
|
|
577
|
+
readonly cancels?: number;
|
|
498
578
|
}
|
|
499
579
|
/**
|
|
500
580
|
* A middlebox expectation tells what the router of a topology does with the flow in the window of
|
|
@@ -533,6 +613,16 @@ interface LineMatch {
|
|
|
533
613
|
readonly where?: Readonly<Record<string, string | number | boolean>>;
|
|
534
614
|
readonly name?: string;
|
|
535
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* A line count is an exact count of the lines of the target in one run, for a cell whose law gives a
|
|
618
|
+
* count (SYS-67). A count reads no timer, thus it has no band and no noise. The judge compares it with
|
|
619
|
+
* `count`, and `what` names it in the finding.
|
|
620
|
+
*/
|
|
621
|
+
interface LineCount {
|
|
622
|
+
readonly what: string;
|
|
623
|
+
readonly match: LineMatch;
|
|
624
|
+
readonly count: number;
|
|
625
|
+
}
|
|
536
626
|
/**
|
|
537
627
|
* A driver step is one more band of a driver after the band of its cell. An example is the error
|
|
538
628
|
* of the retry after the interruption. It has its own reference on a socket of the driver, its
|
|
@@ -684,7 +774,120 @@ type Prediction = {
|
|
|
684
774
|
* the client. The end has no band, but each wait has one. A wait outside its band is a finding of its run.
|
|
685
775
|
*/
|
|
686
776
|
readonly reconnects?: Reconnects;
|
|
777
|
+
/**
|
|
778
|
+
* These are the exact counts of the lines of the target in each run. A count that differs is a
|
|
779
|
+
* finding of its run. A measurement-only cell with a reference and counts gets a decision from them.
|
|
780
|
+
*/
|
|
781
|
+
readonly counts?: readonly LineCount[];
|
|
782
|
+
/**
|
|
783
|
+
* This record is the law of an accept queue: its limit, its count, its counters, and what each
|
|
784
|
+
* connection sees. A value outside it is a finding of its run.
|
|
785
|
+
*/
|
|
786
|
+
readonly queue?: QueuePrediction;
|
|
787
|
+
/**
|
|
788
|
+
* This record is the law of a port range: the count of connects, the ports that they use, and
|
|
789
|
+
* when a held port comes back. A value outside it is a finding of its run.
|
|
790
|
+
*/
|
|
791
|
+
readonly ports?: PortsPrediction;
|
|
687
792
|
};
|
|
793
|
+
/** A queue band is a time from one line of a queue cell to another line, with its two bounds. */
|
|
794
|
+
interface QueueBand {
|
|
795
|
+
readonly low: Quantity;
|
|
796
|
+
readonly high: Quantity;
|
|
797
|
+
}
|
|
798
|
+
/** This prediction tells how one connection of a queue cell ends. */
|
|
799
|
+
interface QueueEnd {
|
|
800
|
+
readonly errno: string;
|
|
801
|
+
/** This name is the line of the connection that the time of the end counts from. */
|
|
802
|
+
readonly from: "dial" | "connected" | "sent";
|
|
803
|
+
readonly within: QueueBand;
|
|
804
|
+
/**
|
|
805
|
+
* This band is the time from the last retransmission of the connection to its end. With an
|
|
806
|
+
* `expiry`, the retransmission before the last comes before the expiry, and the last comes after it.
|
|
807
|
+
*/
|
|
808
|
+
readonly retransmitted?: QueueBand;
|
|
809
|
+
}
|
|
810
|
+
/** This prediction tells what one connection of a queue cell sees, by its name in the client. */
|
|
811
|
+
interface QueueConnection {
|
|
812
|
+
readonly name: string;
|
|
813
|
+
/** This choice says if the connect of the connection succeeds. */
|
|
814
|
+
readonly connects: boolean;
|
|
815
|
+
/** These bands are the waits of the SYN: the first from the dial, each next one from the SYN before it. */
|
|
816
|
+
readonly syns?: readonly QueueBand[];
|
|
817
|
+
/** These bands are the waits of each SYN-ACK sent again: the first from the dial, each next one from the SYN-ACK before it. */
|
|
818
|
+
readonly synAcks?: readonly QueueBand[];
|
|
819
|
+
/** This prediction tells how the connection ends. It is null for a connection that lives to the end of the run. */
|
|
820
|
+
readonly ends: QueueEnd | null;
|
|
821
|
+
/** This count is the segments that the peer has not acknowledged at the end of the run. */
|
|
822
|
+
readonly unacked?: number;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* This prediction is the law of an accept queue. It reads the lines of the queue client and of the
|
|
826
|
+
* queue target. The counters count from the `opened` line of the run.
|
|
827
|
+
*/
|
|
828
|
+
interface QueuePrediction {
|
|
829
|
+
/** This count is the limit of the queue: `sk_max_ack_backlog`, as TCP_INFO of the listener gives it. */
|
|
830
|
+
readonly max: number;
|
|
831
|
+
/** This count is the most connections that the queue holds at one time. */
|
|
832
|
+
readonly held: number;
|
|
833
|
+
/** This count is the connections that connect, and the kernel of the client tells this. */
|
|
834
|
+
readonly connected: number;
|
|
835
|
+
/** These counts are the bounds of `ListenOverflows`. */
|
|
836
|
+
readonly overflows: {
|
|
837
|
+
readonly low: number;
|
|
838
|
+
readonly high: number;
|
|
839
|
+
};
|
|
840
|
+
/** This count is `EmbryonicRsts`: the handshakes that the target reset. */
|
|
841
|
+
readonly resets: number;
|
|
842
|
+
/** These predictions are the connections with a law of their own. */
|
|
843
|
+
readonly connections: readonly QueueConnection[];
|
|
844
|
+
/** A run where this connection did not take the last place in the queue is void: its burst did not land in its sequence. */
|
|
845
|
+
readonly admitted?: string;
|
|
846
|
+
/** This record tells when the kernel of the target forgets the handshakes: the `TCPTimeouts` of their last firings, from the first dial of the burst. */
|
|
847
|
+
readonly expiry?: {
|
|
848
|
+
readonly timeouts: number;
|
|
849
|
+
readonly within: QueueBand;
|
|
850
|
+
};
|
|
851
|
+
/** This record tells when the count of the queue gets back to `count`, from the first dial of the burst, after an accept. */
|
|
852
|
+
readonly rise?: {
|
|
853
|
+
readonly count: number;
|
|
854
|
+
readonly within: QueueBand;
|
|
855
|
+
};
|
|
856
|
+
/** This count is the connections that the target accepted in the run. */
|
|
857
|
+
readonly drained?: number;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* This prediction is the law of a port range. It reads the lines of the ports client. In each round
|
|
861
|
+
* the client connects to each destination until a connect fails with EADDRNOTAVAIL.
|
|
862
|
+
*/
|
|
863
|
+
interface PortsPrediction {
|
|
864
|
+
/** This port is the low end of the range. */
|
|
865
|
+
readonly low: number;
|
|
866
|
+
/** This count is the ports of the range. */
|
|
867
|
+
readonly size: number;
|
|
868
|
+
/** This count is the rounds of each run. */
|
|
869
|
+
readonly rounds: number;
|
|
870
|
+
/** This count is the destinations of each round. */
|
|
871
|
+
readonly destinations: number;
|
|
872
|
+
/** These counts are the only connects that one destination can get in one round. */
|
|
873
|
+
readonly counts: readonly number[];
|
|
874
|
+
/** These offsets from the low port are the only ports that the run uses. It uses each of them. */
|
|
875
|
+
readonly seen: readonly number[];
|
|
876
|
+
/** This time is the longest connect that fails. */
|
|
877
|
+
readonly refusedWithin: Quantity;
|
|
878
|
+
/** This record is the band of the share of the rounds that get `count` connects. */
|
|
879
|
+
readonly share?: {
|
|
880
|
+
readonly count: number;
|
|
881
|
+
readonly low: number;
|
|
882
|
+
readonly high: number;
|
|
883
|
+
};
|
|
884
|
+
/** This choice says that the ports of the parity of the low port come first in each round. */
|
|
885
|
+
readonly lowFirst?: boolean;
|
|
886
|
+
/** This band is the time from the `closing` line to each `freed` line: when each held port comes back. */
|
|
887
|
+
readonly freed?: QueueBand;
|
|
888
|
+
/** This count is the segments that the sockets of the client send again. */
|
|
889
|
+
readonly retransmissions?: number;
|
|
890
|
+
}
|
|
688
891
|
/** A target is the server that a cell connects to: a stock image. */
|
|
689
892
|
interface Target {
|
|
690
893
|
/** This image is a stock image, pinned by digest. */
|
|
@@ -740,7 +943,7 @@ interface Target {
|
|
|
740
943
|
* grpc-go and `etcd` is etcd on Alpine: each is a subject of a target (`Target.subject`).
|
|
741
944
|
* `docker-proxy` is the userland proxy of Docker Engine 29.1.5, the program of a proxy hop.
|
|
742
945
|
*/
|
|
743
|
-
type Shipped = "harness" | "node" | "go-http" | "node-http" | "mongo" | "pg" | "hikari" | "amqp" | "grpc" | "grpc-go" | "etcd" | "router" | "pgbouncer" | "pgpool" | "docker-proxy";
|
|
946
|
+
type Shipped = "harness" | "node" | "go-http" | "node-http" | "mongo" | "pg" | "hikari" | "amqp" | "kafka" | "grpc" | "grpc-go" | "etcd" | "router" | "pgbouncer" | "pgpool" | "docker-proxy";
|
|
744
947
|
/**
|
|
745
948
|
* An image source tells where an image comes from, as data. The harness resolves each kind to an
|
|
746
949
|
* image that it can start:
|
|
@@ -912,19 +1115,26 @@ interface Cell {
|
|
|
912
1115
|
/**
|
|
913
1116
|
* This driver replaces the TCP client of the harness, with its own options. The driver then
|
|
914
1117
|
* ignores `keepalive` and `userTimeout`:
|
|
915
|
-
* - ioredis 6 on Node, whose keepAlive is the keepalive delay of the socket.
|
|
1118
|
+
* - ioredis 6 on Node, whose keepAlive is the keepalive delay of the socket. In a group cell,
|
|
1119
|
+
* its write is `SET w:<n> <n>`, or `RPUSH w <n>` with `op: "rpush"`, thus a list keeps each
|
|
1120
|
+
* copy. `window` is the count of writers, each with one write in flight: 1 at the default.
|
|
916
1121
|
* - the MongoDB driver on Node, against the replica set `set`. Its operation is a write, or
|
|
917
1122
|
* the read of a long document (`read-long`). `Cell.driverOptions` are its options.
|
|
918
1123
|
* - node-postgres on Node, as a `pg.Pool` against one server. Its operation is `query`, or
|
|
919
1124
|
* `SELECT 1`. `Cell.driverOptions` are the options of its pool.
|
|
920
1125
|
* - HikariCP over pgjdbc on Java, as a `HikariDataSource` against one server. Its operation is
|
|
921
|
-
* `query`, or `SELECT 1`. `Cell.driverOptions` are the properties of HikariConfig
|
|
922
|
-
* with the prefix `dataSource.` is a property of pgjdbc
|
|
1126
|
+
* `query`, or `SELECT 1`. `Cell.driverOptions` are the properties of HikariConfig. A name
|
|
1127
|
+
* with the prefix `dataSource.` is a property of pgjdbc, and a name with the prefix `system.`
|
|
1128
|
+
* is a system property of the JVM.
|
|
923
1129
|
*
|
|
924
1130
|
* A pg or hikari driver can send a different text in flight (`inflight`): the second query of
|
|
925
1131
|
* an in-flight run. The first query then makes the pool ready at once, and only the query in
|
|
926
1132
|
* flight is slow, for example `SELECT pg_sleep(20)`.
|
|
927
1133
|
*
|
|
1134
|
+
* A hikari driver can send a burst in place of one query in flight. A burst is `count` queries,
|
|
1135
|
+
* query 2 and the next ones. Each one has its own thread, and it starts `gap` after the one before
|
|
1136
|
+
* it. The client writes a `burst` line at the end of the last one, with its counts.
|
|
1137
|
+
*
|
|
928
1138
|
* A grpc driver is grpc-js on Node, with one channel. Its operation is a unary call of
|
|
929
1139
|
* grpc.health.v1.Health/Check. `Cell.driverOptions` are its channel options (`grpc.*`) and
|
|
930
1140
|
* `deadline_ms`. With `hold`, the call in flight asks the server of the harness to hold its reply
|
|
@@ -933,6 +1143,8 @@ interface Cell {
|
|
|
933
1143
|
readonly driver?: {
|
|
934
1144
|
readonly name: "ioredis";
|
|
935
1145
|
readonly keepAlive: Quantity;
|
|
1146
|
+
readonly op?: "set" | "rpush";
|
|
1147
|
+
readonly window?: number;
|
|
936
1148
|
} | {
|
|
937
1149
|
readonly name: "mongodb";
|
|
938
1150
|
readonly set: string;
|
|
@@ -945,6 +1157,10 @@ interface Cell {
|
|
|
945
1157
|
readonly name: "hikari";
|
|
946
1158
|
readonly query?: string;
|
|
947
1159
|
readonly inflight?: string;
|
|
1160
|
+
readonly burst?: {
|
|
1161
|
+
readonly count: number;
|
|
1162
|
+
readonly gap: Quantity;
|
|
1163
|
+
};
|
|
948
1164
|
} | {
|
|
949
1165
|
readonly name: "amqp";
|
|
950
1166
|
} | {
|
|
@@ -1024,6 +1240,14 @@ interface Cell {
|
|
|
1024
1240
|
* record, and the cluster does not read it.
|
|
1025
1241
|
*/
|
|
1026
1242
|
readonly cluster?: Cluster;
|
|
1243
|
+
/**
|
|
1244
|
+
* This Kafka group replaces the target: brokers of Apache Kafka with one controller. The client is
|
|
1245
|
+
* then the producer of kafka-clients, with the settings of `driverOptions`. `target` stays for the
|
|
1246
|
+
* record, and the group does not read it.
|
|
1247
|
+
*/
|
|
1248
|
+
readonly kafka?: Kafka;
|
|
1249
|
+
/** This pair adds a standby to the target (`Pair`). The client is then node-postgres, in a stream of K writers. */
|
|
1250
|
+
readonly pair?: Pair;
|
|
1027
1251
|
/** This place is the node of the client in a cluster: the node of the leader, or of a follower. Without it, the node of a follower. */
|
|
1028
1252
|
readonly clientAt?: ClusterClientAt;
|
|
1029
1253
|
/**
|
|
@@ -1031,6 +1255,11 @@ interface Cell {
|
|
|
1031
1255
|
* that instant, on the clock of the VM. On a direct pair, a frozen target thaws after it.
|
|
1032
1256
|
*/
|
|
1033
1257
|
readonly hold?: Quantity;
|
|
1258
|
+
/**
|
|
1259
|
+
* This time is how much longer a run of a replica set continues after the lift. A prediction of
|
|
1260
|
+
* no failover after a short hold needs it: the run must outlast the timers of an election.
|
|
1261
|
+
*/
|
|
1262
|
+
readonly watch?: Quantity;
|
|
1034
1263
|
/** This flag makes a planned failover coordinated: `SENTINEL FAILOVER <group> COORDINATED`. */
|
|
1035
1264
|
readonly coordinated?: boolean;
|
|
1036
1265
|
/**
|
|
@@ -1228,8 +1457,10 @@ interface RunGroup {
|
|
|
1228
1457
|
readonly timeline: readonly SentinelEvent[];
|
|
1229
1458
|
/** This name is the member that was the primary when the group converged after the run, or null when it did not converge. */
|
|
1230
1459
|
readonly after: string | null;
|
|
1231
|
-
/** These are the sequence numbers that the group holds after the run, when it converged, or null. */
|
|
1460
|
+
/** These are the sequence numbers that the group holds after the run, one entry for each copy, when it converged, or null. */
|
|
1232
1461
|
readonly holds: readonly number[] | null;
|
|
1462
|
+
/** These are the sequence numbers that the old primary of a pair holds after its crash recovery. */
|
|
1463
|
+
readonly oldHolds?: readonly number[];
|
|
1233
1464
|
/** This record tells what a recovery drill did. */
|
|
1234
1465
|
readonly drill?: RunDrill;
|
|
1235
1466
|
}
|
|
@@ -1282,5 +1513,5 @@ interface RunTopology {
|
|
|
1282
1513
|
}>>;
|
|
1283
1514
|
}
|
|
1284
1515
|
//#endregion
|
|
1285
|
-
export {
|
|
1286
|
-
//# sourceMappingURL=cell-
|
|
1516
|
+
export { Replies as $, LineCount as A, Prediction as B, HopSignal as C, latestEndOf as Ct, Jitter as D, ImageSource as E, Observer as F, QueueConnection as G, ProxyHop as H, Outcomes as I, Reconnects as J, QueueEnd as K, Pair as L, MiddleboxEvent as M, MiddleboxExpectation as N, Kafka as O, MiddleboxForm as P, ReplicaSetStage as Q, PinnedImage as R, HopPlace as S, isHopPlace as St, HttpProbe as T, ProxyTime as U, ProbeEvent as V, QueueBand as W, Reference as X, Recovers as Y, ReplicaSet as Z, GroupPlace as _, Subject as _t, Cluster as a, RunDrill as at, Hop as b, Toxic as bt, CountBand as c, RunPart as ct, DriverStep as d, SentinelEvent as dt, RequestPrediction as et, Fabric as f, SentinelStage as ft, Group as g, StageBand as gt, FaultPlace as h, Stage as ht, ClientImage as i, RunBackup as it, LineMatch as j, KernelRecord as k, Drill as l, RunRoute as lt, Fault as m, ShippedImage as mt, Cell as n, RttRange as nt, ClusterClientAt as o, RunFabric as ot, FabricManifest as p, Shipped as pt, QueuePrediction as q, Client as r, Run as rt, ClusterStage as s, RunGroup as st, BuildImage as t, RouterHop as tt, DriverBand as u, RunTopology as ut, GroupPrediction as v, Target as vt, HopWrite as w, HopEvent as x, WsProbe as xt, GroupRole as y, Topology as yt, PortsPrediction as z };
|
|
1517
|
+
//# sourceMappingURL=cell-DZm8zXN4.d.ts.map
|
|
@@ -191,6 +191,11 @@ const SHIPPED = {
|
|
|
191
191
|
dockerfile: "harness/image/amqp/Dockerfile",
|
|
192
192
|
from: ["harness/image/amqp"]
|
|
193
193
|
},
|
|
194
|
+
kafka: {
|
|
195
|
+
name: "loom-harness-kafka",
|
|
196
|
+
dockerfile: "harness/image/kafka/Dockerfile",
|
|
197
|
+
from: ["harness/image/kafka"]
|
|
198
|
+
},
|
|
194
199
|
grpc: {
|
|
195
200
|
name: "loom-harness-grpc",
|
|
196
201
|
dockerfile: "harness/image/grpc/Dockerfile",
|
|
@@ -624,6 +629,8 @@ const pgClientImage = () => imageOf(shipped("pg"));
|
|
|
624
629
|
const hikariClientImage = () => imageOf(shipped("hikari"));
|
|
625
630
|
/** This function gives the image of the RabbitMQ client: amqplib, with the client and the admin of a cluster cell. */
|
|
626
631
|
const amqpClientImage = () => imageOf(shipped("amqp"));
|
|
632
|
+
/** This function gives the image of the Kafka client: kafka-clients on Java, with the client and the admin of a Kafka cell. */
|
|
633
|
+
const kafkaClientImage = () => imageOf(shipped("kafka"));
|
|
627
634
|
/** This function gives the image of the gRPC client of the harness: grpc-js 1.14.6 on Node 24.21.0, with its client. */
|
|
628
635
|
const grpcClientImage = () => imageOf(shipped("grpc"));
|
|
629
636
|
/**
|
|
@@ -634,6 +641,6 @@ const targetSourceOf = (target) => target.subject ?? target.image;
|
|
|
634
641
|
/** This function gives the image that a target operates: the image of its subject, or its stock image. */
|
|
635
642
|
const targetImageOf = (target) => imageOf(targetSourceOf(target));
|
|
636
643
|
//#endregion
|
|
637
|
-
export {
|
|
644
|
+
export { unlockedManifests as A, programOf as C, subjectProblems as D, subjectClient as E, jarVersions as F, locksOf as I, npmVersions as L, LOCK_NAMES as M, cargoVersions as N, targetImageOf as O, goVersions as P, versionsOf as R, planOf as S, shippedSource as T, kafkaClientImage as _, SHIPPED_NAMES as a, nodeHttpImage as b, buildRecordOf as c, harnessImage as d, harnessVersion as f, inContext as g, imageProblems as h, SHIPPED_CLIENTS as i, unpinnedBases as j, targetSourceOf as k, goHttpImage as l, imageOf as m, PINNED as n, amqpClientImage as o, hikariClientImage as p, REPOSITORY as r, basesOf as s, CLIENT_NAMES as t, grpcClientImage as u, mongoClientImage as v, routerImage as w, pgClientImage as x, nodeClientImage as y };
|
|
638
645
|
|
|
639
|
-
//# sourceMappingURL=image-
|
|
646
|
+
//# sourceMappingURL=image-EZ8ioZ3L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-EZ8ioZ3L.js","names":[],"sources":["../src/locks.ts","../src/image.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\n/**\n * This module reads the lockfiles of a build context for the manifest of a run. A lockfile pins each\n * package that a build installs, thus its sha256 pins the packages that the image holds. The\n * versions of the packages that each manifest names directly are the part that a person reads.\n */\n\n/** A context file is a file of a build context: its path in the context, and its content. */\nexport interface ContextFile {\n readonly at: string;\n readonly content: Buffer | string;\n}\n\n/**\n * These are the names of the lockfiles that the harness records. A `go.mod` is the lockfile of a Go\n * module, because its `require` lines give exact versions. For them, Go selects the same packages\n * each time, and its checksum database examines each package.\n */\nexport const LOCK_NAMES: ReadonlySet<string> = new Set([\n \"package-lock.json\",\n \"npm-lock.json\",\n \"npm-shrinkwrap.json\",\n \"pnpm-lock.yaml\",\n \"yarn.lock\",\n \"bun.lock\",\n \"Cargo.lock\",\n \"go.mod\",\n \"go.sum\",\n \"jars.sha256\",\n]);\n\nconst nameOf = (at: string): string => at.split(\"/\").at(-1)!;\nconst folderOf = (at: string): string => at.split(\"/\").slice(0, -1).join(\"/\");\nconst textOf = (content: Buffer | string): string => (typeof content === \"string\" ? content : content.toString(\"utf8\"));\n\n/** This function gives each lockfile of a context by its path, with the sha256 of its content in hex. */\nexport const locksOf = (files: readonly ContextFile[]): Record<string, string> =>\n Object.fromEntries(\n files\n .filter((file) => LOCK_NAMES.has(nameOf(file.at)))\n .map((file) => [file.at, createHash(\"sha256\").update(file.content).digest(\"hex\")] as const)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n );\n\n/** This function gives the version of each direct dependency of an npm lockfile (lockfileVersion 2 or 3). */\nexport const npmVersions = (lock: string): Record<string, string> => {\n const parsed = JSON.parse(lock) as { packages?: Record<string, { version?: string; dependencies?: Record<string, string>; optionalDependencies?: Record<string, string> }> };\n const packages = parsed.packages ?? {};\n const root = packages[\"\"] ?? {};\n const names = [...Object.keys(root.dependencies ?? {}), ...Object.keys(root.optionalDependencies ?? {})];\n const found: Record<string, string> = {};\n for (const name of names) {\n const version = packages[`node_modules/${name}`]?.version;\n if (version !== undefined) found[name] = version;\n }\n return found;\n};\n\n/** This function gives the packages of a Cargo.lock: the name, the version and the dependencies of each. */\nconst cargoPackages = (lock: string): Array<{ name: string; version: string; dependencies: string[] }> =>\n lock\n .split(/^\\[\\[package\\]\\]\\s*$/m)\n .slice(1)\n .flatMap((block) => {\n const name = /^name = \"([^\"]+)\"$/m.exec(block)?.[1];\n const version = /^version = \"([^\"]+)\"$/m.exec(block)?.[1];\n if (name === undefined || version === undefined) return [];\n const list = /^dependencies = \\[([^\\]]*)\\]/m.exec(block)?.[1] ?? \"\";\n const dependencies = [...list.matchAll(/\"([^\"]+)\"/g)].map((match) => match[1]!);\n return [{ name, version, dependencies }];\n });\n\n/**\n * This function gives the version of each direct dependency of the root package of a Cargo.lock: the\n * package that the Cargo.toml beside it names. An entry of a dependency list is a name, or a name\n * and a version when the lock holds two versions of that name.\n */\nexport const cargoVersions = (lock: string, manifest: string): Record<string, string> => {\n const rootName = /^\\[package\\][^[]*?^name\\s*=\\s*\"([^\"]+)\"/ms.exec(manifest)?.[1];\n const packages = cargoPackages(lock);\n const root = packages.find((each) => each.name === rootName);\n if (root === undefined) return {};\n const found: Record<string, string> = {};\n for (const entry of root.dependencies) {\n const [name, version] = entry.split(\" \");\n const pinned = version ?? packages.find((each) => each.name === name)?.version;\n if (name !== undefined && pinned !== undefined) found[name] = pinned;\n }\n return found;\n};\n\n/** This function gives the version of each direct requirement of a go.mod. A requirement that is `// indirect` is not direct. */\nexport const goVersions = (mod: string): Record<string, string> => {\n const found: Record<string, string> = {};\n const lines = mod.replace(/^require\\s*\\(([^)]*)\\)/gm, (_all, block: string) =>\n block\n .split(/\\r?\\n/)\n .map((line) => `require ${line.trim()}`)\n .join(\"\\n\"),\n );\n for (const line of lines.split(/\\r?\\n/)) {\n const match = /^require\\s+(\\S+)\\s+(v\\S+)(.*)$/.exec(line.trim());\n if (match === null || /\\/\\/\\s*indirect/.test(match[3]!)) continue;\n found[match[1]!] = match[2]!;\n }\n return found;\n};\n\n/** This function gives the version of each jar of a list of jars by their SHA-256: `<hex> HikariCP-7.1.0.jar` gives HikariCP 7.1.0. */\nexport const jarVersions = (list: string): Record<string, string> => {\n const found: Record<string, string> = {};\n for (const line of list.split(/\\r?\\n/)) {\n const match = /^[0-9a-f]{64}\\s+\\*?(.+?)-(\\d[^-]*(?:-[A-Za-z0-9.]+)?)\\.jar$/.exec(line.trim());\n if (match !== null) found[match[1]!] = match[2]!;\n }\n return found;\n};\n\n/**\n * This function gives the version of each package that the manifests of a context name directly,\n * by its name. It reads each npm lockfile, Cargo.lock, go.mod and list of jars. A package of a\n * workspace in the context is a package of the image too, with the version of its package.json.\n * Such a package has a name and a version, and it is not private. The record has the sha256 of\n * the lockfile of a pnpm, yarn or bun workspace (`locksOf`). This function does not read versions\n * from such a lockfile.\n */\nexport const versionsOf = (files: readonly ContextFile[]): Record<string, string> => {\n const byPath = new Map(files.map((file) => [file.at, textOf(file.content)] as const));\n const found: Record<string, string> = {};\n for (const [at, text] of [...byPath].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {\n const name = nameOf(at);\n const folder = folderOf(at);\n const beside = (other: string): string | undefined => byPath.get(folder === \"\" ? other : `${folder}/${other}`);\n if (name === \"package-lock.json\" || name === \"npm-lock.json\" || name === \"npm-shrinkwrap.json\") Object.assign(found, npmVersions(text));\n else if (name === \"Cargo.lock\") Object.assign(found, cargoVersions(text, beside(\"Cargo.toml\") ?? \"\"));\n else if (name === \"go.mod\") Object.assign(found, goVersions(text));\n else if (name === \"jars.sha256\") Object.assign(found, jarVersions(text));\n else if (name === \"package.json\") {\n const manifest = JSON.parse(text) as { name?: string; version?: string; private?: boolean };\n if (manifest.private !== true && manifest.name !== undefined && manifest.version !== undefined) found[manifest.name] = manifest.version;\n }\n }\n return found;\n};\n","import { execFile } from \"node:child_process\";\nimport { 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\";\nimport { LOCK_NAMES, locksOf, versionsOf, type ContextFile } from \"./locks.ts\";\n\n/** These are the manifests of packages that `versionsOf` reads beside the lockfiles. */\nconst READ_FOR_VERSIONS: ReadonlySet<string> = new Set([\"package.json\", \"Cargo.toml\"]);\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 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 // the Kafka client: kafka-clients 4.3.2 on Temurin 21.0.12.1, with its admin: its jars by their SHA-256\n kafka: { name: \"loom-harness-kafka\", dockerfile: \"harness/image/kafka/Dockerfile\", from: [\"harness/image/kafka\"] },\n // the gRPC client: grpc-js 1.14.6 on Node 24.21.0\n grpc: { name: \"loom-harness-grpc\", dockerfile: \"harness/image/grpc/Dockerfile\", from: [\"harness/image/grpc\"] },\n // the gRPC server: a server of grpc-go v1.84.0, built with Go 1.27.2, that can hold a reply\n \"grpc-go\": { name: \"loom-harness-grpc-go\", dockerfile: \"harness/image/grpc-go/Dockerfile\", from: [\"harness/image/grpc-go\"] },\n // etcd v3.6.15 from its image, on Alpine: a shell to hold it, and its binaries unchanged\n etcd: { name: \"loom-harness-etcd\", dockerfile: \"harness/image/etcd/Dockerfile\", from: [\"harness/image/etcd\"] },\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 // docker-proxy of Docker Engine 29.1.5, a proxy hop: its binary from the static bundle of Docker, by its SHA-256\n \"docker-proxy\": { name: \"loom-harness-docker-proxy\", dockerfile: \"harness/image/docker-proxy/Dockerfile\", from: [\"harness/image/docker-proxy\"] },\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 function gives the source of a shipped image. */\nexport const shippedSource = (name: Shipped): ImageSource => ({ kind: \"shipped\", name });\nconst shipped = shippedSource;\n\n/** This function gives the version of the package of the harness, from its package.json. */\nexport const harnessVersion = (): string =>\n (JSON.parse(readFileSync(join(ROOTS[\"harness\"]!, \"package.json\"), \"utf8\")) as { version: string }).version;\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 grpc: { image: shipped(\"grpc\"), command: [\"node\", \"/opt/harness/grpc/client-grpc.mjs\"] },\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[] => basesOf(dockerfile).filter((image) => !PINNED.test(image));\n\n/**\n * This function gives each base of a Dockerfile, in the sequence of the file. A base is the image of\n * a `FROM` that is not `scratch` or an earlier stage by its name.\n */\nexport const basesOf = (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())) 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 /** This record tells what the image holds, for the manifest of a run. */\n readonly record: BuildRecord;\n };\n\n/**\n * A build record tells what an image that the harness builds holds, for the manifest of a run. The\n * tag is a sha256 of each file of the context. The bases are pinned by digest, and each lockfile\n * has its sha256. Thus the record pins each program of the image. The versions are the part that a\n * person reads.\n */\nexport interface BuildRecord {\n readonly tag: string;\n /** These are the bases of its Dockerfile, each pinned by digest (`basesOf`). */\n readonly bases: readonly string[];\n /** These are the lockfiles of its context by their path, each with the sha256 of its content (`locksOf`). */\n readonly locks: Readonly<Record<string, string>>;\n /** These are the versions of the packages that its manifests name directly, by their names (`versionsOf`). */\n readonly versions: Readonly<Record<string, 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 bases = basesOf(readFileSync(docker.file, \"utf8\"));\n const unpinned = bases.filter((image) => !PINNED.test(image));\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 const read: ContextFile[] = [];\n for (const { file, at } of files) {\n const content = readFileSync(file);\n hash.update(at);\n hash.update(content);\n if (LOCK_NAMES.has(posix.basename(at)) || READ_FOR_VERSIONS.has(posix.basename(at))) read.push({ at, content });\n }\n const tag = `${name}:${hash.digest(\"hex\").slice(0, 12)}`;\n const record: BuildRecord = { tag, bases, locks: locksOf(read), versions: versionsOf(read) };\n const plan: ImagePlan = { kind: \"build\", tag, dockerfile, files, record };\n if (source.kind === \"shipped\") shippedPlans.set(source.name, plan);\n return plan;\n};\n\n/** These counts are how many lines of the output of a failed build its error holds: its error lines, then its last lines. */\nconst BUILD_ERRORS = 10;\nconst BUILD_TAIL = 15;\n\n/**\n * This function gives the lines of the output of a failed build that its error holds. They are each\n * line that names an error, for example the diagnostic of a compiler, then the last lines.\n */\nexport const buildFailure = (output: string): string => {\n const lines = output.trim().split(/\\r?\\n/);\n const tail = lines.slice(-BUILD_TAIL);\n const errors = lines.slice(0, -BUILD_TAIL).filter((line) => /\\berror\\b/i.test(line)).slice(0, BUILD_ERRORS);\n return [...errors, ...(errors.length > 0 ? [\"...\"] : []), ...tail].join(\"\\n\");\n};\n\n/**\n * This function builds a staged context with the CLI of Docker, thus with BuildKit where the CLI has\n * it. Testcontainers reads nothing of the output of a build, and it fails with \"Failed to build\n * image\" only. The error of a failed build here holds its error lines and the end of its output.\n */\nconst dockerBuild = (context: string, plan: Extract<ImagePlan, { readonly kind: \"build\" }>): Promise<void> =>\n new Promise((resolve, reject) => {\n const args = [\"build\", \"--progress=plain\", \"--tag\", plan.tag, \"--file\", join(context, plan.dockerfile), context];\n execFile(\"docker\", args, { maxBuffer: 64 * 1024 * 1024 }, (error, _stdout, stderr) => {\n if (error === null) {\n resolve();\n return;\n }\n reject(new Error(`docker build of ${plan.tag} failed (${error.message.split(\"\\n\")[0]}):\\n${buildFailure(String(stderr))}`));\n });\n });\n\nconst built = new Map<string, Promise<string>>();\n\n/**\n * These are the waits before each next try of a build that fails, in milliseconds. A fault of a\n * registry or of the network often stops in seconds. In run 39278390448 of the cells, two runners\n * failed a build in the same second, and the same builds then passed.\n */\nexport const BUILD_WAITS: readonly number[] = [5_000, 20_000];\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 tries again after\n * each wait (`waits`), and it writes each failure to stderr. A build that fails each try leaves the\n * cache. 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 waits: readonly number[] = BUILD_WAITS,\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 for (let attempt = 0; ; attempt += 1) {\n try {\n await builder(context, plan);\n break;\n } catch (error) {\n const wait = waits[attempt];\n if (wait === undefined) throw error;\n process.stderr.write(`the build of ${plan.tag} failed (${error instanceof Error ? error.message : String(error)}): try ${attempt + 2} after ${wait} ms\\n`);\n await new Promise((resolve) => setTimeout(resolve, wait));\n }\n }\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. A build goes through the CLI\n * of Docker.\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/**\n * This function gives the build record of the image of a source, for the manifest of a run. A\n * stock image by its name, or a pinned image, is a pull, and it has no build record.\n */\nexport const buildRecordOf = (source: string | ImageSource): BuildRecord | undefined => {\n const plan = planOf(source);\n return plan.kind === \"build\" ? plan.record : undefined;\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 RabbitMQ client: amqplib, with the client and the admin of a cluster cell. */\nexport const amqpClientImage = (): Promise<string> => imageOf(shipped(\"amqp\"));\n/** This function gives the image of the Kafka client: kafka-clients on Java, with the client and the admin of a Kafka cell. */\nexport const kafkaClientImage = (): Promise<string> => imageOf(shipped(\"kafka\"));\n\n/** This function gives the image of the gRPC client of the harness: grpc-js 1.14.6 on Node 24.21.0, with its client. */\nexport const grpcClientImage = (): Promise<string> => imageOf(shipped(\"grpc\"));\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`), or its stock image.\n */\nexport const targetSourceOf = (target: Target): string | ImageSource => target.subject ?? target.image;\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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"}
|