@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/README.md +21 -5
- package/build/{cell-B3-BSTit.d.ts → cell-biffSiEJ.d.ts} +415 -10
- package/build/{image-BMdV7yWD.js → image-BxVTFZ1o.js} +30 -2
- package/build/image-BxVTFZ1o.js.map +1 -0
- package/build/index.d.ts +509 -5
- package/build/index.js +2208 -245
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +5 -2
- package/build/schema.js +7 -3
- package/build/schema.js.map +1 -1
- package/image/Dockerfile +3 -3
- package/image/amqp/client-cluster.mjs +68 -31
- package/image/hikari/ClientHikari.java +81 -6
- package/image/kafka/AdminKafka.java +152 -0
- package/image/kafka/ClientKafka.java +234 -0
- package/image/kafka/ConsumerKafka.java +359 -0
- package/image/kafka/Dockerfile +23 -0
- package/image/kafka/jars.sha256 +5 -0
- package/image/native/Cargo.toml +1 -1
- package/image/native/src/bin/client.rs +164 -28
- package/image/native/src/bin/ctfull.rs +633 -0
- package/image/native/src/bin/inject.rs +16 -0
- package/image/native/src/bin/load.rs +481 -0
- package/image/native/src/bin/ports.rs +367 -0
- package/image/native/src/bin/queue.rs +542 -0
- package/image/native/src/lib.rs +51 -0
- package/image/node/client-sentinel.mjs +28 -12
- package/image/pg/client-pg.mjs +31 -4
- package/package.json +9 -2
- package/build/image-BMdV7yWD.js.map +0 -1
package/build/schema.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
/**
|
|
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]);
|
package/build/schema.js.map
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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.
|
|
32
|
-
//
|
|
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())
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
}
|