@mark1russell7/kind 0.10.1 → 0.11.1
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 +13 -0
- package/build/index.d.ts +40 -3
- package/build/index.js +95 -8
- package/build/index.js.map +1 -1
- package/build/{judge-BbLWVTYj.js → judge-Bb4ebE9V.js} +8 -4
- package/build/{judge-BbLWVTYj.js.map → judge-Bb4ebE9V.js.map} +1 -1
- package/build/{judge-BD4UVQfy.d.ts → judge-C3mbgYxY.d.ts} +30 -4
- package/build/judge.d.ts +2 -2
- package/build/judge.js +1 -1
- package/image/app.mjs +11 -4
- package/image/client.mjs +148 -10
- package/package.json +2 -2
|
@@ -132,9 +132,12 @@ type Stack = "service" | "grpc";
|
|
|
132
132
|
* - `ztunnel-restart` deletes the ztunnel pod of each node with its grace. The DaemonSet makes a new pod on each node (T15).
|
|
133
133
|
* - `certificate-expiry` keeps the calls of each client going for the hold. No act of the lab touches a
|
|
134
134
|
* connection. The lab reads the workload certificates of ztunnel at the fault and at the heal (T15).
|
|
135
|
+
* - `cannot-serve` makes the route of one replica fail while a flag file is there, for the hold. The
|
|
136
|
+
* flag also fails the readiness check of the replica (`flag` of the declaration). Before the
|
|
137
|
+
* fault, each client opens its connections again with SIGHUP, thus they spread over the replicas.
|
|
135
138
|
* - `none` injects nothing: a calibration measures the path itself.
|
|
136
139
|
*/
|
|
137
|
-
type TriggerKind = "pod-deletion" | "rolling-update" | "node-drain" | "scale-in" | "crash" | "liveness-restart" | "freeze-process" | "rst" | "blackhole" | "peer-gone-without-close" | "app-stall" | "rollout" | "cpu-throttle" | "authorization-policy" | "idle" | "ztunnel-restart" | "certificate-expiry";
|
|
140
|
+
type TriggerKind = "pod-deletion" | "rolling-update" | "node-drain" | "scale-in" | "crash" | "liveness-restart" | "freeze-process" | "rst" | "blackhole" | "peer-gone-without-close" | "app-stall" | "rollout" | "cpu-throttle" | "authorization-policy" | "idle" | "ztunnel-restart" | "certificate-expiry" | "cannot-serve";
|
|
138
141
|
/**
|
|
139
142
|
* A trigger is its kind and the settings of its fault. The parameter `A` is the form of an amount:
|
|
140
143
|
* a quantity in the format `/2`, a number in the format `/1`.
|
|
@@ -227,10 +230,27 @@ interface Declaration {
|
|
|
227
230
|
readonly tick: Quantity;
|
|
228
231
|
readonly threshold: Quantity;
|
|
229
232
|
} | null;
|
|
233
|
+
/**
|
|
234
|
+
* This flag is the lame-duck state of the HTTP edge. With true or no value, the edge closes each
|
|
235
|
+
* connection in band while the service is not ready, thus each caller opens a new connection. On
|
|
236
|
+
* HTTP/1.1 the signal is `Connection: close`, and on HTTP/2 it is GOAWAY. With false, the edge keeps
|
|
237
|
+
* each connection open, thus a caller learns only from the endpoints.
|
|
238
|
+
*/
|
|
239
|
+
readonly lameDuck?: boolean | undefined;
|
|
240
|
+
/**
|
|
241
|
+
* This text is the protocol of the HTTP edge and of its clients. `h2c` is HTTP/2 without TLS: the
|
|
242
|
+
* app listens with `node:http2`, and each client sends its requests as streams on its sessions.
|
|
243
|
+
* With `http/1.1` or no value, the edge is `node:http`.
|
|
244
|
+
*/
|
|
245
|
+
readonly protocol?: EdgeProtocol | undefined;
|
|
230
246
|
}
|
|
247
|
+
/** An edge protocol is the protocol of the HTTP edge of the service: HTTP/1.1, or HTTP/2 without TLS (`h2c`). */
|
|
248
|
+
type EdgeProtocol = "http/1.1" | "h2c";
|
|
231
249
|
/**
|
|
232
250
|
* A workload is the clients: one pod on each node, each with requests to the ClusterIP. The HTTP client
|
|
233
|
-
* opens a new connection for each request
|
|
251
|
+
* opens a new connection for each request, or it keeps `connections` open. On HTTP/2 (`protocol` of
|
|
252
|
+
* the declaration), each connection is a session, and each request is a stream on it. The gRPC client
|
|
253
|
+
* sends each call on its one channel.
|
|
234
254
|
*/
|
|
235
255
|
interface Workload {
|
|
236
256
|
/** This time is the interval between two requests of one client. The clients do not wait for an answer. */
|
|
@@ -240,6 +260,12 @@ interface Workload {
|
|
|
240
260
|
* gRPC has no deadline at the defaults of grpc-js, thus the client cancels it at this time.
|
|
241
261
|
*/
|
|
242
262
|
readonly timeout: Quantity;
|
|
263
|
+
/**
|
|
264
|
+
* This count is the keep-alive connections of each HTTP client. Each request goes on the next
|
|
265
|
+
* connection in turn. With no value, the client opens a new connection for each request. A client
|
|
266
|
+
* of HTTP/2 keeps this count of sessions, and it needs the count.
|
|
267
|
+
*/
|
|
268
|
+
readonly connections?: number | undefined;
|
|
243
269
|
}
|
|
244
270
|
/**
|
|
245
271
|
* A band is a range of times after a reference event of the run. The format `/1` gives each end
|
|
@@ -1015,5 +1041,5 @@ declare const quantileOf: (sorted: readonly number[], p: number) => number;
|
|
|
1015
1041
|
*/
|
|
1016
1042
|
declare const medianInterval: (sorted: readonly number[]) => [number, number];
|
|
1017
1043
|
//#endregion
|
|
1018
|
-
export { updatesOf as $, Declaration as $t, TriggerV1 as A,
|
|
1019
|
-
//# sourceMappingURL=judge-
|
|
1044
|
+
export { updatesOf as $, Declaration as $t, TriggerV1 as A, nodeNames as An, rulesOf as At, RunCheck as B, ztunnelPodsOf as Bt, KindCellResult as C, NODE_IMAGES as Cn, outcomesOf as Ct, PredictionV1 as D, goDuration as Dn, requestsOf as Dt, LoadSample as E, cidrRange as En, recordsOf as Et, KIND_NOISE as F, slicesOf as Ft, judgeKind as G, PORT as Gt, checkRun as H, CLIENT as Ht, KindDecision as I, socketsOf as It, propagationOf as J, bandOf as Jt, observationAfter as K, REDIS as Kt, KindVerdict as L, stoppingOf as Lt, upgrade as M, scenarioProblems as Mn, signalsOf as Mt, upgradeCell as N, sliceDropOf as Nt, RunCheckV1 as O, kindConfig as On, restartOf as Ot, Deleted as P, sliceEmptyOf as Pt, unitOf as Q, Band as Qt, LAST_SLACK as R, transitionOf as Rt, KindCellRecordV1 as S, KubeProxyMode as Sn, kernelCounts as St, KindVerdictV1 as T, cidrOverlap as Tn, podsOf as Tt, deletionsOf as U, HEALTH_PORT as Ut, RunFacts as V, ztunnelReplaced as Vt, factsOf as W, LOG_LABEL as Wt, referenceOf as X, secondsOf as Xt, propagationSource as Y, seconds as Yt, reusesReset as Z, showBand as Zt, ClusterManifest as _, WindowPrediction as _n, dropOf as _t, STAGES as a, MeshFlavor as an, RecordLine as at, FORMAT_V1 as b, KindConfig as bn, intakeOf as bt, readRecord as c, Prediction as cn, SliceLine as ct, calibrationOf as d, Replica as dn, TunnelLine as dt, EdgeProtocol as en, Endpoint as et, cellsTable as f, ResetReuse as fn, ZtunnelPodLine as ft, BandV1 as g, TriggerKind as gn, deletionOf as gt, timeSpread as h, Trigger as hn, byNode as ht, quantileOf as i, Mesh as in, PropagationSource as it, judge as j, scenario as jn, signalOf as jt, RunFactsV1 as k, kubeProxyPatch as kn, restartsAt as kt, Judged as l, RedisDeclaration as ln, SocketLine as lt, predictionText as m, Template as mn, byClients as mt, medianInterval as n, KindRun as nn, Line as nt, kindCellRecordSchema as o, MeshMode as on, Request as ot, medianText as p, Stack as pn, backendsAt as pt, observationEnd as q, SERVER as qt, middle as r, KubeletProbe as rn, PodLine as rt, kindCellRecordV1Schema as s, PodSettings as sn, RulesLine as st, RELATIVE as t, KindCell as tn, HBONE_PORT as tt, TimeSpread as u, Reference as un, TCP_STATE as ut, EnvironmentManifest as v, Workload as vn, emptyOf as vt, KindCellV1 as w, NodeImage as wn, podsAt as wt, KindCellRecord as x, KindScenario as xn, isTunnelState as xt, FORMAT as y, KUBE_PROXY_MODES as yn, failuresOf as yt, Propagated as z, tunnelsOf as zt };
|
|
1045
|
+
//# sourceMappingURL=judge-C3mbgYxY.d.ts.map
|
package/build/judge.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as updatesOf, $t as Declaration, A as TriggerV1, At as rulesOf, B as RunCheck, Bt as ztunnelPodsOf, C as KindCellResult, Ct as outcomesOf, D as PredictionV1, Dt as requestsOf, E as LoadSample, Et as recordsOf, F as KIND_NOISE, Ft as slicesOf, G as judgeKind, Gt as PORT, H as checkRun, Ht as CLIENT, I as KindDecision, It as socketsOf, J as propagationOf, Jt as bandOf, K as observationAfter, Kt as REDIS, L as KindVerdict, Lt as stoppingOf, M as upgrade, Mt as signalsOf, N as upgradeCell, Nt as sliceDropOf, O as RunCheckV1, Ot as restartOf, P as Deleted, Pt as sliceEmptyOf, Q as unitOf, Qt as Band, R as LAST_SLACK, Rt as transitionOf, S as KindCellRecordV1, St as kernelCounts, T as KindVerdictV1, Tt as podsOf, U as deletionsOf, Ut as HEALTH_PORT, V as RunFacts, Vt as ztunnelReplaced, W as factsOf, Wt as LOG_LABEL, X as referenceOf, Xt as secondsOf, Y as propagationSource, Yt as seconds, Z as reusesReset, Zt as showBand, _ as ClusterManifest, _n as
|
|
2
|
-
export { type Band, type BandV1, CLIENT, type ClusterManifest, type Declaration, type Deleted, Endpoint, type EnvironmentManifest, FORMAT, FORMAT_V1, HBONE_PORT, HEALTH_PORT, type Judged, KIND_NOISE, type KindCell, type KindCellRecord, type KindCellRecordV1, type KindCellResult, type KindCellV1, type KindDecision, type KindRun, type KindVerdict, type KindVerdictV1, type KubeProxyMode, type KubeletProbe, LAST_SLACK, LOG_LABEL, Line, type LoadSample, type Mesh, type MeshFlavor, type MeshMode, PORT, PodLine, type PodSettings, type Prediction, type PredictionV1, type Propagated, PropagationSource, REDIS, RELATIVE, RecordLine, type RedisDeclaration, type Reference, type Replica, Request, type ResetReuse, RulesLine, type RunCheck, type RunCheckV1, type RunFacts, type RunFactsV1, SERVER, STAGES, SliceLine, SocketLine, type Stack, TCP_STATE, type Template, type TimeSpread, type Trigger, type TriggerKind, type TriggerV1, TunnelLine, type WindowPrediction, type Workload, ZtunnelPodLine, backendsAt, bandOf, byClients, byNode, calibrationOf, cellsTable, checkRun, deletionOf, deletionsOf, dropOf, emptyOf, factsOf, failuresOf, intakeOf, isTunnelState, judge, judgeKind, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, medianInterval, medianText, middle, observationAfter, observationEnd, outcomesOf, podsAt, podsOf, predictionText, propagationOf, propagationSource, quantileOf, readRecord, recordsOf, referenceOf, requestsOf, restartOf, restartsAt, reusesReset, rulesOf, seconds, secondsOf, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, slicesOf, socketsOf, stoppingOf, timeSpread, transitionOf, tunnelsOf, unitOf, updatesOf, upgrade, upgradeCell, ztunnelPodsOf, ztunnelReplaced };
|
|
1
|
+
import { $ as updatesOf, $t as Declaration, A as TriggerV1, At as rulesOf, B as RunCheck, Bt as ztunnelPodsOf, C as KindCellResult, Ct as outcomesOf, D as PredictionV1, Dt as requestsOf, E as LoadSample, Et as recordsOf, F as KIND_NOISE, Ft as slicesOf, G as judgeKind, Gt as PORT, H as checkRun, Ht as CLIENT, I as KindDecision, It as socketsOf, J as propagationOf, Jt as bandOf, K as observationAfter, Kt as REDIS, L as KindVerdict, Lt as stoppingOf, M as upgrade, Mt as signalsOf, N as upgradeCell, Nt as sliceDropOf, O as RunCheckV1, Ot as restartOf, P as Deleted, Pt as sliceEmptyOf, Q as unitOf, Qt as Band, R as LAST_SLACK, Rt as transitionOf, S as KindCellRecordV1, Sn as KubeProxyMode, St as kernelCounts, T as KindVerdictV1, Tt as podsOf, U as deletionsOf, Ut as HEALTH_PORT, V as RunFacts, Vt as ztunnelReplaced, W as factsOf, Wt as LOG_LABEL, X as referenceOf, Xt as secondsOf, Y as propagationSource, Yt as seconds, Z as reusesReset, Zt as showBand, _ as ClusterManifest, _n as WindowPrediction, _t as dropOf, a as STAGES, an as MeshFlavor, at as RecordLine, b as FORMAT_V1, bt as intakeOf, c as readRecord, cn as Prediction, ct as SliceLine, d as calibrationOf, dn as Replica, dt as TunnelLine, en as EdgeProtocol, et as Endpoint, f as cellsTable, fn as ResetReuse, ft as ZtunnelPodLine, g as BandV1, gn as TriggerKind, gt as deletionOf, h as timeSpread, hn as Trigger, ht as byNode, i as quantileOf, in as Mesh, it as PropagationSource, j as judge, jt as signalOf, k as RunFactsV1, kt as restartsAt, l as Judged, ln as RedisDeclaration, lt as SocketLine, m as predictionText, mn as Template, mt as byClients, n as medianInterval, nn as KindRun, nt as Line, o as kindCellRecordSchema, on as MeshMode, ot as Request, p as medianText, pn as Stack, pt as backendsAt, q as observationEnd, qt as SERVER, r as middle, rn as KubeletProbe, rt as PodLine, s as kindCellRecordV1Schema, sn as PodSettings, st as RulesLine, t as RELATIVE, tn as KindCell, tt as HBONE_PORT, u as TimeSpread, un as Reference, ut as TCP_STATE, v as EnvironmentManifest, vn as Workload, vt as emptyOf, w as KindCellV1, wt as podsAt, x as KindCellRecord, xt as isTunnelState, y as FORMAT, yt as failuresOf, z as Propagated, zt as tunnelsOf } from "./judge-C3mbgYxY.js";
|
|
2
|
+
export { type Band, type BandV1, CLIENT, type ClusterManifest, type Declaration, type Deleted, type EdgeProtocol, Endpoint, type EnvironmentManifest, FORMAT, FORMAT_V1, HBONE_PORT, HEALTH_PORT, type Judged, KIND_NOISE, type KindCell, type KindCellRecord, type KindCellRecordV1, type KindCellResult, type KindCellV1, type KindDecision, type KindRun, type KindVerdict, type KindVerdictV1, type KubeProxyMode, type KubeletProbe, LAST_SLACK, LOG_LABEL, Line, type LoadSample, type Mesh, type MeshFlavor, type MeshMode, PORT, PodLine, type PodSettings, type Prediction, type PredictionV1, type Propagated, PropagationSource, REDIS, RELATIVE, RecordLine, type RedisDeclaration, type Reference, type Replica, Request, type ResetReuse, RulesLine, type RunCheck, type RunCheckV1, type RunFacts, type RunFactsV1, SERVER, STAGES, SliceLine, SocketLine, type Stack, TCP_STATE, type Template, type TimeSpread, type Trigger, type TriggerKind, type TriggerV1, TunnelLine, type WindowPrediction, type Workload, ZtunnelPodLine, backendsAt, bandOf, byClients, byNode, calibrationOf, cellsTable, checkRun, deletionOf, deletionsOf, dropOf, emptyOf, factsOf, failuresOf, intakeOf, isTunnelState, judge, judgeKind, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, medianInterval, medianText, middle, observationAfter, observationEnd, outcomesOf, podsAt, podsOf, predictionText, propagationOf, propagationSource, quantileOf, readRecord, recordsOf, referenceOf, requestsOf, restartOf, restartsAt, reusesReset, rulesOf, seconds, secondsOf, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, slicesOf, socketsOf, stoppingOf, timeSpread, transitionOf, tunnelsOf, unitOf, updatesOf, upgrade, upgradeCell, ztunnelPodsOf, ztunnelReplaced };
|
package/build/judge.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ut as CLIENT, u as FORMAT, d as FORMAT_V1, N as HBONE_PORT, dt as HEALTH_PORT, h as KIND_NOISE, g as LAST_SLACK, ft as LOG_LABEL, pt as PORT, mt as REDIS, k as RELATIVE, ht as SERVER, t as STAGES, P as TCP_STATE, F as backendsAt, gt as bandOf, I as byClients, L as byNode, a as calibrationOf, o as cellsTable, _ as checkRun, R as deletionOf, v as deletionsOf, z as dropOf, B as emptyOf, y as factsOf, V as failuresOf, H as intakeOf, U as isTunnelState, f as judge, b as judgeKind, W as kernelCounts, n as kindCellRecordSchema, r as kindCellRecordV1Schema, A as medianInterval, s as medianText, j as middle, x as observationAfter, S as observationEnd, G as outcomesOf, K as podsAt, q as podsOf, c as predictionText, C as propagationOf, w as propagationSource, M as quantileOf, i as readRecord, J as recordsOf, T as referenceOf, Y as requestsOf, X as restartOf, Z as restartsAt, E as reusesReset, Q as rulesOf, _t as seconds, vt as secondsOf, yt as showBand, $ as signalOf, et as signalsOf, tt as sliceDropOf, nt as sliceEmptyOf, rt as slicesOf, it as socketsOf, at as stoppingOf, l as timeSpread, ot as transitionOf, st as tunnelsOf, D as unitOf, O as updatesOf, p as upgrade, m as upgradeCell, ct as ztunnelPodsOf, lt as ztunnelReplaced } from "./judge-
|
|
1
|
+
export { ut as CLIENT, u as FORMAT, d as FORMAT_V1, N as HBONE_PORT, dt as HEALTH_PORT, h as KIND_NOISE, g as LAST_SLACK, ft as LOG_LABEL, pt as PORT, mt as REDIS, k as RELATIVE, ht as SERVER, t as STAGES, P as TCP_STATE, F as backendsAt, gt as bandOf, I as byClients, L as byNode, a as calibrationOf, o as cellsTable, _ as checkRun, R as deletionOf, v as deletionsOf, z as dropOf, B as emptyOf, y as factsOf, V as failuresOf, H as intakeOf, U as isTunnelState, f as judge, b as judgeKind, W as kernelCounts, n as kindCellRecordSchema, r as kindCellRecordV1Schema, A as medianInterval, s as medianText, j as middle, x as observationAfter, S as observationEnd, G as outcomesOf, K as podsAt, q as podsOf, c as predictionText, C as propagationOf, w as propagationSource, M as quantileOf, i as readRecord, J as recordsOf, T as referenceOf, Y as requestsOf, X as restartOf, Z as restartsAt, E as reusesReset, Q as rulesOf, _t as seconds, vt as secondsOf, yt as showBand, $ as signalOf, et as signalsOf, tt as sliceDropOf, nt as sliceEmptyOf, rt as slicesOf, it as socketsOf, at as stoppingOf, l as timeSpread, ot as transitionOf, st as tunnelsOf, D as unitOf, O as updatesOf, p as upgrade, m as upgradeCell, ct as ztunnelPodsOf, lt as ztunnelReplaced } from "./judge-Bb4ebE9V.js";
|
package/image/app.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// service: the lab has no transport of its own.
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import http from "node:http";
|
|
9
|
+
import http2 from "node:http2";
|
|
9
10
|
import { handle, httpTransport, pathOf, probeServer } from "@mark1russell7/service/http";
|
|
10
11
|
import { redisClient } from "@mark1russell7/service/redis";
|
|
11
12
|
import { CONTROLLER, ROUTE, compose } from "./compose.ts";
|
|
@@ -53,7 +54,8 @@ const redis =
|
|
|
53
54
|
{ clock, record },
|
|
54
55
|
);
|
|
55
56
|
|
|
56
|
-
// the flag file fails the check of the liveness restart: the agent writes it in the root of the container
|
|
57
|
+
// the flag file fails the check of the liveness restart: the agent writes it in the root of the container.
|
|
58
|
+
// With failsRoute of the config (cannot-serve), the flag also fails the route
|
|
57
59
|
const FLAG = process.env.LAB_FLAG ?? "/tmp/lab-fail";
|
|
58
60
|
const flag = () => (existsSync(FLAG) ? "error" : "ok");
|
|
59
61
|
|
|
@@ -65,10 +67,14 @@ const KNOBS = { server_idle_close: s(6), server_idle_sweep_interval: s(1), serve
|
|
|
65
67
|
|
|
66
68
|
// the server transport: the transport gets the lifecycle at its start, thus a request before the end
|
|
67
69
|
// of the startup gets the refusal of the service (503, retryable), and Connection: close while the
|
|
68
|
-
// service is not ready
|
|
69
|
-
|
|
70
|
+
// service is not ready. With the protocol h2c of the declaration, the edge is a server of HTTP/2
|
|
71
|
+
// without TLS, and the close of a connection is a GOAWAY on its session (the candidate C of S2)
|
|
72
|
+
const server = declaration.protocol === "h2c" ? http2.createServer() : http.createServer();
|
|
70
73
|
server.on("listening", () => say({ ev: "app", what: "listening", port: config.port }));
|
|
71
|
-
|
|
74
|
+
// lameDuck false of the declaration keeps each connection open while the service is not ready: then a
|
|
75
|
+
// caller learns only from the endpoints (the candidate A of S2). The default is the lame duck
|
|
76
|
+
const edge = declaration.lameDuck === false ? { keepAlive: () => true } : {};
|
|
77
|
+
const transport = httpTransport(server, KNOBS, { listen: { port: config.port }, clock, ...edge });
|
|
72
78
|
// each request of the port is the one route. A failure of the route answers 503, as the lab always
|
|
73
79
|
// did: the T07 and T18 cells count the 503s, and T18 predicts them while Redis is down. Each 503 gets
|
|
74
80
|
// Retry-After from the rule of the transport
|
|
@@ -77,6 +83,7 @@ transport.mount(
|
|
|
77
83
|
CONTROLLER,
|
|
78
84
|
() => ROUTE,
|
|
79
85
|
async (_req, signal) => {
|
|
86
|
+
if (config.failsRoute === true && existsSync(FLAG)) throw new Error("the replica cannot serve");
|
|
80
87
|
if (redis !== undefined) {
|
|
81
88
|
const sent = await redis.send("GET", ["lab"], { signal });
|
|
82
89
|
if (sent.outcome !== "ok") throw new Error(`redis ${sent.outcome}: ${sent.error}`);
|
package/image/client.mjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// A client of a kind cell: an open loop of requests to the ClusterIP of the service, each on a new
|
|
2
|
-
// connection, thus kube-proxy picks a backend for each one.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// connection, thus kube-proxy picks a backend for each one. With CONNECTIONS, the client keeps that
|
|
3
|
+
// count of keep-alive connections, and each request goes on the next one in turn. kube-proxy picks a
|
|
4
|
+
// backend only for a new connection. SIGHUP opens new connections. Each request is one JSON line with
|
|
5
|
+
// its start on CLOCK_MONOTONIC, its duration, its outcome (ok, 503, refused, reset, timeout,
|
|
6
|
+
// unreachable, or another error) and its local port (`lp`). With PROTOCOL h2c, each connection is a
|
|
7
|
+
// session of HTTP/2, and each request is a stream on the next session in turn.
|
|
5
8
|
import http from "node:http";
|
|
9
|
+
import http2 from "node:http2";
|
|
10
|
+
import net from "node:net";
|
|
6
11
|
|
|
7
12
|
const [host, port] = (process.env.TARGET ?? "").split(":");
|
|
8
13
|
const every = Number(process.env.EVERY_MS ?? 20);
|
|
@@ -10,6 +15,50 @@ const timeout = Number(process.env.TIMEOUT_MS ?? 1000);
|
|
|
10
15
|
const pod = process.env.POD_NAME ?? "client";
|
|
11
16
|
const node = process.env.NODE_NAME ?? "";
|
|
12
17
|
const now = () => Number(process.hrtime.bigint()) / 1e6;
|
|
18
|
+
const h2 = process.env.PROTOCOL === "h2c";
|
|
19
|
+
// a client of HTTP/2 needs at least one session
|
|
20
|
+
const connections = process.env.CONNECTIONS === undefined ? (h2 ? 1 : 0) : Number(process.env.CONNECTIONS);
|
|
21
|
+
|
|
22
|
+
// each agent holds one keep-alive connection. A `Connection: close` of the server ends it after its
|
|
23
|
+
// answer, and the next request on the agent opens a new connection
|
|
24
|
+
const agentsOf = () => Array.from({ length: connections }, () => new http.Agent({ keepAlive: true, maxSockets: 1 }));
|
|
25
|
+
let agents = h2 ? [] : agentsOf();
|
|
26
|
+
let turn = 0;
|
|
27
|
+
|
|
28
|
+
// each slot of HTTP/2 holds one session on its own socket, thus the client knows its local port. A
|
|
29
|
+
// GOAWAY, a close or an error of the session makes the slot open a new session at its next stream.
|
|
30
|
+
// kube-proxy picks a backend for that new connection. The old session ends the streams that it has
|
|
31
|
+
const sessionOf = () => {
|
|
32
|
+
const socket = net.connect({ host, port: Number(port) });
|
|
33
|
+
const session = http2.connect(`http://${host}:${port}`, { createConnection: () => socket });
|
|
34
|
+
const held = { session, socket, usable: true };
|
|
35
|
+
const drop = () => {
|
|
36
|
+
held.usable = false;
|
|
37
|
+
};
|
|
38
|
+
session.on("goaway", drop);
|
|
39
|
+
session.on("close", drop);
|
|
40
|
+
session.on("error", drop);
|
|
41
|
+
return held;
|
|
42
|
+
};
|
|
43
|
+
let slots = h2 ? Array.from({ length: connections }, sessionOf) : [];
|
|
44
|
+
const slotAt = (at) => {
|
|
45
|
+
if (!slots[at].usable) slots[at] = sessionOf();
|
|
46
|
+
return slots[at];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// SIGHUP: new agents or sessions for the next requests. The old ones go after the timeout, thus each
|
|
50
|
+
// request on them ends first
|
|
51
|
+
process.on("SIGHUP", () => {
|
|
52
|
+
const old = agents;
|
|
53
|
+
const oldSlots = slots;
|
|
54
|
+
if (h2) slots = Array.from({ length: connections }, sessionOf);
|
|
55
|
+
else agents = agentsOf();
|
|
56
|
+
say({ ev: "client", what: "reconnect", t: now() });
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
for (const agent of old) agent.destroy();
|
|
59
|
+
for (const held of oldSlots) held.session.destroy();
|
|
60
|
+
}, timeout + 100);
|
|
61
|
+
});
|
|
13
62
|
|
|
14
63
|
let pending = [];
|
|
15
64
|
const flush = () => {
|
|
@@ -37,6 +86,8 @@ const portOf = (socket) => {
|
|
|
37
86
|
return typeof out.port === "number" ? out.port : null;
|
|
38
87
|
};
|
|
39
88
|
|
|
89
|
+
const outcomeOfStatus = (status) => (status === 200 ? "ok" : status === 503 ? "503" : `status-${status}`);
|
|
90
|
+
|
|
40
91
|
const fire = () => {
|
|
41
92
|
const t = now();
|
|
42
93
|
let done = false;
|
|
@@ -51,13 +102,17 @@ const fire = () => {
|
|
|
51
102
|
lp ??= portOf(socket);
|
|
52
103
|
say({ ev: "req", t, d: Math.round((now() - t) * 1000) / 1000, o, ...extra, lp });
|
|
53
104
|
};
|
|
54
|
-
const
|
|
105
|
+
const options =
|
|
106
|
+
connections === 0
|
|
107
|
+
? { agent: false, headers: { connection: "close" } }
|
|
108
|
+
: { agent: agents[turn++ % connections] };
|
|
109
|
+
const req = http.request({ host, port: Number(port), path: "/work", ...options }, (res) => {
|
|
55
110
|
let body = "";
|
|
56
111
|
res.setEncoding("utf8");
|
|
57
112
|
res.on("data", (chunk) => {
|
|
58
113
|
body += chunk;
|
|
59
114
|
});
|
|
60
|
-
res.on("end", () => finish(res.statusCode
|
|
115
|
+
res.on("end", () => finish(outcomeOfStatus(res.statusCode), { b: body.slice(0, 64) }));
|
|
61
116
|
res.on("error", (error) => finish(outcomeOf(error), { e: error.code ?? String(error) }));
|
|
62
117
|
});
|
|
63
118
|
const timer = setTimeout(() => {
|
|
@@ -73,12 +128,95 @@ const fire = () => {
|
|
|
73
128
|
// connect() comes in the tick before this event, thus the handle has the port here, a refused
|
|
74
129
|
// connection's too: in 200 refusals, each port was here and none at setImmediate
|
|
75
130
|
lp = portOf(opened);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
131
|
+
// a kept connection is already connected: a listener on it does not fire, and stays
|
|
132
|
+
if (opened.connecting)
|
|
133
|
+
opened.once("connect", () => {
|
|
134
|
+
lp ??= portOf(opened);
|
|
135
|
+
});
|
|
79
136
|
});
|
|
80
137
|
req.end();
|
|
81
138
|
};
|
|
82
139
|
|
|
83
|
-
|
|
84
|
-
|
|
140
|
+
// a stream that the server did not take: REFUSED_STREAM, a stream above the last one of a GOAWAY, or
|
|
141
|
+
// a stream on a session that closed before it went. RFC 9113 (section 8.7) lets the client send it
|
|
142
|
+
// again, and the client does that once, on a new session
|
|
143
|
+
const REFUSED = http2.constants.NGHTTP2_REFUSED_STREAM;
|
|
144
|
+
const untaken = (error, stream) =>
|
|
145
|
+
stream?.rstCode === REFUSED ||
|
|
146
|
+
/REFUSED_STREAM/.test(String(error?.message ?? "")) ||
|
|
147
|
+
error?.code === "ERR_HTTP2_GOAWAY_SESSION" ||
|
|
148
|
+
error?.code === "ERR_HTTP2_INVALID_SESSION";
|
|
149
|
+
|
|
150
|
+
// one request of HTTP/2: a stream on the next session in turn. Each stream writes the same line as a
|
|
151
|
+
// request of HTTP/1.1, and `r` tells that the client sent it again
|
|
152
|
+
const fireH2 = () => {
|
|
153
|
+
const t = now();
|
|
154
|
+
const at = turn++ % connections;
|
|
155
|
+
let done = false;
|
|
156
|
+
let lp = null;
|
|
157
|
+
let held = null;
|
|
158
|
+
let stream = null;
|
|
159
|
+
let again = false;
|
|
160
|
+
const finish = (o, extra = {}) => {
|
|
161
|
+
if (done) return;
|
|
162
|
+
done = true;
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
lp ??= portOf(held?.socket);
|
|
165
|
+
say({ ev: "req", t, d: Math.round((now() - t) * 1000) / 1000, o, ...extra, lp, ...(again ? { r: 1 } : {}) });
|
|
166
|
+
};
|
|
167
|
+
const failed = (error) => {
|
|
168
|
+
if (done) return;
|
|
169
|
+
if (!again && untaken(error, stream)) {
|
|
170
|
+
again = true;
|
|
171
|
+
held.usable = false;
|
|
172
|
+
send();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
finish(outcomeOf(error), { e: error?.code ?? String(error) });
|
|
176
|
+
};
|
|
177
|
+
const send = () => {
|
|
178
|
+
held = slotAt(at);
|
|
179
|
+
lp = portOf(held.socket);
|
|
180
|
+
if (held.socket.connecting)
|
|
181
|
+
held.socket.once("connect", () => {
|
|
182
|
+
lp ??= portOf(held.socket);
|
|
183
|
+
});
|
|
184
|
+
try {
|
|
185
|
+
stream = held.session.request({ ":path": "/work" });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
failed(error);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const mine = stream;
|
|
191
|
+
let status = 0;
|
|
192
|
+
let body = "";
|
|
193
|
+
mine.setEncoding("utf8");
|
|
194
|
+
mine.on("response", (headers) => {
|
|
195
|
+
status = Number(headers[":status"]);
|
|
196
|
+
});
|
|
197
|
+
mine.on("data", (chunk) => {
|
|
198
|
+
body += chunk;
|
|
199
|
+
});
|
|
200
|
+
mine.on("end", () => {
|
|
201
|
+
if (mine === stream && status !== 0) finish(outcomeOfStatus(status), { b: body.slice(0, 64) });
|
|
202
|
+
});
|
|
203
|
+
mine.on("error", (error) => {
|
|
204
|
+
if (mine === stream) failed(error);
|
|
205
|
+
});
|
|
206
|
+
mine.on("close", () => {
|
|
207
|
+
if (mine !== stream || done) return;
|
|
208
|
+
if (status !== 0) finish(outcomeOfStatus(status), { b: body.slice(0, 64) });
|
|
209
|
+
else failed(Object.assign(new Error(`the stream closed with code ${mine.rstCode}`), { code: mine.rstCode === REFUSED ? "REFUSED_STREAM" : "ECONNRESET" }));
|
|
210
|
+
});
|
|
211
|
+
mine.end();
|
|
212
|
+
};
|
|
213
|
+
const timer = setTimeout(() => {
|
|
214
|
+
lp ??= portOf(held?.socket);
|
|
215
|
+
stream?.close(http2.constants.NGHTTP2_CANCEL);
|
|
216
|
+
finish("timeout", { e: "LAB_TIMEOUT" });
|
|
217
|
+
}, timeout);
|
|
218
|
+
send();
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
say({ ev: "client", what: "start", t: now(), target: `${host}:${port}`, every, timeout, connections, protocol: h2 ? "h2c" : "http/1.1" });
|
|
222
|
+
setInterval(h2 ? fireH2 : fire, every);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/kind",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.1",
|
|
5
5
|
"description": "Kubernetes in Docker as a lab for connection timers: a kind cluster from a scenario, faults in the namespaces of its pods, observations on the one monotonic clock of the VM, and predictions from the ordering law. Its judge entry reads and judges a raw record without Docker.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"kubernetes",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@mark1russell7/harness": "^0.12.4",
|
|
69
69
|
"@mark1russell7/quantity": "^0.2.0",
|
|
70
|
-
"@mark1russell7/service": "^0.5.
|
|
70
|
+
"@mark1russell7/service": "^0.5.19",
|
|
71
71
|
"testcontainers": "12.2.0"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|