@mark1russell7/harness 0.11.22 → 0.11.23

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 { $ as Subject, S as ImageSource, i as ClientImage, r as Client } from "./cell-BxBZUssi.js";
1
+ import { $ as Subject, S as ImageSource, i as ClientImage, r as Client } from "./cell-Dn1TvJcA.js";
2
2
  import { z } from "zod";
3
3
  //#region src/schema.d.ts
4
4
  /**
@@ -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
- emit("start", { boot_id: bootId(), mode, client: "undici", node: process.version, options, connect });
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 pending = client.request({ path, method: how, body: how === "POST" ? "x" : undefined });
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
- emit("error", { i, errno: error.code ?? "ERROR", cause: error.cause?.code ?? null, message: error.message });
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.22",
4
+ "version": "0.11.23",
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",