@mark1russell7/harness 0.11.22 → 0.11.24
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 +9 -2
- package/build/{cell-BxBZUssi.d.ts → cell-wInJC-GH.d.ts} +12 -3
- package/build/index.d.ts +7 -4
- package/build/index.js +47 -13
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +1 -1
- package/image/go-http/main.go +14 -0
- package/image/node-http/client-undici.mjs +39 -5
- package/package.json +1 -1
package/build/schema.d.ts
CHANGED
package/image/go-http/main.go
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
// [--bytes N] [--go PATH] [--max-s N] [--linger-ms N]
|
|
7
7
|
// [--idle-conn-timeout-ms N] [--max-idle-per-host N] [--dial-timeout-ms N]
|
|
8
8
|
// [--keepalive-ms N] [--keepalive-interval-ms N] [--keepalive-count N] [--timeout-ms N]
|
|
9
|
+
// [--limiter-key 1]
|
|
10
|
+
//
|
|
11
|
+
// The cells of a keyed limiter give --limiter-key 1: each request sends a key of this process in
|
|
12
|
+
// x-limiter-key, the key of the limiter of the server. Go sends no request again for a status.
|
|
9
13
|
//
|
|
10
14
|
// The first request is GET /, and the end of its reply is the ready line. Then the client holds
|
|
11
15
|
// the connection idle (idle), sends the second request when the file --go appears (inflight), or
|
|
@@ -37,6 +41,9 @@ import (
|
|
|
37
41
|
|
|
38
42
|
var out sync.Mutex
|
|
39
43
|
|
|
44
|
+
// limiterKey is the key of this process for the limiter of the server, in x-limiter-key, or empty
|
|
45
|
+
var limiterKey string
|
|
46
|
+
|
|
40
47
|
// now gives the time on CLOCK_MONOTONIC in nanoseconds: the clock of the prober.
|
|
41
48
|
func now() int64 {
|
|
42
49
|
var ts syscall.Timespec
|
|
@@ -185,6 +192,9 @@ func request(client *http.Client, base string, i int, method, path string) bool
|
|
|
185
192
|
emit("fail", map[string]any{"why": err.Error()})
|
|
186
193
|
os.Exit(1)
|
|
187
194
|
}
|
|
195
|
+
if limiterKey != "" {
|
|
196
|
+
req.Header.Set("x-limiter-key", limiterKey)
|
|
197
|
+
}
|
|
188
198
|
// the stamp comes before the call: the transport writes the request after it
|
|
189
199
|
at := now()
|
|
190
200
|
emitAt("sent", at, map[string]any{"i": i})
|
|
@@ -234,7 +244,11 @@ func main() {
|
|
|
234
244
|
intervalMs := flags.Float64("keepalive-interval-ms", 0, "KeepAliveConfig.Interval")
|
|
235
245
|
probes := flags.Int("keepalive-count", 0, "KeepAliveConfig.Count")
|
|
236
246
|
timeoutMs := flags.Float64("timeout-ms", 0, "Client.Timeout")
|
|
247
|
+
keyed := flags.Int("limiter-key", 0, "1 sends a key of this process in x-limiter-key: the key of the limiter of the server")
|
|
237
248
|
flags.Parse(os.Args[3:])
|
|
249
|
+
if *keyed == 1 {
|
|
250
|
+
limiterKey = fmt.Sprintf("%d-%d", os.Getpid(), time.Now().UnixNano())
|
|
251
|
+
}
|
|
238
252
|
given := map[string]bool{}
|
|
239
253
|
flags.Visit(func(f *flag.Flag) { given[f.Name] = true })
|
|
240
254
|
if *mode != "idle" && *mode != "inflight" && *mode != "gap" {
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// [--bytes N] [--go PATH] [--max-s N] [--linger-ms N]
|
|
3
3
|
// [--keepalive-timeout-ms N] [--keepalive-max-timeout-ms N] [--keepalive-threshold-ms N]
|
|
4
4
|
// [--headers-timeout-ms N] [--body-timeout-ms N] [--connect-timeout-ms N]
|
|
5
|
-
// [--keepalive-initial-delay-ms N]
|
|
5
|
+
// [--keepalive-initial-delay-ms N] [--limiter-key 1] [--retry 1]
|
|
6
|
+
// [--retry-max-retries N] [--retry-min-timeout-ms N] [--retry-max-timeout-ms N]
|
|
7
|
+
// [--retry-timeout-factor N]
|
|
6
8
|
//
|
|
7
9
|
// The client of the HTTP cells that uses an HTTP/1.1 Client of undici 8.12.2, at its defaults
|
|
8
10
|
// with each flag over them. Each event is a JSON line, as the TCP client of the harness writes them.
|
|
@@ -11,9 +13,16 @@
|
|
|
11
13
|
// follows each socket. The channel undici:client:sendHeaders tells which socket each request goes
|
|
12
14
|
// on. The disconnect event of the client tells why a socket ended: an end of file (other side
|
|
13
15
|
// closed), a reset, or the keep-alive timer of the client (socket idle timeout).
|
|
16
|
+
//
|
|
17
|
+
// The cells of a keyed limiter give two more flags. `--limiter-key 1` sends a key of this process
|
|
18
|
+
// in the header x-limiter-key of each request: the key of the limiter of the server. `--retry 1`
|
|
19
|
+
// sends each request through a RetryAgent of undici at its defaults (lib/handler/retry-handler.js
|
|
20
|
+
// :125-133): 5 retries, from 500 ms, a factor of 2, at most 30 s, and Retry-After read. Each
|
|
21
|
+
// retry flag sets one option over them. Each send of a request is an `attempt` line, thus each
|
|
22
|
+
// retry has its own line and its own socket.
|
|
14
23
|
|
|
15
24
|
import diagnostics from "node:diagnostics_channel";
|
|
16
|
-
import { Client, buildConnector } from "undici";
|
|
25
|
+
import { Client, RetryAgent, buildConnector } from "undici";
|
|
17
26
|
import { bootId, commandLine, emit, now, pathOf, sleep, waitFor } from "./lines.mjs";
|
|
18
27
|
|
|
19
28
|
const usage = "client-undici HOST PORT --mode idle|inflight|gap";
|
|
@@ -42,7 +51,20 @@ const connect = {};
|
|
|
42
51
|
if (cli.has("--connect-timeout-ms")) connect.timeout = cli.number("--connect-timeout-ms");
|
|
43
52
|
if (cli.has("--keepalive-initial-delay-ms")) connect.keepAliveInitialDelay = cli.number("--keepalive-initial-delay-ms");
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
// the retries of a RetryAgent: undici's defaults, and each flag over them
|
|
55
|
+
const retrying = cli.number("--retry", 0) === 1;
|
|
56
|
+
const retry = {};
|
|
57
|
+
const setRetry = (option, name) => {
|
|
58
|
+
if (cli.has(name)) retry[option] = cli.number(name);
|
|
59
|
+
};
|
|
60
|
+
setRetry("maxRetries", "--retry-max-retries");
|
|
61
|
+
setRetry("minTimeout", "--retry-min-timeout-ms");
|
|
62
|
+
setRetry("maxTimeout", "--retry-max-timeout-ms");
|
|
63
|
+
setRetry("timeoutFactor", "--retry-timeout-factor");
|
|
64
|
+
// the key of this process for the limiter of the server: each run has its own count
|
|
65
|
+
const key = cli.number("--limiter-key", 0) === 1 ? `${process.pid}-${now()}` : null;
|
|
66
|
+
|
|
67
|
+
emit("start", { boot_id: bootId(), mode, client: "undici", node: process.version, options, connect, ...(retrying ? { retry } : {}), ...(key === null ? {} : { key }) });
|
|
46
68
|
|
|
47
69
|
setTimeout(() => {
|
|
48
70
|
emit("deadline");
|
|
@@ -88,11 +110,20 @@ const client = new Client(`http://${cli.host}:${cli.port}`, {
|
|
|
88
110
|
}),
|
|
89
111
|
});
|
|
90
112
|
|
|
113
|
+
// the sends of each request: a retry of a RetryAgent sends it again, on a socket of the pool
|
|
114
|
+
const attempts = new Map();
|
|
91
115
|
diagnostics.subscribe("undici:client:sendHeaders", ({ socket }) => {
|
|
92
116
|
const entry = track(socket);
|
|
93
117
|
emit("request", { i: requestIndex, local: entry.local, reused: entry.used });
|
|
94
118
|
entry.used = true;
|
|
119
|
+
if (retrying || key !== null) {
|
|
120
|
+
const n = (attempts.get(requestIndex) ?? 0) + 1;
|
|
121
|
+
attempts.set(requestIndex, n);
|
|
122
|
+
emit("attempt", { i: requestIndex, n, local: entry.local, why: n === 1 ? "first" : "retry" });
|
|
123
|
+
}
|
|
95
124
|
});
|
|
125
|
+
// a RetryAgent over the client: each retry dispatches the request on the client again
|
|
126
|
+
const dispatcher = retrying ? new RetryAgent(client, retry) : client;
|
|
96
127
|
client.on("disconnect", (_origin, _targets, error) => {
|
|
97
128
|
// UND_ERR_INFO is a close by the client: no reuse (reset), or its keep-alive timer (socket idle timeout)
|
|
98
129
|
const by = OWN.has(error?.code) ? "client" : "peer";
|
|
@@ -103,7 +134,8 @@ client.on("disconnect", (_origin, _targets, error) => {
|
|
|
103
134
|
const request = async (i, how, path) => {
|
|
104
135
|
requestIndex = i;
|
|
105
136
|
const at = now();
|
|
106
|
-
const
|
|
137
|
+
const headers = key === null ? undefined : { "x-limiter-key": key };
|
|
138
|
+
const pending = dispatcher.request({ path, method: how, headers, body: how === "POST" ? "x" : undefined });
|
|
107
139
|
emit("sent", { i }, at);
|
|
108
140
|
try {
|
|
109
141
|
const { statusCode, body } = await pending;
|
|
@@ -117,7 +149,9 @@ const request = async (i, how, path) => {
|
|
|
117
149
|
else emit("replied", { i, n: { value: n, unit: "B" } });
|
|
118
150
|
return true;
|
|
119
151
|
} catch (error) {
|
|
120
|
-
|
|
152
|
+
// a RetryAgent that gives up gives RequestRetryError: the status of the last reply, and the count of its sends
|
|
153
|
+
const retried = error.code === "UND_ERR_REQ_RETRY" ? { status: error.statusCode ?? null, count: error.data?.count ?? null } : {};
|
|
154
|
+
emit("error", { i, errno: error.code ?? "ERROR", cause: error.cause?.code ?? null, message: error.message, ...retried });
|
|
121
155
|
return false;
|
|
122
156
|
}
|
|
123
157
|
};
|
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.24",
|
|
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",
|