@mark1russell7/harness 0.11.5 → 0.11.7

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/build/schema.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { J as Subject, _ as ImageSource, i as ClientImage, r as Client } from "./cell-lSCgFB54.js";
1
+ import { J as Subject, _ as ImageSource, i as ClientImage, r as Client } from "./cell-7HFQIHlK.js";
2
2
  import { z } from "zod";
3
3
  //#region src/schema.d.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
1
  // client-hikari HOST PORT --mode idle|inflight|stream [--interval-ms N] [--max-s N] [--go PATH]
2
- // [--query TEXT] [--before] [--options JSON]
2
+ // [--query TEXT] [--inflight-query TEXT] [--before] [--options JSON]
3
3
  //
4
4
  // The client of a HikariCP cell: HikariCP 7.1.0 over pgjdbc 42.7.13, on Java 21. Each event is a
5
5
  // JSON line with `ev` and `t`, the time on CLOCK_MONOTONIC in nanoseconds (System.nanoTime, which
@@ -24,7 +24,8 @@
24
24
  // connection goes back to the pool), then holds until --max-s. inflight sends one query, waits for
25
25
  // the file --go, sends one more query into the fault, and holds after its end. stream sends a query
26
26
  // each --interval-ms after the reply before. --before starts the client into a fault that holds
27
- // already: the start of its pool meets it.
27
+ // already: the start of its pool meets it. --inflight-query is the text of the query in flight
28
+ // (query 2 of inflight), and --query is the text of each other query.
28
29
  //
29
30
  // The options are one flat JSON object. A name with the prefix `dataSource.` is a property of
30
31
  // pgjdbc. Each other name is a property of HikariConfig. A cell gives each option that it changes,
@@ -53,6 +54,8 @@ import java.util.Set;
53
54
  import java.util.concurrent.ConcurrentHashMap;
54
55
  import org.postgresql.core.PGStream;
55
56
  import org.postgresql.jdbc.PgConnection;
57
+ import org.postgresql.util.PSQLException;
58
+ import org.postgresql.util.ServerErrorMessage;
56
59
 
