@mark1russell7/harness 0.11.2 → 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.
@@ -0,0 +1,19 @@
1
+ import { J as Subject, _ as ImageSource, i as ClientImage, r as Client } from "./cell-Di_YHiuB.js";
2
+ import { z } from "zod";
3
+ //#region src/schema.d.ts
4
+ /**
5
+ * This schema accepts the source of an image (`ImageSource`). It is a pinned image with its digest,
6
+ * a build context whose paths stay in it, or an image that the harness ships.
7
+ */
8
+ export declare const imageSourceSchema: z.ZodType<ImageSource>;
9
+ /** This schema accepts a client image (`ClientImage`): a stock image by its name or the source of an image, and its command. */
10
+ export declare const clientImageSchema: z.ZodType<ClientImage>;
11
+ /** This schema accepts the client of a cell (`Cell.client`): a client that the harness ships by its name, or a client image. */
12
+ export declare const clientSchema: z.ZodType<Client | ClientImage>;
13
+ /**
14
+ * This schema accepts a subject (`Subject`): its name, the source of its image and its stock base.
15
+ * It also accepts the command of each of its programs.
16
+ */
17
+ export declare const subjectSchema: z.ZodType<Subject>;
18
+ //#endregion
19
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1,56 @@
1
+ import { S as subjectProblems, a as SHIPPED_NAMES, d as imageProblems, t as CLIENT_NAMES } from "./image-BfuXg-5t.js";
2
+ import { z } from "zod";
3
+ //#region src/schema.ts
4
+ /** This function adds each problem that a check finds as an issue of a schema. */
5
+ const issues = (check) => (value, ctx) => {
6
+ for (const message of check(value)) ctx.addIssue({
7
+ code: "custom",
8
+ message
9
+ });
10
+ };
11
+ /** This schema accepts a command: argv of one text or more. */
12
+ const command = z.array(z.string().min(1)).min(1);
13
+ /** This schema accepts the shape of each kind of image source. The checks of its values come after it. */
14
+ const sourceShape = z.discriminatedUnion("kind", [
15
+ z.strictObject({
16
+ kind: z.literal("pinned"),
17
+ image: z.string()
18
+ }),
19
+ z.strictObject({
20
+ kind: z.literal("build"),
21
+ name: z.string(),
22
+ context: z.string(),
23
+ dockerfile: z.string().optional(),
24
+ files: z.array(z.string()).optional()
25
+ }),
26
+ z.strictObject({
27
+ kind: z.literal("shipped"),
28
+ name: z.enum(SHIPPED_NAMES)
29
+ })
30
+ ]);
31
+ /**
32
+ * This schema accepts the source of an image (`ImageSource`). It is a pinned image with its digest,
33
+ * a build context whose paths stay in it, or an image that the harness ships.
34
+ */
35
+ const imageSourceSchema = sourceShape.superRefine(issues(imageProblems));
36
+ /** This schema accepts a client image (`ClientImage`): a stock image by its name or the source of an image, and its command. */
37
+ const clientImageSchema = z.strictObject({
38
+ image: z.union([z.string().min(1), imageSourceSchema]),
39
+ command
40
+ });
41
+ /** This schema accepts the client of a cell (`Cell.client`): a client that the harness ships by its name, or a client image. */
42
+ const clientSchema = z.union([z.enum(CLIENT_NAMES), clientImageSchema]);
43
+ /**
44
+ * This schema accepts a subject (`Subject`): its name, the source of its image and its stock base.
45
+ * It also accepts the command of each of its programs.
46
+ */
47
+ const subjectSchema = z.strictObject({
48
+ name: z.string().min(1),
49
+ image: sourceShape,
50
+ base: z.string(),
51
+ programs: z.record(z.string(), command)
52
+ }).superRefine(issues(subjectProblems));
53
+ //#endregion
54
+ export { clientImageSchema, clientSchema, imageSourceSchema, subjectSchema };
55
+
56
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","names":[],"sources":["../src/schema.ts"],"sourcesContent":["// The entry \"@mark1russell7/harness/schema\": the zod schemas of the data that a caller gives the\n// harness as JSON. These are the source of an image, a client and a subject. The root entry of\n// the package does not import zod.\nimport { z } from \"zod\";\nimport type { Client, ClientImage, ImageSource, Subject } from \"./cell.ts\";\nimport { CLIENT_NAMES, SHIPPED_NAMES, imageProblems, subjectProblems } from \"./image.ts\";\n\n/** This function adds each problem that a check finds as an issue of a schema. */\nconst issues =\n <T>(check: (value: T) => string[]) =>\n (value: T, ctx: z.RefinementCtx): void => {\n for (const message of check(value)) ctx.addIssue({ code: \"custom\", message });\n };\n\n/** This schema accepts a command: argv of one text or more. */\nconst command = z.array(z.string().min(1)).min(1);\n\n/** This schema accepts the shape of each kind of image source. The checks of its values come after it. */\nconst sourceShape = z.discriminatedUnion(\"kind\", [\n z.strictObject({ kind: z.literal(\"pinned\"), image: z.string() }),\n z.strictObject({\n kind: z.literal(\"build\"),\n name: z.string(),\n context: z.string(),\n dockerfile: z.string().optional(),\n files: z.array(z.string()).optional(),\n }),\n z.strictObject({ kind: z.literal(\"shipped\"), name: z.enum(SHIPPED_NAMES) }),\n]);\n\n/**\n * This schema accepts the source of an image (`ImageSource`). It is a pinned image with its digest,\n * a build context whose paths stay in it, or an image that the harness ships.\n */\nexport const imageSourceSchema: z.ZodType<ImageSource> = sourceShape.superRefine(issues(imageProblems));\n\n/** This schema accepts a client image (`ClientImage`): a stock image by its name or the source of an image, and its command. */\nexport const clientImageSchema: z.ZodType<ClientImage> = z.strictObject({\n image: z.union([z.string().min(1), imageSourceSchema]),\n command,\n});\n\n/** This schema accepts the client of a cell (`Cell.client`): a client that the harness ships by its name, or a client image. */\nexport const clientSchema: z.ZodType<Client | ClientImage> = z.union([z.enum(CLIENT_NAMES), clientImageSchema]);\n\n/**\n * This schema accepts a subject (`Subject`): its name, the source of its image and its stock base.\n * It also accepts the command of each of its programs.\n */\nexport const subjectSchema: z.ZodType<Subject> = z\n .strictObject({\n name: z.string().min(1),\n image: sourceShape,\n base: z.string(),\n programs: z.record(z.string(), command),\n })\n .superRefine(issues(subjectProblems));\n"],"mappings":";;;;AAQA,MAAM,UACA,WACH,OAAU,QAA+B;CACxC,KAAK,MAAM,WAAW,MAAM,KAAK,GAAG,IAAI,SAAS;EAAE,MAAM;EAAU;CAAQ,CAAC;AAC9E;;AAGF,MAAM,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;;AAGhD,MAAM,cAAc,EAAE,mBAAmB,QAAQ;CAC/C,EAAE,aAAa;EAAE,MAAM,EAAE,QAAQ,QAAQ;EAAG,OAAO,EAAE,OAAO;CAAE,CAAC;CAC/D,EAAE,aAAa;EACb,MAAM,EAAE,QAAQ,OAAO;EACvB,MAAM,EAAE,OAAO;EACf,SAAS,EAAE,OAAO;EAClB,YAAY,EAAE,OAAO,CAAC,CAAC,SAAS;EAChC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS;CACtC,CAAC;CACD,EAAE,aAAa;EAAE,MAAM,EAAE,QAAQ,SAAS;EAAG,MAAM,EAAE,KAAK,aAAa;CAAE,CAAC;AAC5E,CAAC;;;;;AAMD,MAAa,oBAA4C,YAAY,YAAY,OAAO,aAAa,CAAC;;AAGtG,MAAa,oBAA4C,EAAE,aAAa;CACtE,OAAO,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC;CACrD;AACF,CAAC;;AAGD,MAAa,eAAgD,EAAE,MAAM,CAAC,EAAE,KAAK,YAAY,GAAG,iBAAiB,CAAC;;;;;AAM9G,MAAa,gBAAoC,EAC9C,aAAa;CACZ,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACtB,OAAO;CACP,MAAM,EAAE,OAAO;CACf,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO;AACxC,CAAC,CAAC,CACD,YAAY,OAAO,eAAe,CAAC"}
@@ -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
@@ -0,0 +1,3 @@
1
+ 2be45c641b841e81df87292e181f2c203500c5f0e9a1676f135927387f3cf40b HikariCP-7.1.0.jar
2
+ 6e0e4cc2d8cae902084f8a2b18728b073a6fd9d1f87c9d8bff8f298c18185b93 postgresql-42.7.13.jar
3
+ 7b751d952061954d5abfed7181c1f645d336091b679891591d63329c622eb832 slf4j-api-2.0.17.jar
@@ -8,6 +8,8 @@
8
8
  //
