@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.
- package/README.md +41 -7
- package/build/cell-Di_YHiuB.d.ts +999 -0
- package/build/image-BfuXg-5t.js +427 -0
- package/build/image-BfuXg-5t.js.map +1 -0
- package/build/index.d.ts +119 -945
- package/build/index.js +22 -193
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +19 -0
- package/build/schema.js +56 -0
- package/build/schema.js.map +1 -0
- package/image/hikari/ClientHikari.java +424 -0
- package/image/hikari/Dockerfile +19 -0
- package/image/hikari/jars.sha256 +3 -0
- package/image/pg/client-pg.mjs +4 -0
- package/package.json +8 -1
|
@@ -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
|
package/build/schema.js
ADDED
|
@@ -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
|
package/image/pg/client-pg.mjs
CHANGED
|
@@ -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.
|
|
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",
|