57
60
  public final class ClientHikari {
58
61
  private static final PrintStream OUT = new PrintStream(new FileOutputStream(FileDescriptor.out), false, StandardCharsets.UTF_8);
@@ -65,6 +68,7 @@ public final class ClientHikari {
65
68
  private static String host;
66
69
  private static String port;
67
70
  private static String query;
71
+ private static String inflightQuery;
68
72
  private static HikariConfig config;
69
73
 
70
74
  private ClientHikari() {}
@@ -334,8 +338,13 @@ public final class ClientHikari {
334
338
  Runtime.getRuntime().halt(code);
335
339
  }
336
340
 
337
- /** This function sends query n through the pool. Its stamp is before the borrow. It gives null for a reply, or the error. */
341
+ /** This function sends query n through the pool, with the text of each query. Its stamp is before the borrow. It gives null for a reply, or the error. */
338
342
  private static Throwable operate(int n) {
343
+ return operate(n, query);
344
+ }
345
+
346
+ /** This function sends query n through the pool, with its text. Its stamp is before the borrow. It gives null for a reply, or the error. */
347
+ private static Throwable operate(int n, String text) {
339
348
  long at = now();
340
349
  if (n == 1) emit("connecting", at, "\"at\":" + q(host + ":" + port));
341
350
  emit("sent", at, "\"n\":" + n);
@@ -346,7 +355,7 @@ public final class ClientHikari {
346
355
  try (Connection connection = pool.getConnection()) {
347
356
  seen(connection);
348
357
  try (Statement statement = connection.createStatement()) {
349
- if (statement.execute(query)) {
358
+ if (statement.execute(text)) {
350
359
  try (ResultSet result = statement.getResultSet()) {
351
360
  while (result.next()) rows += 1;
352
361
  }
@@ -361,6 +370,41 @@ public final class ClientHikari {
361
370
  }
362
371
  }
363
372
 
373
+ /**
374
+ * This function loads the code of a connect and of the start of a pool before the first line, on
375
+ * the loopback of the namespace. A new JVM loads each class at its first use, and that load would
376
+ * be in the band of a cell that starts into a fault. A pool with no check and no idle connection
377
+ * makes no connection: it loads the classes of the pool. It also sets the login timeout of
378
+ * DriverManager, thus pgjdbc then connects in a task of an executor (Driver.java:303-311,
379
+ * :730-739). A connect to port 1 of the loopback after it is refused at once: it loads that
380
+ * connect of pgjdbc and its errors. HikariCP gives each new connection a random UUID
381
+ * (PoolBase.java:363), and the first one starts the SecureRandom of the JVM. An error of the
382
+ * server that pgjdbc parses loads the classes of an ERROR of a statement, for example of its
383
+ * timeout.
384
+ */
385
+ private static void warmUp() {
386
+ HikariConfig empty = new HikariConfig();
387
+ empty.setJdbcUrl("jdbc:postgresql://127.0.0.1:1/warmup");
388
+ empty.setUsername("warmup");
389
+ empty.setPoolName("warmup");
390
+ empty.setInitializationFailTimeout(-1);
391
+ empty.setMinimumIdle(0);
392
+ // the pool starts and closes with no connection
393
+ new HikariDataSource(empty).close();
394
+ Properties refused = new Properties();
395
+ refused.setProperty("user", "warmup");
396
+ try {
397
+ new org.postgresql.Driver().connect("jdbc:postgresql://127.0.0.1:1/warmup", refused);
398
+ } catch (SQLException expected) {
399
+ // the refusal is the purpose of the connect
400
+ }
401
+ java.util.UUID.randomUUID();
402
+ // the fields of an ErrorResponse, each a code and a text with a NUL after it
403
+ PSQLException cancelled = new PSQLException(new ServerErrorMessage("SERROR\0VERROR\0C57014\0Mwarm-up\0"));
404
+ errnoOf(cancelled);
405
+ failure(cancelled);
406
+ }
407
+
364
408
  public static void main(String[] args) throws Exception {
365
409
  List<String> argv = List.of(args);
366
410
  String mode = flag(argv, "--mode", null);
@@ -374,8 +418,10 @@ public final class ClientHikari {
374
418
  long max = Long.parseLong(flag(argv, "--max-s", "120")) * 1000;
375
419
  Path go = Path.of(flag(argv, "--go", "/tmp/go"));
376
420
  query = flag(argv, "--query", "SELECT 1");
421
+ inflightQuery = flag(argv, "--inflight-query", query);
377
422
  boolean before = argv.contains("--before");
378
423
  Map<String, String> options = parseObject(flag(argv, "--options", "{}"));
424
+ warmUp();
379
425
 
380
426
  String bootId;
381
427
  try {
@@ -394,7 +440,7 @@ public final class ClientHikari {
394
440
  + ",\"client\":\"hikari\",\"driver\":" + q(org.postgresql.util.DriverInfo.DRIVER_VERSION)
395
441
  + ",\"pool\":" + q(hikari == null ? "7.1.0" : hikari)
396
442
  + ",\"java\":" + q(System.getProperty("java.version"))
397
- + ",\"mode\":" + q(mode) + ",\"query\":" + q(query) + ",\"options\":" + given);
443
+ + ",\"mode\":" + q(mode) + ",\"query\":" + q(query) + ",\"inflight_query\":" + q(inflightQuery) + ",\"options\":" + given);
398
444
 
399
445
  Properties properties = new Properties();
400
446
  for (Map.Entry<String, String> each : options.entrySet()) properties.setProperty(each.getKey(), each.getValue());
@@ -445,7 +491,7 @@ public final class ClientHikari {
445
491
  if (mode.equals("idle")) watchIdle();
446
492
  if (mode.equals("inflight")) {
447
493
  while (!Files.exists(go)) Thread.sleep(2);
448
- Throwable error = operate(2);
494
+ Throwable error = operate(2, inflightQuery);
449
495
  if (error == null) emit("data", "\"n\":2");
450
496
  else emit("error", "\"errno\":" + q(errnoOf(error)) + ",\"message\":" + q(error.getMessage()));
451
497
  }
@@ -1,5 +1,5 @@
1
1
  // client-pg HOST PORT --mode idle|inflight|stream [--interval-ms N] [--max-s N] [--go PATH]
2
- // [--query TEXT] [--before] [--options JSON]
2
+ // [--query TEXT] [--inflight-query TEXT] [--before] [--options JSON]
3
3
  //
4
4
  // The client of a PostgreSQL cell: node-postgres (pg 8.23.1 with pg-pool 3.14.0) on Node 24.21.0.
5
5
  // Each event is a JSON line with `ev` and `t`, the time on CLOCK_MONOTONIC in nanoseconds
@@ -24,6 +24,9 @@
24
24
  // inflight sends one query, waits for the file --go, sends one more query into the fault, and holds
25
25
  // after its end. stream sends a query each --interval-ms after the reply before, until --max-s or
26
26
  // SIGTERM. --before starts the client into a fault that holds already: its first connect meets it.
27
+ // --inflight-query is the text of the query in flight (query 2 of inflight), and --query is the
28
+ // text of each other query. Thus a slow query in flight does not delay the reply that makes the
29
+ // client ready.
27
30
 
28
31
  import { readFileSync } from "node:fs";
29
32
  import { existsSync } from "node:fs";
@@ -46,6 +49,7 @@ const interval = Number(flag("--interval-ms") ?? 50);
46
49
  const max = Number(flag("--max-s") ?? 120) * 1000;
47
50
  const go = flag("--go") ?? "/tmp/go";
48
51
  const query = flag("--query") ?? "SELECT 1";
52
+ const inflightQuery = flag("--inflight-query") ?? query;
49
53
  const before = argv.includes("--before");
50
54
  // the options of the pool. A cell gives each option that it changes, and pg-pool gives the others
51
55
  const options = JSON.parse(flag("--options") ?? "{}");
@@ -60,7 +64,7 @@ const bootId = () => {
60
64
  return null;
61
65
  }
62
66
  };
63
- emit("start", { boot_id: bootId(), client: "pg", driver: versionOf("pg"), pool: versionOf("pg-pool"), node: process.versions.node, mode, query, options });
67
+ emit("start", { boot_id: bootId(), client: "pg", driver: versionOf("pg"), pool: versionOf("pg-pool"), node: process.versions.node, mode, query, inflight_query: inflightQuery, options });
64
68
 
65
69
  const pool = new pg.Pool({ host, port: Number(port), user: "postgres", database: "postgres", ...options });
66
70
 
@@ -116,10 +120,10 @@ setTimeout(() => {
116
120
  stop(0);
117
121
  }, max);
118
122
 
119
- /** This function sends query n through the pool. Its stamp is before the call. It gives null for a reply, or the error. */
120
- const operate = async (n) => {
123
+ /** This function sends query n through the pool, with the text of n. Its stamp is before the call. It gives null for a reply, or the error. */
124
+ const operate = async (n, text = query) => {
121
125
  const at = now();
122
- const reply = pool.query(query);
126
+ const reply = pool.query(text);
123
127
  // a band of a cell that starts into a fault counts from the start of the first connect
124
128
  if (n === 1) emit("connecting", { at: `${host}:${port}` }, at);
125
129
  emit("sent", { n }, at);
@@ -158,7 +162,7 @@ if (mode === "stream") {
158
162
  resolve();
159
163
  }, 2);
160
164
  });
161
- const error = await operate(2);
165
+ const error = await operate(2, inflightQuery);
162
166
  if (error === null) emit("data", { n: 2 });
163
167
  else emit("error", { errno: errnoOf(error), message: error.message });
164
168
  }
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.5",
4
+ "version": "0.11.7",
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",