9
9
  // The lines:
10
10
  // - start: the versions of pg, pg-pool and Node, the mode and the options of the pool.
11
+ // - connecting: the first query, which makes the first connection of the pool. Its stamp is the
12
+ // stamp of the first sent line. The pool arms its connect timer in that call.
11
13
  // - socket, socket-end, socket-error: each socket of the pool, with its local address.
12
14
  // - connected: the first socket. The TCP client of the harness names its own socket so.
13
15
  // - pool: each event of the pool: connect, acquire, release, remove, and error with its errno.
@@ -118,6 +120,8 @@ setTimeout(() => {
118
120
  const operate = async (n) => {
119
121
  const at = now();
120
122
  const reply = pool.query(query);
123
+ // a band of a cell that starts into a fault counts from the start of the first connect
124
+ if (n === 1) emit("connecting", { at: `${host}:${port}` }, at);
121
125
  emit("sent", { n }, at);
122
126
  try {
123
127
  const result = await reply;
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.2",
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",
@@ -33,6 +33,10 @@
33
33
  "types": "./build/index.d.ts",
34
34
  "default": "./build/index.js"
35
35
  },
36
+ "./schema": {
37
+ "types": "./build/schema.d.ts",
38
+ "default": "./build/schema.js"
39
+ },
36
40
  "./package.json": "./package.json"
37
41
  },
38
42
  "files": [
@@ -65,6 +69,9 @@
65
69
  "image/pg/client-pg.mjs",
66
70
  "image/pg/package.json",
67
71
  "image/pg/npm-lock.json",
72
+ "image/hikari/Dockerfile",
73
+ "image/hikari/ClientHikari.java",
74
+ "image/hikari/jars.sha256",
68
75
  "image/procedures/Dockerfile",
69
76
  "image/procedures/package.json",
70
77
  "image/procedures/npm-lock.json",