@mark1russell7/harness 0.11.3 → 0.11.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 +3 -0
- package/build/{cell-BmSnJJtn.d.ts → cell-Di_YHiuB.d.ts} +10 -4
- package/build/{image-Q_fs3rCC.js → image-BfuXg-5t.js} +19 -2
- package/build/image-BfuXg-5t.js.map +1 -0
- package/build/index.d.ts +3 -1
- package/build/index.js +3 -3
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +1 -1
- package/build/schema.js +1 -1
- package/image/hikari/ClientHikari.java +424 -0
- package/image/hikari/Dockerfile +19 -0
- package/image/hikari/jars.sha256 +3 -0
- package/package.json +4 -1
- package/build/image-Q_fs3rCC.js.map +0 -1
package/build/schema.d.ts
CHANGED
package/build/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as subjectProblems, a as SHIPPED_NAMES, d as imageProblems, t as CLIENT_NAMES } from "./image-BfuXg-5t.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
//#region src/schema.ts
|
|
4
4
|
/** This function adds each problem that a check finds as an issue of a schema. */
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// client-hikari HOST PORT --mode idle|inflight|stream [--interval-ms N] [--max-s N] [--go PATH]
|
|
2
|
+
// [--query TEXT] [--before] [--options JSON]
|
|
3
|
+
//
|
|
4
|
+
// The client of a HikariCP cell: HikariCP 7.1.0 over pgjdbc 42.7.13, on Java 21. Each event is a
|
|
5
|
+
// JSON line with `ev` and `t`, the time on CLOCK_MONOTONIC in nanoseconds (System.nanoTime, which
|
|
6
|
+
// the JVM reads from it on Linux). The prober stamps the records of the kernel on the same clock,
|
|
7
|
+
// and the injector stamps each fault on it.
|
|
8
|
+
//
|
|
9
|
+
// The lines:
|
|
10
|
+
// - start: the versions of HikariCP, pgjdbc and Java, the mode and the options.
|
|
11
|
+
// - connecting: the start of the pool and of its first connection. Its stamp is the stamp of the
|
|
12
|
+
// first sent line.
|
|
13
|
+
// - socket: each connection that the client borrows, with its local address, at its first borrow.
|
|
14
|
+
// - connected: the first socket. The TCP client of the harness names its own socket so.
|
|
15
|
+
// - pool: each change of the counts of the pool: total, active, idle and waiting.
|
|
16
|
+
// - ready: the reply to the first query: the connection is idle in the pool (idle, inflight), or
|
|
17
|
+
// the stream starts (stream).
|
|
18
|
+
// - sent, ack, fail: each query by its sequence number, with its error.
|
|
19
|
+
// - data, error: the end of the query in flight, as the TCP client of the harness tells it.
|
|
20
|
+
// - done, deadline: the end of the client.
|
|
21
|
+
//
|
|
22
|
+
// The modes are those of the pg client. idle sends one query, closes its connection (the
|
|
23
|
+
// connection goes back to the pool), then holds until --max-s. inflight sends one query, waits for
|
|
24
|
+
// the file --go, sends one more query into the fault, and holds after its end. stream sends a query
|
|
25
|
+
// each --interval-ms after the reply before. --before starts the client into a fault that holds
|
|
26
|
+
// already: the start of its pool meets it.
|
|
27
|
+
//
|
|
28
|
+
// The options are one flat JSON object. A name with the prefix `dataSource.` is a property of
|
|
29
|
+
// pgjdbc. Each other name is a property of HikariConfig. A cell gives each option that it changes,
|
|
30
|
+
// and HikariCP and pgjdbc give the others.
|
|
31
|
+
|
|
32
|
+
import com.zaxxer.hikari.HikariConfig;
|
|
33
|
+
import com.zaxxer.hikari.HikariDataSource;
|
|
34
|
+
import com.zaxxer.hikari.HikariPoolMXBean;
|
|
35
|
+
import java.io.FileDescriptor;
|
|
36
|
+
import java.io.FileOutputStream;
|
|
37
|
+
import java.io.PrintStream;
|
|
38
|
+
import java.lang.reflect.Field;
|
|
39
|
+
import java.net.Socket;
|
|
40
|
+
import java.nio.charset.StandardCharsets;
|
|
41
|
+
import java.nio.file.Files;
|
|
42
|
+
import java.nio.file.Path;
|
|
43
|
+
import java.sql.Connection;
|
|
44
|
+
import java.sql.ResultSet;
|
|
45
|
+
import java.sql.SQLException;
|
|
46
|
+
import java.sql.Statement;
|
|
47
|
+
import java.util.LinkedHashMap;
|
|
48
|
+
import java.util.List;
|
|
49
|
+
import java.util.Map;
|
|
50
|
+
import java.util.Properties;
|
|
51
|
+
import java.util.Set;
|
|
52
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
53
|
+
import org.postgresql.core.PGStream;
|
|
54
|
+
import org.postgresql.jdbc.PgConnection;
|
|
55
|
+
|
|
56
|
+
public final class ClientHikari {
|
|
57
|
+
private static final PrintStream OUT = new PrintStream(new FileOutputStream(FileDescriptor.out), false, StandardCharsets.UTF_8);
|
|
58
|
+
private static final Object LOCK = new Object();
|
|
59
|
+
private static final Set<String> SEEN = ConcurrentHashMap.newKeySet();
|
|
60
|
+
private static volatile boolean stopped = false;
|
|
61
|
+
private static volatile HikariDataSource pool;
|
|
62
|
+
private static volatile String connected;
|
|
63
|
+
private static String host;
|
|
64
|
+
private static String port;
|
|
65
|
+
private static String query;
|
|
66
|
+
private static HikariConfig config;
|
|
67
|
+
|
|
68
|
+
private ClientHikari() {}
|
|
69
|
+
|
|
70
|
+
private static long now() {
|
|
71
|
+
return System.nanoTime();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** This function writes one line: its event, its stamp, and the JSON fields after them. */
|
|
75
|
+
private static void emit(String ev, long t, String fields) {
|
|
76
|
+
String line = "{\"ev\":" + q(ev) + ",\"t\":" + t + (fields.isEmpty() ? "" : "," + fields) + "}\n";
|
|
77
|
+
synchronized (LOCK) {
|
|
78
|
+
OUT.print(line);
|
|
79
|
+
OUT.flush();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static void emit(String ev, String fields) {
|
|
84
|
+
emit(ev, now(), fields);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** This function gives a text as a JSON string, or null. */
|
|
88
|
+
private static String q(String text) {
|
|
89
|
+
if (text == null) return "null";
|
|
90
|
+
StringBuilder out = new StringBuilder("\"");
|
|
91
|
+
for (char c : text.toCharArray()) {
|
|
92
|
+
switch (c) {
|
|
93
|
+
case '"' -> out.append("\\\"");
|
|
94
|
+
case '\\' -> out.append("\\\\");
|
|
95
|
+
case '\n' -> out.append("\\n");
|
|
96
|
+
case '\r' -> out.append("\\r");
|
|
97
|
+
case '\t' -> out.append("\\t");
|
|
98
|
+
default -> {
|
|
99
|
+
if (c < 0x20) out.append(String.format("\\u%04x", (int) c));
|
|
100
|
+
else out.append(c);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out.append('"').toString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private static String flag(List<String> argv, String name, String otherwise) {
|
|
108
|
+
int at = argv.indexOf(name);
|
|
109
|
+
return at == -1 || at + 1 >= argv.size() ? otherwise : argv.get(at + 1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- the options: a flat JSON object of names to numbers, texts, true, false or null
|
|
113
|
+
|
|
114
|
+
private static Map<String, String> parseObject(String text) {
|
|
115
|
+
Map<String, String> out = new LinkedHashMap<>();
|
|
116
|
+
int[] at = {0};
|
|
117
|
+
skip(text, at);
|
|
118
|
+
expect(text, at, '{');
|
|
119
|
+
skip(text, at);
|
|
120
|
+
if (peek(text, at) == '}') return out;
|
|
121
|
+
for (;;) {
|
|
122
|
+
skip(text, at);
|
|
123
|
+
String name = parseString(text, at);
|
|
124
|
+
skip(text, at);
|
|
125
|
+
expect(text, at, ':');
|
|
126
|
+
skip(text, at);
|
|
127
|
+
String value = parseValue(text, at);
|
|
128
|
+
if (value != null) out.put(name, value);
|
|
129
|
+
skip(text, at);
|
|
130
|
+
char next = text.charAt(at[0]++);
|
|
131
|
+
if (next == '}') return out;
|
|
132
|
+
if (next != ',') throw new IllegalArgumentException("the options are not a flat JSON object: " + text);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private static char peek(String text, int[] at) {
|
|
137
|
+
return text.charAt(at[0]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private static void skip(String text, int[] at) {
|
|
141
|
+
while (at[0] < text.length() && Character.isWhitespace(text.charAt(at[0]))) at[0]++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private static void expect(String text, int[] at, char c) {
|
|
145
|
+
if (text.charAt(at[0]++) != c) throw new IllegalArgumentException("the options are not a flat JSON object: " + text);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private static String parseString(String text, int[] at) {
|
|
149
|
+
expect(text, at, '"');
|
|
150
|
+
StringBuilder out = new StringBuilder();
|
|
151
|
+
for (;;) {
|
|
152
|
+
char c = text.charAt(at[0]++);
|
|
153
|
+
if (c == '"') return out.toString();
|
|
154
|
+
if (c == '\\') {
|
|
155
|
+
char e = text.charAt(at[0]++);
|
|
156
|
+
switch (e) {
|
|
157
|
+
case 'n' -> out.append('\n');
|
|
158
|
+
case 't' -> out.append('\t');
|
|
159
|
+
case 'r' -> out.append('\r');
|
|
160
|
+
case 'u' -> {
|
|
161
|
+
out.append((char) Integer.parseInt(text.substring(at[0], at[0] + 4), 16));
|
|
162
|
+
at[0] += 4;
|
|
163
|
+
}
|
|
164
|
+
default -> out.append(e);
|
|
165
|
+
}
|
|
166
|
+
} else out.append(c);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private static String parseValue(String text, int[] at) {
|
|
171
|
+
char c = peek(text, at);
|
|
172
|
+
if (c == '"') return parseString(text, at);
|
|
173
|
+
int from = at[0];
|
|
174
|
+
while (at[0] < text.length() && ",} \t\r\n".indexOf(text.charAt(at[0])) == -1) at[0]++;
|
|
175
|
+
String word = text.substring(from, at[0]);
|
|
176
|
+
if (word.equals("null")) return null;
|
|
177
|
+
// a whole number in a JSON number of the cell (30000.0) is a whole number for a setter of HikariConfig
|
|
178
|
+
if (word.endsWith(".0")) return word.substring(0, word.length() - 2);
|
|
179
|
+
return word;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---- the error of a query as data
|
|
183
|
+
|
|
184
|
+
private static Throwable rootOf(Throwable error) {
|
|
185
|
+
Throwable at = error;
|
|
186
|
+
while (at.getCause() != null && at.getCause() != at) at = at.getCause();
|
|
187
|
+
return at;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** This function gives the first SQLException in the causes of an error: the start of a pool wraps the one of pgjdbc. */
|
|
191
|
+
private static SQLException sqlOf(Throwable error) {
|
|
192
|
+
for (Throwable at = error; at != null; at = at.getCause() == at ? null : at.getCause()) {
|
|
193
|
+
if (at instanceof SQLException sql) return sql;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private static String failure(Throwable error) {
|
|
199
|
+
SQLException sql = sqlOf(error);
|
|
200
|
+
String state = sql == null ? null : sql.getSQLState();
|
|
201
|
+
Integer code = sql == null ? null : sql.getErrorCode();
|
|
202
|
+
Throwable root = rootOf(error);
|
|
203
|
+
return "\"name\":" + q(error.getClass().getSimpleName())
|
|
204
|
+
+ ",\"sqlState\":" + q(state)
|
|
205
|
+
+ ",\"code\":" + (code == null ? "null" : code)
|
|
206
|
+
+ ",\"message\":" + q(error.getMessage())
|
|
207
|
+
+ ",\"cause\":" + (root == error ? "null" : "{\"name\":" + q(root.getClass().getSimpleName()) + ",\"message\":" + q(root.getMessage()) + "}");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** This function gives the errno of an error: the SQLState of pgjdbc or HikariCP, or the message. */
|
|
211
|
+
private static String errnoOf(Throwable error) {
|
|
212
|
+
SQLException sql = sqlOf(error);
|
|
213
|
+
if (sql != null && sql.getSQLState() != null) return sql.getSQLState();
|
|
214
|
+
return error.getMessage();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---- each connection that the client borrows: its socket, read from pgjdbc
|
|
218
|
+
|
|
219
|
+
/** This function gives the local address of the socket of a connection, or null. */
|
|
220
|
+
private static String localOf(Connection connection) {
|
|
221
|
+
try {
|
|
222
|
+
PgConnection pg = connection.unwrap(PgConnection.class);
|
|
223
|
+
Object executor = pg.getQueryExecutor();
|
|
224
|
+
Field field = null;
|
|
225
|
+
for (Class<?> at = executor.getClass(); at != null && field == null; at = at.getSuperclass()) {
|
|
226
|
+
try {
|
|
227
|
+
field = at.getDeclaredField("pgStream");
|
|
228
|
+
} catch (NoSuchFieldException absent) {
|
|
229
|
+
// the field is on a superclass
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (field == null) return null;
|
|
233
|
+
field.setAccessible(true);
|
|
234
|
+
Socket socket = ((PGStream) field.get(executor)).getSocket();
|
|
235
|
+
return socket.getLocalAddress().getHostAddress() + ":" + socket.getLocalPort();
|
|
236
|
+
} catch (Exception error) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private static void seen(Connection connection) {
|
|
242
|
+
String local = localOf(connection);
|
|
243
|
+
if (local == null || !SEEN.add(local)) return;
|
|
244
|
+
emit("socket", "\"role\":\"pool\",\"local\":" + q(local));
|
|
245
|
+
if (connected == null) {
|
|
246
|
+
connected = local;
|
|
247
|
+
emit("connected", "\"local\":" + q(local));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---- the counts of the pool, each 5 ms: a line for each change
|
|
252
|
+
|
|
253
|
+
private static void watchPool() {
|
|
254
|
+
Thread watcher = new Thread(() -> {
|
|
255
|
+
String last = "";
|
|
256
|
+
while (!stopped) {
|
|
257
|
+
HikariDataSource ds = pool;
|
|
258
|
+
if (ds != null) {
|
|
259
|
+
try {
|
|
260
|
+
HikariPoolMXBean bean = ds.getHikariPoolMXBean();
|
|
261
|
+
if (bean != null) {
|
|
262
|
+
String counts = "\"total\":" + bean.getTotalConnections()
|
|
263
|
+
+ ",\"active\":" + bean.getActiveConnections()
|
|
264
|
+
+ ",\"idle\":" + bean.getIdleConnections()
|
|
265
|
+
+ ",\"waiting\":" + bean.getThreadsAwaitingConnection();
|
|
266
|
+
if (!counts.equals(last)) {
|
|
267
|
+
emit("pool", counts);
|
|
268
|
+
last = counts;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch (RuntimeException closed) {
|
|
272
|
+
// the pool closed
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
Thread.sleep(5);
|
|
277
|
+
} catch (InterruptedException stop) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}, "pool-watcher");
|
|
282
|
+
watcher.setDaemon(true);
|
|
283
|
+
watcher.start();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---- the end of the client
|
|
287
|
+
|
|
288
|
+
private static void stop(int code) {
|
|
289
|
+
synchronized (LOCK) {
|
|
290
|
+
if (stopped) return;
|
|
291
|
+
stopped = true;
|
|
292
|
+
}
|
|
293
|
+
emit("done", "");
|
|
294
|
+
HikariDataSource ds = pool;
|
|
295
|
+
if (ds != null) {
|
|
296
|
+
Thread closer = new Thread(ds::close, "pool-closer");
|
|
297
|
+
closer.setDaemon(true);
|
|
298
|
+
closer.start();
|
|
299
|
+
try {
|
|
300
|
+
closer.join(1000);
|
|
301
|
+
} catch (InterruptedException stopNow) {
|
|
302
|
+
// the exit goes on
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
Runtime.getRuntime().halt(code);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** This function sends query n through the pool. Its stamp is before the borrow. It gives null for a reply, or the error. */
|
|
309
|
+
private static Throwable operate(int n) {
|
|
310
|
+
long at = now();
|
|
311
|
+
if (n == 1) emit("connecting", at, "\"at\":" + q(host + ":" + port));
|
|
312
|
+
emit("sent", at, "\"n\":" + n);
|
|
313
|
+
try {
|
|
314
|
+
// the first query starts the pool: its check of initializationFailTimeout makes the first connection
|
|
315
|
+
if (pool == null) pool = new HikariDataSource(config);
|
|
316
|
+
long rows = 0;
|
|
317
|
+
try (Connection connection = pool.getConnection()) {
|
|
318
|
+
seen(connection);
|
|
319
|
+
try (Statement statement = connection.createStatement()) {
|
|
320
|
+
if (statement.execute(query)) {
|
|
321
|
+
try (ResultSet result = statement.getResultSet()) {
|
|
322
|
+
while (result.next()) rows += 1;
|
|
323
|
+
}
|
|
324
|
+
} else rows = statement.getUpdateCount();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
emit("ack", "\"n\":" + n + ",\"rows\":" + rows);
|
|
328
|
+
return null;
|
|
329
|
+
} catch (Throwable error) {
|
|
330
|
+
emit("fail", "\"n\":" + n + "," + failure(error));
|
|
331
|
+
return error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
public static void main(String[] args) throws Exception {
|
|
336
|
+
List<String> argv = List.of(args);
|
|
337
|
+
String mode = flag(argv, "--mode", null);
|
|
338
|
+
if (args.length < 2 || mode == null || !List.of("idle", "inflight", "stream").contains(mode)) {
|
|
339
|
+
emit("fail", "\"why\":\"client-hikari HOST PORT --mode idle|inflight|stream\"");
|
|
340
|
+
Runtime.getRuntime().halt(1);
|
|
341
|
+
}
|
|
342
|
+
host = args[0];
|
|
343
|
+
port = args[1];
|
|
344
|
+
long interval = Long.parseLong(flag(argv, "--interval-ms", "50"));
|
|
345
|
+
long max = Long.parseLong(flag(argv, "--max-s", "120")) * 1000;
|
|
346
|
+
Path go = Path.of(flag(argv, "--go", "/tmp/go"));
|
|
347
|
+
query = flag(argv, "--query", "SELECT 1");
|
|
348
|
+
boolean before = argv.contains("--before");
|
|
349
|
+
Map<String, String> options = parseObject(flag(argv, "--options", "{}"));
|
|
350
|
+
|
|
351
|
+
String bootId;
|
|
352
|
+
try {
|
|
353
|
+
bootId = Files.readString(Path.of("/proc/sys/kernel/random/boot_id")).trim();
|
|
354
|
+
} catch (Exception absent) {
|
|
355
|
+
bootId = null;
|
|
356
|
+
}
|
|
357
|
+
StringBuilder given = new StringBuilder("{");
|
|
358
|
+
for (Map.Entry<String, String> each : options.entrySet()) {
|
|
359
|
+
if (given.length() > 1) given.append(',');
|
|
360
|
+
given.append(q(each.getKey())).append(':').append(q(each.getValue()));
|
|
361
|
+
}
|
|
362
|
+
given.append('}');
|
|
363
|
+
String hikari = HikariDataSource.class.getPackage().getImplementationVersion();
|
|
364
|
+
emit("start", "\"boot_id\":" + q(bootId)
|
|
365
|
+
+ ",\"client\":\"hikari\",\"driver\":" + q(org.postgresql.util.DriverInfo.DRIVER_VERSION)
|
|
366
|
+
+ ",\"pool\":" + q(hikari == null ? "7.1.0" : hikari)
|
|
367
|
+
+ ",\"java\":" + q(System.getProperty("java.version"))
|
|
368
|
+
+ ",\"mode\":" + q(mode) + ",\"query\":" + q(query) + ",\"options\":" + given);
|
|
369
|
+
|
|
370
|
+
Properties properties = new Properties();
|
|
371
|
+
for (Map.Entry<String, String> each : options.entrySet()) properties.setProperty(each.getKey(), each.getValue());
|
|
372
|
+
config = new HikariConfig(properties);
|
|
373
|
+
config.setJdbcUrl("jdbc:postgresql://" + host + ":" + port + "/postgres");
|
|
374
|
+
config.setUsername("postgres");
|
|
375
|
+
config.setPoolName("cell");
|
|
376
|
+
|
|
377
|
+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
|
378
|
+
// SIGTERM: the end of the client, with exit code 0, as the pg client gives
|
|
379
|
+
synchronized (LOCK) {
|
|
380
|
+
if (stopped) return;
|
|
381
|
+
stopped = true;
|
|
382
|
+
}
|
|
383
|
+
emit("done", "");
|
|
384
|
+
Runtime.getRuntime().halt(0);
|
|
385
|
+
}, "sigterm"));
|
|
386
|
+
Thread deadline = new Thread(() -> {
|
|
387
|
+
try {
|
|
388
|
+
Thread.sleep(max);
|
|
389
|
+
} catch (InterruptedException stop) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
emit("deadline", "");
|
|
393
|
+
stop(0);
|
|
394
|
+
}, "deadline");
|
|
395
|
+
deadline.setDaemon(true);
|
|
396
|
+
deadline.start();
|
|
397
|
+
watchPool();
|
|
398
|
+
|
|
399
|
+
if (mode.equals("stream")) {
|
|
400
|
+
emit("ready", "\"at\":" + q(host + ":" + port));
|
|
401
|
+
for (int n = 1; !stopped; n += 1) {
|
|
402
|
+
operate(n);
|
|
403
|
+
Thread.sleep(interval);
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// the first query makes the connection of the pool. Its reply is the last data before the fault
|
|
408
|
+
Throwable failed = operate(1);
|
|
409
|
+
if (failed != null) {
|
|
410
|
+
emit("error", "\"errno\":" + q(errnoOf(failed)) + ",\"message\":" + q(failed.getMessage()));
|
|
411
|
+
stop(before ? 0 : 1);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// the connection went back to the pool at the end of the query: it is idle there
|
|
415
|
+
emit("ready", "\"reply\":\"ack\"");
|
|
416
|
+
if (mode.equals("inflight")) {
|
|
417
|
+
while (!Files.exists(go)) Thread.sleep(2);
|
|
418
|
+
Throwable error = operate(2);
|
|
419
|
+
if (error == null) emit("data", "\"n\":2");
|
|
420
|
+
else emit("error", "\"errno\":" + q(errnoOf(error)) + ",\"message\":" + q(error.getMessage()));
|
|
421
|
+
}
|
|
422
|
+
Thread.sleep(Long.MAX_VALUE);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# The harness's client of HikariCP 7.1.0 over pgjdbc 42.7.13, on Temurin 21.0.12.1. It runs as a
|
|
2
|
+
# container in the client's network namespace, as the harness's own TCP client does. It prints the
|
|
3
|
+
# same JSON lines, with the state of the pool beside them.
|
|
4
|
+
# lab.ts stages the build context: this folder, under harness/image/hikari. The build gets each jar
|
|
5
|
+
# from Maven Central and checks its SHA-256 (jars.sha256), thus a build cannot get another jar.
|
|
6
|
+
FROM eclipse-temurin:21.0.12.1_1-jdk-alpine@sha256:0bfc69a4758a86710e5c474032d28400a8bd00874766f9e8b1642ac2fd293159 AS build
|
|
7
|
+
WORKDIR /build
|
|
8
|
+
COPY harness/image/hikari/jars.sha256 ./
|
|
9
|
+
RUN wget -q https://repo1.maven.org/maven2/com/zaxxer/HikariCP/7.1.0/HikariCP-7.1.0.jar \
|
|
10
|
+
&& wget -q https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.13/postgresql-42.7.13.jar \
|
|
11
|
+
&& wget -q https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar \
|
|
12
|
+
&& sha256sum -c jars.sha256
|
|
13
|
+
COPY harness/image/hikari/ClientHikari.java ./
|
|
14
|
+
RUN javac --release 21 -cp "HikariCP-7.1.0.jar:postgresql-42.7.13.jar:slf4j-api-2.0.17.jar" -d classes ClientHikari.java
|
|
15
|
+
|
|
16
|
+
FROM eclipse-temurin:21.0.12.1_1-jre-alpine@sha256:51ab5e3302e7141ce665ca3ea85e8b5cd648eafbc3c0c90dd79d6537684e4555
|
|
17
|
+
WORKDIR /opt/harness/hikari
|
|
18
|
+
COPY --from=build /build/HikariCP-7.1.0.jar /build/postgresql-42.7.13.jar /build/slf4j-api-2.0.17.jar ./lib/
|
|
19
|
+
COPY --from=build /build/classes ./classes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/harness",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.4",
|
|
5
5
|
"description": "A fault-injection harness for connection timers: a cell runs a client against a stock target in Docker, through a topology of routers and proxies, a replicated group under Sentinels or a replica set, injects a fault, measures the kernel's own records and judges a prediction. HTTP clients of Go, Node and undici race the keep-alive of a Node server, and the MongoDB driver meets the faults of a replica set. The WebSocket client of @procedures/client drops a silent connection on its own heartbeat, and the harness times that detection and the reconnect from the program's lines and the kernel's stamps.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"fault-injection",
|
|
@@ -69,6 +69,9 @@
|
|
|
69
69
|
"image/pg/client-pg.mjs",
|
|
70
70
|
"image/pg/package.json",
|
|
71
71
|
"image/pg/npm-lock.json",
|
|
72
|
+
"image/hikari/Dockerfile",
|
|
73
|
+
"image/hikari/ClientHikari.java",
|
|
74
|
+
"image/hikari/jars.sha256",
|
|
72
75
|
"image/procedures/Dockerfile",
|
|
73
76
|
"image/procedures/package.json",
|
|
74
77
|
"image/procedures/npm-lock.json",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"image-Q_fs3rCC.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 // @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};\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 \"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 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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"}
|