@mark1russell7/harness 0.14.3 → 0.15.0

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 { T as ImageSource, i as ClientImage, r as Client, st as Subject } from "./cell-B3-BSTit.js";
1
+ import { A as ImageSource, At as Subject, i as Client, o as ClientImage } from "./cell-biffSiEJ.js";
2
2
  import { z } from "zod";
3
3
  //#region src/schema.d.ts
4
4
  /**
@@ -6,7 +6,10 @@ import { z } from "zod";
6
6
  * a build context whose paths stay in it, or an image that the harness ships.
7
7
  */
8
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. */
9
+ /**
10
+ * This schema accepts a client image (`ClientImage`): a stock image by its name or the source of an
11
+ * image, and its command. It also accepts the capabilities that its container adds.
12
+ */
10
13
  export declare const clientImageSchema: z.ZodType<ClientImage>;
11
14
  /** This schema accepts the client of a cell (`Cell.client`): a client that the harness ships by its name, or a client image. */
12
15
  export declare const clientSchema: z.ZodType<Client | ClientImage>;
package/build/schema.js CHANGED
@@ -1,4 +1,4 @@
1
- import { E as subjectProblems, a as SHIPPED_NAMES, h as imageProblems, t as CLIENT_NAMES } from "./image-BMdV7yWD.js";
1
+ import { D as subjectProblems, a as SHIPPED_NAMES, h as imageProblems, t as CLIENT_NAMES, z as CLIENT_CAPABILITIES } from "./image-BxVTFZ1o.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. */
@@ -33,10 +33,14 @@ const sourceShape = z.discriminatedUnion("kind", [
33
33
  * a build context whose paths stay in it, or an image that the harness ships.
34
34
  */
35
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. */
36
+ /**
37
+ * This schema accepts a client image (`ClientImage`): a stock image by its name or the source of an
38
+ * image, and its command. It also accepts the capabilities that its container adds.
39
+ */
37
40
  const clientImageSchema = z.strictObject({
38
41
  image: z.union([z.string().min(1), imageSourceSchema]),
39
- command
42
+ command,
43
+ capabilities: z.array(z.enum(CLIENT_CAPABILITIES)).optional()
40
44
  });
41
45
  /** This schema accepts the client of a cell (`Cell.client`): a client that the harness ships by its name, or a client image. */
42
46
  const clientSchema = z.union([z.enum(CLIENT_NAMES), clientImageSchema]);
@@ -1 +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"}
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 { CLIENT_CAPABILITIES } from \"./cell.ts\";\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/**\n * This schema accepts a client image (`ClientImage`): a stock image by its name or the source of an\n * image, and its command. It also accepts the capabilities that its container adds.\n */\nexport const clientImageSchema: z.ZodType<ClientImage> = z.strictObject({\n image: z.union([z.string().min(1), imageSourceSchema]),\n command,\n capabilities: z.array(z.enum(CLIENT_CAPABILITIES)).optional(),\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":";;;;AASA,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;;;;;AAMtG,MAAa,oBAA4C,EAAE,aAAa;CACtE,OAAO,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC;CACrD;CACA,cAAc,EAAE,MAAM,EAAE,KAAK,mBAAmB,CAAC,CAAC,CAAC,SAAS;AAC9D,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"}
package/image/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
- # The harness's own image: the prober, the cell client, the injector and the namespace admin all
2
- # run from it. A target's image stays stock. Both stages are pinned by digest,
1
+ # The harness's own image: the prober, the cell client, the load driver of HTTP, the injector and
2
+ # the namespace admin all run from it. A target's image stays stock. Both stages are pinned by digest,
3
3
  # the crates by Cargo.lock, and the packages apk installs are recorded in each run's manifest.
4
4
  # lab.ts stages the build context: the prober's and the harness's native sources, and the prober's
5
5
  # identity fixture, each under its package's name.
@@ -26,6 +26,6 @@ FROM alpine:3.23@sha256:865b95f46d98cf867a156fe4a135ad3fe50d2056aa3f25ed31662dff
26
26
  # tc for netem, ethtool for the offloads that decide whether netem acts on segments
27
27
  RUN apk add --no-cache iproute2 iproute2-tc iptables ethtool libelf zlib zstd-libs libgcc
28
28
  COPY --from=build /src/prober.bpf.o /src/prober/native/target/release/prober /opt/harness/
29
- COPY --from=build /src/harness/image/native/target/release/client /src/harness/image/native/target/release/inject /opt/harness/
29
+ COPY --from=build /src/harness/image/native/target/release/client /src/harness/image/native/target/release/inject /src/harness/image/native/target/release/queue /src/harness/image/native/target/release/ports /src/harness/image/native/target/release/ctfull /src/harness/image/native/target/release/load /opt/harness/
30
30
  WORKDIR /opt/harness
31
31
  CMD ["sleep", "infinity"]
@@ -9,11 +9,13 @@
9
9
  // HOSTS is host:port,host:port,... in the sequence of the connects: the first is the node of the
10
10
  // client. amqplib connects to one node and does not connect again by itself. When the connection ends,
11
11
  // this client connects to the next node of the list after --reconnect-ms (1000 ms), as an application
12
- // does. Then it publishes again the sequence number that has no confirm.
12
+ // does. Then it publishes again each sequence number that has no confirm.
13
13
  //
14
14
  // The options (--options JSON): `heartbeat` in seconds goes in the URL. Without it, amqplib takes the
15
15
  // heartbeat that the broker proposes. `user` and `password` are the user of the broker: guest, as
16
- // amqplib gives it, when the options name none.
16
+ // amqplib gives it, when the options name none. `window` is the count of writers: each writer has one
17
+ // publish at a time on the one channel, thus the client has at most that count with no confirm. It is
18
+ // 1 when the options name none. Each other name is a socket option of amqplib, for example `noDelay`.
17
19
  //
18
20
  // The lines:
19
21
  // - start: the versions of amqplib and Node, the hosts and the options. clocks: the wall clock.
@@ -46,7 +48,7 @@ const interval = Number(flag("--interval-ms") ?? 50);
46
48
  const max = Number(flag("--max-s") ?? 120) * 1000;
47
49
  const reconnect = Number(flag("--reconnect-ms") ?? 1000);
48
50
  const options = JSON.parse(flag("--options") ?? "{}");
49
- const { heartbeat, user = "guest", password = "guest" } = options;
51
+ const { heartbeat, user = "guest", password = "guest", window: writers = 1, ...socketOptions } = options;
50
52
 
51
53
  const { default: amqp } = await import("amqplib");
52
54
  const versionOf = (name) => JSON.parse(readFileSync(new URL(`./node_modules/${name}/package.json`, import.meta.url), "utf8")).version;
@@ -83,7 +85,7 @@ const open = async () => {
83
85
  next += 1;
84
86
  emit("connecting", { at: node });
85
87
  try {
86
- connection = await amqp.connect(urlOf(node));
88
+ connection = await amqp.connect(urlOf(node), socketOptions);
87
89
  } catch (error) {
88
90
  emit("connection", { event: "connect-failed", at: node, errno: errnoOf(error), failure: failure(error) });
89
91
  return false;
@@ -156,36 +158,71 @@ setTimeout(() => {
156
158
  }, max);
157
159
 
158
160
  let ready = false;
159
- let n = 1;
160
- while (!stopped) {
161
- if (channel === null) {
162
- if (!(await open())) {
161
+ let nextNumber = 1;
162
+ // these promises keep one connect and one end of a connection for all the writers
163
+ let opening = null;
164
+ let leaving = null;
165
+
166
+ /** This function connects when no channel is open. Each writer waits for the same connect. */
167
+ const connected = () => {
168
+ if (channel !== null) return Promise.resolve();
169
+ opening ??= (async () => {
170
+ while (!stopped) {
171
+ if (await open()) {
172
+ if (!ready) {
173
+ ready = true;
174
+ emit("ready", { at });
175
+ }
176
+ break;
177
+ }
163
178
  channel = null;
164
179
  await sleep(reconnect);
180
+ }
181
+ opening = null;
182
+ })();
183
+ return opening;
184
+ };
185
+
186
+ /** This function ends the connection of a closed channel. Each writer of that channel waits for the same end. */
187
+ const left = (closed) => {
188
+ if (closed !== channel) return leaving ?? Promise.resolve();
189
+ leaving ??= (async () => {
190
+ // the channel closed: its connection ends with it, or after one more wait the client ends the connection
191
+ const closing = connection;
192
+ await Promise.race([ended, sleep(reconnect)]);
193
+ closing?.close().catch(() => undefined);
194
+ channel = null;
195
+ connection = null;
196
+ if (!stopped) await sleep(reconnect);
197
+ leaving = null;
198
+ })();
199
+ return leaving;
200
+ };
201
+
202
+ /** This function is one writer: it takes the next sequence number at each confirm. */
203
+ const writer = async () => {
204
+ let n = nextNumber;
205
+ nextNumber += 1;
206
+ while (!stopped) {
207
+ await leaving;
208
+ await connected();
209
+ if (stopped) return;
210
+ const on = channel;
211
+ // a publish ends at its confirm, at a nack, or at the close of its channel
212
+ const failed = await publish(n);
213
+ if (failed === null) {
214
+ n = nextNumber;
215
+ nextNumber += 1;
216
+ await sleep(interval);
165
217
  continue;
166
218
  }
167
- if (!ready) {
168
- ready = true;
169
- emit("ready", { at });
219
+ // the same sequence number goes again: on the same channel after a nack, or on the next node after an end
220
+ if (failed.message === "message nacked") {
221
+ await sleep(interval);
222
+ continue;
170
223
  }
224
+ await left(on);
171
225
  }
172
- // a publish ends at its confirm, at a nack, or at the close of its channel
173
- const failed = await publish(n);
174
- if (failed === null) {
175
- n += 1;
176
- await sleep(interval);
177
- continue;
178
- }
179
- // the same sequence number goes again: on the same channel after a nack, or on the next node after an end
180
- if (failed.message === "message nacked") {
181
- await sleep(interval);
182
- continue;
183
- }
184
- // the channel closed: its connection ends with it, or after one more wait the client ends the connection
185
- const closing = connection;
186
- await Promise.race([ended, sleep(reconnect)]);
187
- closing?.close().catch(() => undefined);
188
- channel = null;
189
- connection = null;
190
- if (!stopped) await sleep(reconnect);
191
- }
226
+ };
227
+
228
+ await Promise.all(Array.from({ length: writers }, writer));
@@ -1,5 +1,6 @@
1
1
  // client-hikari HOST PORT --mode idle|inflight|stream [--interval-ms N] [--max-s N] [--go PATH]
2
2
  // [--query TEXT] [--inflight-query TEXT] [--before] [--options JSON]
3
+ // [--burst N --burst-gap-ms N]
3
4
  //
4
5
  // The client of a HikariCP cell: HikariCP 7.1.0 over pgjdbc 42.7.13, on Java 21. Each event is a
5
6
  // JSON line with `ev` and `t`, the time on CLOCK_MONOTONIC in nanoseconds (System.nanoTime, which
@@ -18,6 +19,7 @@
18
19
  // - sent, ack, fail: each query by its sequence number, with its error.
19
20
  // - data, error: the end of the query in flight, as the TCP client of the harness tells it. In idle
20
21
  // mode, the close of the socket of the first connection is an error line too: the pool closed it.
22
+ // - burst: the end of a burst: its counts of replies and of failures, then the counts of the pool.
21
23
  // - done, deadline: the end of the client.
22
24
  //
23
25
  // The modes are those of the pg client. idle sends one query, closes its connection (the
@@ -27,9 +29,15 @@
27
29
  // already: the start of its pool meets it. --inflight-query is the text of the query in flight
28
30
  // (query 2 of inflight), and --query is the text of each other query.
29
31
  //
32
+ // --burst sends N queries in flight in place of one, each on its own thread, query 2 to query N + 1.
33
+ // The starts are --burst-gap-ms apart, thus the queue of the pool has the order of the numbers. The
34
+ // burst starts when each connection of the pool is idle.
35
+ //
30
36
  // The options are one flat JSON object. A name with the prefix `dataSource.` is a property of
31
- // pgjdbc. Each other name is a property of HikariConfig. A cell gives each option that it changes,
32
- // and HikariCP and pgjdbc give the others.
37
+ // pgjdbc. A name with the prefix `system.` is a system property of the JVM. The client sets it
38
+ // after its warm-up and before its pool, for example `system.com.zaxxer.hikari.aliveBypassWindowMs`,
39
+ // which HikariPool reads when the pool starts. Each other name is a property of HikariConfig. A cell
40
+ // gives each option that it changes, and HikariCP and pgjdbc give the others.
33
41
 
34
42
  import com.zaxxer.hikari.HikariConfig;
35
43
  import com.zaxxer.hikari.HikariDataSource;
@@ -46,12 +54,15 @@ import java.sql.Connection;
46
54
  import java.sql.ResultSet;
47
55
  import java.sql.SQLException;
48
56
  import java.sql.Statement;
57
+ import java.util.ArrayList;
49
58
  import java.util.LinkedHashMap;
50
59
  import java.util.List;
51
60
  import java.util.Map;
52
61
  import java.util.Properties;
53
62
  import java.util.Set;
54
63
  import java.util.concurrent.ConcurrentHashMap;
64
+ import java.util.concurrent.atomic.AtomicInteger;
65
+ import java.util.concurrent.locks.LockSupport;
55
66
  import org.postgresql.core.PGStream;
56
67
  import org.postgresql.jdbc.PgConnection;
57
68
  import org.postgresql.util.PSQLException;
@@ -61,6 +72,9 @@ public final class ClientHikari {
61
72
  private static final PrintStream OUT = new PrintStream(new FileOutputStream(FileDescriptor.out), false, StandardCharsets.UTF_8);
62
73
  private static final Object LOCK = new Object();
63
74
  private static final Set<String> SEEN = ConcurrentHashMap.newKeySet();
75
+ /** This time is the start of a burst after its threads start, in nanoseconds: each thread then waits for its own start. */
76
+ private static final long BURST_LEAD = 50_000_000L;
77
+ private static final String SYSTEM = "system.";
64
78
  private static volatile boolean stopped = false;
65
79
  private static volatile HikariDataSource pool;
66
80
  private static volatile String connected;
@@ -405,6 +419,58 @@ public final class ClientHikari {
405
419
  failure(cancelled);
406
420
  }
407
421
 
422
+ /**
423
+ * This function loads the code of a borrow that times out, before the first line of a burst. One
424
+ * borrow waits on a pool that cannot connect, for the 250 ms floor of connectionTimeout
425
+ * (HikariConfig.java:189-200). A new JVM would otherwise load that code in the band of the first
426
+ * failure.
427
+ */
428
+ private static void warmTimeout() {
429
+ HikariConfig empty = new HikariConfig();
430
+ empty.setJdbcUrl("jdbc:postgresql://127.0.0.1:1/warmup");
431
+ empty.setUsername("warmup");
432
+ empty.setPoolName("warmup");
433
+ empty.setInitializationFailTimeout(-1);
434
+ empty.setMinimumIdle(0);
435
+ empty.setConnectionTimeout(250);
436
+ try (HikariDataSource ds = new HikariDataSource(empty); Connection connection = ds.getConnection()) {
437
+ throw new IllegalStateException("the warm-up pool gave a connection");
438
+ } catch (SQLException expected) {
439
+ failure(expected);
440
+ }
441
+ }
442
+
443
+ /**
444
+ * This function sends the queries of a burst, each on its own thread, and writes the burst line
445
+ * when each query ended. Each thread waits for its start on the clock, thus a slow start of one
446
+ * thread does not move the next one.
447
+ */
448
+ private static void burst(int count, long gap) throws InterruptedException {
449
+ HikariPoolMXBean bean = pool.getHikariPoolMXBean();
450
+ while (bean.getIdleConnections() < config.getMaximumPoolSize()) Thread.sleep(2);
451
+ AtomicInteger acks = new AtomicInteger();
452
+ AtomicInteger fails = new AtomicInteger();
453
+ long first = now() + BURST_LEAD;
454
+ List<Thread> threads = new ArrayList<>();
455
+ for (int k = 0; k < count; k += 1) {
456
+ int n = k + 2;
457
+ long start = first + k * gap;
458
+ Thread thread = new Thread(() -> {
459
+ for (long left = start - now(); left > 0; left = start - now()) LockSupport.parkNanos(left);
460
+ (operate(n, inflightQuery) == null ? acks : fails).incrementAndGet();
461
+ }, "burst-" + n);
462
+ thread.setDaemon(true);
463
+ thread.start();
464
+ threads.add(thread);
465
+ }
466
+ for (Thread thread : threads) thread.join();
467
+ emit("burst", "\"acks\":" + acks.get() + ",\"fails\":" + fails.get()
468
+ + ",\"total\":" + bean.getTotalConnections()
469
+ + ",\"active\":" + bean.getActiveConnections()
470
+ + ",\"idle\":" + bean.getIdleConnections()
471
+ + ",\"waiting\":" + bean.getThreadsAwaitingConnection());
472
+ }
473
+
408
474
  public static void main(String[] args) throws Exception {
409
475
  List<String> argv = List.of(args);
410
476
  String mode = flag(argv, "--mode", null);
@@ -421,7 +487,10 @@ public final class ClientHikari {
421
487
  inflightQuery = flag(argv, "--inflight-query", query);
422
488
  boolean before = argv.contains("--before");
423
489
  Map<String, String> options = parseObject(flag(argv, "--options", "{}"));
490
+ int burst = Integer.parseInt(flag(argv, "--burst", "0"));
491
+ long burstGap = Long.parseLong(flag(argv, "--burst-gap-ms", "0")) * 1_000_000L;
424
492
  warmUp();
493
+ if (burst > 0) warmTimeout();
425
494
 
426
495
  String bootId;
427
496
  try {
@@ -443,7 +512,10 @@ public final class ClientHikari {
443
512
  + ",\"mode\":" + q(mode) + ",\"query\":" + q(query) + ",\"inflight_query\":" + q(inflightQuery) + ",\"options\":" + given);
444
513
 
445
514
  Properties properties = new Properties();
446
- for (Map.Entry<String, String> each : options.entrySet()) properties.setProperty(each.getKey(), each.getValue());
515
+ for (Map.Entry<String, String> each : options.entrySet()) {
516
+ if (each.getKey().startsWith(SYSTEM)) System.setProperty(each.getKey().substring(SYSTEM.length()), each.getValue());
517
+ else properties.setProperty(each.getKey(), each.getValue());
518
+ }
447
519
  config = new HikariConfig(properties);
448
520
  config.setJdbcUrl("jdbc:postgresql://" + host + ":" + port + "/postgres");
449
521
  // the user of a cell, when it gives one: Supavisor routes a client by its user, <db_user>.<tenant>
@@ -492,9 +564,12 @@ public final class ClientHikari {
492
564
  if (mode.equals("idle")) watchIdle();
493
565
  if (mode.equals("inflight")) {
494
566
  while (!Files.exists(go)) Thread.sleep(2);
495
- Throwable error = operate(2, inflightQuery);
496
- if (error == null) emit("data", "\"n\":2");
497
- else emit("error", "\"errno\":" + q(errnoOf(error)) + ",\"message\":" + q(error.getMessage()));
567
+ if (burst > 0) burst(burst, burstGap);
568
+ else {
569
+ Throwable error = operate(2, inflightQuery);
570
+ if (error == null) emit("data", "\"n\":2");
571
+ else emit("error", "\"errno\":" + q(errnoOf(error)) + ",\"message\":" + q(error.getMessage()));
572
+ }
498
573
  }
499
574
  Thread.sleep(Long.MAX_VALUE);
500
575
  }
@@ -0,0 +1,152 @@
1
+ // admin-kafka BOOTSTRAP create TOPIC MIN_INSYNC BROKER,BROKER,... [PARTITIONS]
2
+ // admin-kafka BOOTSTRAP view [TOPIC]
3
+ // admin-kafka BOOTSTRAP holds TOPIC
4
+ // admin-kafka BOOTSTRAP fill TOPIC PARTITIONS COUNT
5
+ //
6
+ // The admin of a Kafka lab, on the Admin client, the consumer and the producer of kafka-clients 4.3.2.
7
+ // Each command prints one JSON line. An error ends the command with exit code 1, and the lab then
8
+ // reads the error.
9
+ // - create: a topic with one partition or more, its replicas on the brokers in sequence, and its
10
+ // setting of min.insync.replicas. The first broker is the first leader of partition 0. Each next
11
+ // partition starts its replicas one broker later.
12
+ // - fill: COUNT records in each partition, each acknowledged by each in-sync replica.
13
+ // - view: the ids of the brokers that are not fenced. With a topic, also the leader, the in-sync
14
+ // replicas and the replicas of its partition. The controller lists only brokers that are not fenced.
15
+ // - holds: the value of each record of the partition from its first offset to its high watermark,
16
+ // one entry for each copy.
17
+
18
+ import java.time.Duration;
19
+ import java.util.ArrayList;
20
+ import java.util.Collections;
21
+ import java.util.HashMap;
22
+ import java.util.List;
23
+ import java.util.Map;
24
+ import java.util.Properties;
25
+ import java.util.concurrent.Future;
26
+ import org.apache.kafka.clients.admin.Admin;
27
+ import org.apache.kafka.clients.admin.AdminClientConfig;
28
+ import org.apache.kafka.clients.admin.NewTopic;
29
+ import org.apache.kafka.clients.admin.TopicDescription;
30
+ import org.apache.kafka.clients.consumer.ConsumerConfig;
31
+ import org.apache.kafka.clients.consumer.ConsumerRecord;
32
+ import org.apache.kafka.clients.consumer.KafkaConsumer;
33
+ import org.apache.kafka.clients.producer.KafkaProducer;
34
+ import org.apache.kafka.clients.producer.ProducerConfig;
35
+ import org.apache.kafka.clients.producer.ProducerRecord;
36
+ import org.apache.kafka.clients.producer.RecordMetadata;
37
+ import org.apache.kafka.common.Node;
38
+ import org.apache.kafka.common.TopicPartition;
39
+ import org.apache.kafka.common.TopicPartitionInfo;
40
+ import org.apache.kafka.common.serialization.StringDeserializer;
41
+ import org.apache.kafka.common.serialization.StringSerializer;
42
+
43
+ public final class AdminKafka {
44
+ /** This time bounds each call of the admin, in ms. It has no source: a lab repeats a view that fails. */
45
+ private static final String CALL_MS = "10000";
46
+ /** This time bounds a whole read of the partition, in ms. It has no source: it is far above the read of a run. */
47
+ private static final long READ_MS = 60_000;
48
+
49
+ private AdminKafka() {}
50
+
51
+ private static Admin adminOf(String bootstrap) {
52
+ Properties properties = new Properties();
53
+ properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrap);
54
+ properties.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, CALL_MS);
55
+ properties.put(AdminClientConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, CALL_MS);
56
+ return Admin.create(properties);
57
+ }
58
+
59
+ private static String idsOf(List<Node> nodes) {
60
+ List<String> ids = new ArrayList<>();
61
+ for (Node node : nodes) ids.add(String.valueOf(node.id()));
62
+ return "[" + String.join(",", ids) + "]";
63
+ }
64
+
65
+ private static void create(String bootstrap, String topic, String minInsync, String brokers, int partitions) throws Exception {
66
+ List<Integer> replicas = new ArrayList<>();
67
+ for (String id : brokers.split(",")) replicas.add(Integer.valueOf(id));
68
+ Map<Integer, List<Integer>> assignments = new HashMap<>();
69
+ for (int partition = 0; partition < partitions; partition += 1) {
70
+ assignments.put(partition, new ArrayList<>(replicas));
71
+ Collections.rotate(replicas, -1);
72
+ }
73
+ try (Admin admin = adminOf(bootstrap)) {
74
+ NewTopic created = new NewTopic(topic, assignments).configs(Map.of("min.insync.replicas", minInsync));
75
+ admin.createTopics(List.of(created)).all().get();
76
+ }
77
+ System.out.println("{\"ok\":true}");
78
+ }
79
+
80
+ private static void fill(String bootstrap, String topic, int partitions, int count) throws Exception {
81
+ Properties properties = new Properties();
82
+ properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrap);
83
+ properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
84
+ properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
85
+ properties.put(ProducerConfig.ACKS_CONFIG, "all");
86
+ try (KafkaProducer<String, String> producer = new KafkaProducer<>(properties)) {
87
+ List<Future<RecordMetadata>> sent = new ArrayList<>();
88
+ for (int partition = 0; partition < partitions; partition += 1) {
89
+ for (int at = 0; at < count; at += 1) sent.add(producer.send(new ProducerRecord<>(topic, partition, null, String.valueOf(at))));
90
+ }
91
+ producer.flush();
92
+ for (Future<RecordMetadata> each : sent) each.get();
93
+ }
94
+ System.out.println("{\"ok\":true}");
95
+ }
96
+
97
+ private static void view(String bootstrap, String topic) throws Exception {
98
+ try (Admin admin = adminOf(bootstrap)) {
99
+ String brokers = idsOf(new ArrayList<>(admin.describeCluster().nodes().get()));
100
+ if (topic == null) {
101
+ System.out.println("{\"brokers\":" + brokers + "}");
102
+ return;
103
+ }
104
+ TopicDescription described = admin.describeTopics(List.of(topic)).allTopicNames().get().get(topic);
105
+ TopicPartitionInfo partition = described.partitions().get(0);
106
+ System.out.println("{\"brokers\":" + brokers
107
+ + ",\"leader\":" + (partition.leader() == null ? "null" : String.valueOf(partition.leader().id()))
108
+ + ",\"isr\":" + idsOf(partition.isr())
109
+ + ",\"replicas\":" + idsOf(partition.replicas()) + "}");
110
+ }
111
+ }
112
+
113
+ private static void holds(String bootstrap, String topic) {
114
+ Properties properties = new Properties();
115
+ properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrap);
116
+ properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
117
+ properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
118
+ properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
119
+ properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
120
+ TopicPartition partition = new TopicPartition(topic, 0);
121
+ List<String> held = new ArrayList<>();
122
+ try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties)) {
123
+ consumer.assign(List.of(partition));
124
+ consumer.seekToBeginning(List.of(partition));
125
+ long end = consumer.endOffsets(List.of(partition)).get(partition);
126
+ long begun = System.currentTimeMillis();
127
+ while (consumer.position(partition) < end) {
128
+ if (System.currentTimeMillis() - begun > READ_MS) throw new IllegalStateException("the read stopped at offset " + consumer.position(partition) + " of " + end);
129
+ for (ConsumerRecord<String, String> record : consumer.poll(Duration.ofMillis(500))) held.add(record.value());
130
+ }
131
+ System.out.println("{\"held\":[" + String.join(",", held) + "],\"end\":" + end + "}");
132
+ }
133
+ }
134
+
135
+ public static void main(String[] args) throws Exception {
136
+ if (args.length < 2) {
137
+ System.err.println("admin-kafka BOOTSTRAP create|view|holds|fill ...");
138
+ System.exit(1);
139
+ }
140
+ switch (args[1]) {
141
+ case "create" -> create(args[0], args[2], args[3], args[4], args.length > 5 ? Integer.parseInt(args[5]) : 1);
142
+ case "view" -> view(args[0], args.length > 2 ? args[2] : null);
143
+ case "holds" -> holds(args[0], args[2]);
144
+ case "fill" -> fill(args[0], args[2], Integer.parseInt(args[3]), Integer.parseInt(args[4]));
145
+ default -> {
146
+ System.err.println("no command " + args[1]);
147
+ System.exit(1);
148
+ }
149
+ }
150
+ System.exit(0);
151
+ }
152
+ }