@mark1russell7/kind 0.1.7 → 0.2.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 CHANGED
@@ -89,6 +89,20 @@ Each pod of a cluster shares the kernel of the Docker VM. Thus each stamp of the
89
89
 
90
90
  Before each run, the lab deletes the conntrack entries in SYN_SENT to the ClusterIP on each node (`forgetHandshakes`). A note of the run gives each count. Such an entry is a SYN with no answer, and it keeps its NAT for 120 s. A new SYN from the same port goes where the first one went, and it gets no answer either. The clients send requests between the runs too. Thus the restore of one run left such entries, and a client that took the port again in the next run timed out (T07, 2026-10-17).
91
91
 
92
+ ## The stacks
93
+
94
+ From 0.2.0, a cell names its stack (`stack`): its server and its clients. A cell without a stack has the service.
95
+
96
+ - **`service`** is the service of this repository with its declaration, and the HTTP clients. Each request opens a new connection, thus kube-proxy picks a backend for each request.
97
+ - **`grpc`** is the server of grpc-go v1.84.0 from the image of the harness, and the gRPC clients of the lab image. A client holds one channel. Thus kube-proxy picks a backend only when grpc-js connects again: after a GOAWAY of the server, or after the end of the socket.
98
+ - The probes are the gRPC probes of the kubelet on the port of the server.
99
+ - The lines of grpc-go name their pod and stamp in milliseconds (`--name`, `--clock ms`). Its line `stopping` gives the intake and the SIGTERM of a pod (`stoppingOf`): then GracefulStop closes the listener and sends GOAWAY.
100
+ - The clients start after the server is ready. Each failed connect of a channel makes the backoff of grpc-js longer, up to 120 s.
101
+ - The target of a run is the replica that answers the most clients. A channel stays on its replica, and the channels go to the replica that stays after a run. Thus a target by name has no client in most runs.
102
+ - The cell has no declaration (`declaration` is null), thus `describeCell` refuses it. The server grpc-go has no seam for `liveness-restart`, thus its manifest refuses that trigger.
103
+
104
+ A declaration that can be null changes the type `KindCell`. That is a breaking change before 1.0.0, thus the version is 0.2.0.
105
+
92
106
  ## The image
93
107
 
94
108
  The lab image (`image/`) holds the service of a cell on `@mark1russell7/service`, its clients, the agent and the watcher, on Node 24.21.0. Its base is pinned by digest, and `npm-lock.json` pins the packages from npm. `stageLabImage()` stages its context, and `labImage()` tags the image by its content.
@@ -101,6 +115,7 @@ The lab image (`image/`) holds the service of a cell on `@mark1russell7/service`
101
115
  - **The knobs of the listener.** `server_idle_close` is 6 s, the real idle close of Node 24.21: `keepAliveTimeout` of 5 s and its buffer of 1 s. The sweep of idle connections comes each second after the stop of the intake.
102
116
  - **The seams** of the service are a flag file that fails its liveness check, and SIGUSR2. SIGUSR2 blocks its event loop for the time in a file.
103
117
  - **The clients** (`client.mjs`) send requests on new connections to the ClusterIP. Each request gives one line with its outcome: ok, 503, refused, reset, timeout or unreachable. From 0.1.3, each line also gives the local port of its connection (`lp`), a refused one's too. A conntrack entry is per port. Thus the requests on the port of a failure show the connection before it, and the pod that it went to.
118
+ - **The gRPC clients** (`client-grpc.mjs`) hold one channel of grpc-js 1.14.6 to the ClusterIP, and send a call of `grpc.health.v1.Health/Check` each interval on it. Each call gives the line of a request of `client.mjs`, with the pod that answered (the header `x-pod`). A call that fails at once names the errno of the last connect: refused, reset or unreachable. A call of grpc-js has no deadline at its defaults. Thus a call with no answer in the timeout is a timeout, and the client cancels it.
104
119
 
105
120
  ## The triggers
106
121
 
@@ -111,7 +126,7 @@ The lab image (`image/`) holds the service of a cell on `@mark1russell7/service`
111
126
  | `pod-deletion` | `kubectl delete pod` of the first replica |
112
127
  | `rolling-update` | a new value of an environment variable of the Deployment |
113
128
  | `node-drain` | `kubectl drain` of the node of the first replica, then `uncordon` |
114
- | `scale-in` | one replica less, then back |
129
+ | `scale-in` | one replica less, then back. For the stack `grpc`, the first replica first gets a deletion cost of -1, thus its ReplicaSet removes it |
115
130
  | `crash` | SIGKILL of the process of the service, from the node |
116
131
  | `liveness-restart` | the flag file that fails the liveness check of the first replica |
117
132
  | `freeze-process` | SIGSTOP and SIGCONT: of kube-proxy on one node across a deletion (T07), or of Redis (T18) |
@@ -137,6 +152,8 @@ The plan's `event:clock-jump` has no trigger. A step of CLOCK_REALTIME in the VM
137
152
 
138
153
  `verdict.ts` judges each run, and each cell on its counted runs. Each time lands in its band ± 100 ms: the poll of the rules and the delay of the watcher. A median in a band gets the test of the harness: its order-statistic interval in the band, made 5 % wider.
139
154
 
155
+ From 0.1.8, a run can agree with the law but be past its calibrated band (D-EXTEND). A request failed after the window, but before the rules of its node dropped the target. The check of that run tells it (`tail`), and its cell is `extend`. Such a cell does not hold. A request after the drop is still late, thus its run fails the cell.
156
+
140
157
  ## Tests
141
158
 
142
159
  `pnpm --filter @mark1russell7/kind test` starts the tests of the configuration, the tools, the parser of the rules and the fault mapping. It also starts the tests of the predictions, the verdict and the judge of the real records. They do not need Docker.
package/build/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as RulesLine, $t as cidrOverlap, A as TriggerV1, At as secondsOf, B as RunFacts, Bt as Reference, C as KindCellResult, Ct as HEALTH_PORT, D as PredictionV1, Dt as SERVER, E as LoadSample, Et as REDIS, F as KindDecision, Ft as KindRun, G as referenceOf, Gt as TriggerKind, H as factsOf, Ht as ResetReuse, I as KindVerdict, It as KubeletProbe, J as Endpoint, Jt as KindConfig, K as reusesReset, Kt as Workload, L as LAST_SLACK, Lt as PodSettings, M as upgrade, Mt as Band, N as upgradeCell, Nt as Declaration, O as RunCheckV1, Ot as bandOf, P as KIND_NOISE, Pt as KindCell, Q as Request, Qt as NodeImage, R as Propagated, Rt as Prediction, S as KindCellRecordV1, St as CLIENT, T as KindVerdictV1, Tt as PORT, U as judgeKind, Ut as Template, V as checkRun, Vt as Replica, W as propagationOf, Wt as Trigger, X as PodLine, Xt as KubeProxyMode, Y as Line, Yt as KindScenario, Z as RecordLine, Zt as NODE_IMAGES, _ as ClusterManifest, _t as signalsOf, a as STAGES, an as scenario, at as emptyOf, b as FORMAT_V1, bt as slicesOf, c as readRecord, ct as kernelCounts, d as calibrationOf, dt as podsOf, en as cidrRange, et as SliceLine, f as cellsTable, ft as recordsOf, g as BandV1, gt as signalOf, h as timeSpread, ht as rulesOf, i as quantileOf, in as nodeNames, it as dropOf, j as judge, jt as showBand, k as RunFactsV1, kt as seconds, l as Judged, lt as outcomesOf, m as predictionText, mt as restartsAt, n as medianInterval, nn as kindConfig, nt as byNode, o as kindCellRecordSchema, on as scenarioProblems, ot as failuresOf, p as medianText, pt as requestsOf, q as unitOf, qt as KUBE_PROXY_MODES, r as middle, rn as kubeProxyPatch, rt as deletionOf, s as kindCellRecordV1Schema, st as intakeOf, t as RELATIVE, tn as goDuration, tt as backendsAt, u as TimeSpread, ut as podsAt, v as EnvironmentManifest, vt as sliceDropOf, w as KindCellV1, wt as LOG_LABEL, x as KindCellRecord, xt as transitionOf, y as FORMAT, yt as sliceEmptyOf, z as RunCheck, zt as RedisDeclaration } from "./judge-BkaQ5zGz.js";
1
+ import { $ as RulesLine, $t as KubeProxyMode, A as TriggerV1, At as bandOf, B as RunFacts, Bt as Prediction, C as KindCellResult, Ct as transitionOf, D as PredictionV1, Dt as PORT, E as LoadSample, Et as LOG_LABEL, F as KindDecision, Ft as Declaration, G as referenceOf, Gt as Stack, H as factsOf, Ht as Reference, I as KindVerdict, It as KindCell, J as Endpoint, Jt as TriggerKind, K as reusesReset, Kt as Template, L as LAST_SLACK, Lt as KindRun, M as upgrade, Mt as secondsOf, N as upgradeCell, Nt as showBand, O as RunCheckV1, Ot as REDIS, P as KIND_NOISE, Pt as Band, Q as Request, Qt as KindScenario, R as Propagated, Rt as KubeletProbe, S as KindCellRecordV1, St as stoppingOf, T as KindVerdictV1, Tt as HEALTH_PORT, U as judgeKind, Ut as Replica, V as checkRun, Vt as RedisDeclaration, W as propagationOf, Wt as ResetReuse, X as PodLine, Xt as KUBE_PROXY_MODES, Y as Line, Yt as Workload, Z as RecordLine, Zt as KindConfig, _ as ClusterManifest, _t as signalOf, a as STAGES, an as kindConfig, at as dropOf, b as FORMAT_V1, bt as sliceEmptyOf, c as readRecord, cn as scenario, ct as intakeOf, d as calibrationOf, dt as podsAt, en as NODE_IMAGES, et as SliceLine, f as cellsTable, ft as podsOf, g as BandV1, gt as rulesOf, h as timeSpread, ht as restartsAt, i as quantileOf, in as goDuration, it as deletionOf, j as judge, jt as seconds, k as RunFactsV1, kt as SERVER, l as Judged, ln as scenarioProblems, lt as kernelCounts, m as predictionText, mt as requestsOf, n as medianInterval, nn as cidrOverlap, nt as byClients, o as kindCellRecordSchema, on as kubeProxyPatch, ot as emptyOf, p as medianText, pt as recordsOf, q as unitOf, qt as Trigger, r as middle, rn as cidrRange, rt as byNode, s as kindCellRecordV1Schema, sn as nodeNames, st as failuresOf, t as RELATIVE, tn as NodeImage, tt as backendsAt, u as TimeSpread, ut as outcomesOf, v as EnvironmentManifest, vt as signalsOf, w as KindCellV1, wt as CLIENT, x as KindCellRecord, xt as slicesOf, y as FORMAT, yt as sliceDropOf, z as RunCheck, zt as PodSettings } from "./judge-D5SBTJ-V.js";
2
2
  import { Quantity } from "@mark1russell7/quantity";
3
3
  import { CheckFn, Finding, HealthServer, Poll, Rule, Scenario, ServiceBuilder, ServiceDescription, Stage, Timing, TransportImpl, Verdict } from "@mark1russell7/service";
4
4
  import { RedisClient } from "@mark1russell7/service/redis";
@@ -205,17 +205,37 @@ export declare const openKindLab: (cluster: Cluster, image: string) => Promise<K
205
205
  export declare const localPin: (image: string) => Promise<string>;
206
206
  //#endregion
207
207
  //#region src/manifests.d.ts
208
+ /**
209
+ * This module generates the Kubernetes objects of a cell. They are its namespace, its Redis, the
210
+ * server as a Deployment behind a Service, and one client on each node. The server is the service
211
+ * or grpc-go, by the stack of the cell. Each setting of the pod comes from the cell. The rollout
212
+ * strategy and the probe fields state the defaults of Kubernetes as values.
213
+ */
214
+ /** This function gives the stack of a cell: the service, when the cell names no stack. */
215
+ export declare const stackOf: (cell: Pick<KindCell, "stack">) => Stack;
208
216
  /** This image is Redis 7.4, the stock target of the harness, pinned by digest. */
209
217
  export declare const REDIS_IMAGE = "redis:7.4@sha256:c6eabf748fc7a61dbb5a705c78bcf3d6377b1127a97d0ce965c11c44ba46896f";
210
218
  /** This function gives the namespace of a cell: `lab-` and 10 digits of the sha256 of its name. */
211
219
  export declare const namespaceOf: (cell: Pick<KindCell, "name">) => string;
212
- /** This function gives a probe of the pod: an HTTP GET of the health server, with each field of the cell. */
213
- export declare const probeOf: (path: string, probe: KubeletProbe) => Record<string, unknown>;
220
+ /**
221
+ * This function gives a probe of the pod, with each field of the cell. The probe of the service is an
222
+ * HTTP GET of its health server (`path`). The probe of grpc-go (`path` null) is the gRPC probe of the
223
+ * kubelet. It is a Check of the service "" on the port of the server, and the answer is SERVING until
224
+ * GracefulStop.
225
+ */
226
+ export declare const probeOf: (path: string | null, probe: KubeletProbe) => Record<string, unknown>;
214
227
  /** These are the objects of Redis: one replica behind a Service, with no persistence. */
215
228
  export declare const redisObjects: (namespace: string, image: string) => unknown[];
216
- /** These are the objects of the service: a Service of type ClusterIP, and the Deployment with the settings of the cell. */
229
+ /**
230
+ * These are the objects of the server: a Service of type ClusterIP, and the Deployment with the
231
+ * settings of the cell. The image is the lab image for the service, or the image of grpc-go.
232
+ */
217
233
  export declare const serverObjects: (cell: KindCell, namespace: string, image: string, version?: number) => unknown[];
218
- /** These are the objects of the clients: a DaemonSet with one client on each node, the control plane too. */
234
+ /**
235
+ * These are the objects of the clients: a DaemonSet with one client on each node, the control plane
236
+ * too. The client of the service is `client.mjs`, and the client of grpc-go is `client-grpc.mjs`.
237
+ * Each one is in the lab image, and each one writes the same lines.
238
+ */
219
239
  export declare const clientObjects: (cell: KindCell, namespace: string, image: string, target: string) => unknown[];
220
240
  //#endregion
221
241
  //#region src/predict.d.ts
@@ -395,5 +415,5 @@ export declare const toolPath: (tool: Tool, key?: string, cache?: string) => str
395
415
  */
396
416
  export declare const ensureTool: (tool: Tool, key?: string, cache?: string) => Promise<string>;
397
417
  //#endregion
398
- export { type Band, type BandV1, CLIENT, type Cluster, type ClusterManifest, type Credentials, type Declaration, type DeletionWindow, Endpoint, type EnvironmentManifest, FORMAT, FORMAT_V1, type FaultPlan, HEALTH_PORT, type Judged, KIND_NOISE, KUBE_PROXY_MODES, type KindCell, type KindCellRecord, type KindCellRecordV1, type KindCellResult, type KindCellV1, type KindConfig, type KindDecision, type KindLab, type KindRun, type KindScenario, type KindVerdict, type KindVerdictV1, type KubeProxyMode, type KubeletProbe, LAST_SLACK, LOG_LABEL, Line, type LoadSample, NODE_IMAGES, type NodeImage, PORT, type Parts, PodLine, type PodSettings, type Prediction, type PredictionV1, type Propagated, type Propagation, REDIS, RELATIVE, RecordLine, type RedisDeclaration, type Reference, type Replica, Request, type ResetReuse, RulesLine, type RunCheck, type RunCheckV1, type RunFacts, type RunFactsV1, type RunOptions, SERVER, STAGES, type ServiceRule, SliceLine, type Stamp, type Step, type Target, type Template, type TimeSpread, type Tool, type Trigger, type TriggerKind, type TriggerV1, type Verdicts, type Workload, backendsAt, bandOf, byNode, calibrationOf, cellsTable, checkRun, cidrOverlap, cidrRange, deletionOf, dropOf, emptyOf, factsOf, failuresOf, goDuration, intakeOf, judge, judgeKind, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, kindConfig, kubeProxyPatch, medianInterval, medianText, middle, nodeNames, outcomesOf, podsAt, podsOf, predictionText, propagationOf, quantileOf, readRecord, recordsOf, referenceOf, requestsOf, restartsAt, reusesReset, rulesOf, scenario, scenarioProblems, seconds, secondsOf, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, slicesOf, timeSpread, transitionOf, unitOf, upgrade, upgradeCell };
418
+ export { type Band, type BandV1, CLIENT, type Cluster, type ClusterManifest, type Credentials, type Declaration, type DeletionWindow, Endpoint, type EnvironmentManifest, FORMAT, FORMAT_V1, type FaultPlan, HEALTH_PORT, type Judged, KIND_NOISE, KUBE_PROXY_MODES, type KindCell, type KindCellRecord, type KindCellRecordV1, type KindCellResult, type KindCellV1, type KindConfig, type KindDecision, type KindLab, type KindRun, type KindScenario, type KindVerdict, type KindVerdictV1, type KubeProxyMode, type KubeletProbe, LAST_SLACK, LOG_LABEL, Line, type LoadSample, NODE_IMAGES, type NodeImage, PORT, type Parts, PodLine, type PodSettings, type Prediction, type PredictionV1, type Propagated, type Propagation, REDIS, RELATIVE, RecordLine, type RedisDeclaration, type Reference, type Replica, Request, type ResetReuse, RulesLine, type RunCheck, type RunCheckV1, type RunFacts, type RunFactsV1, type RunOptions, SERVER, STAGES, type ServiceRule, SliceLine, type Stack, type Stamp, type Step, type Target, type Template, type TimeSpread, type Tool, type Trigger, type TriggerKind, type TriggerV1, type Verdicts, type Workload, backendsAt, bandOf, byClients, byNode, calibrationOf, cellsTable, checkRun, cidrOverlap, cidrRange, deletionOf, dropOf, emptyOf, factsOf, failuresOf, goDuration, intakeOf, judge, judgeKind, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, kindConfig, kubeProxyPatch, medianInterval, medianText, middle, nodeNames, outcomesOf, podsAt, podsOf, predictionText, propagationOf, quantileOf, readRecord, recordsOf, referenceOf, requestsOf, restartsAt, reusesReset, rulesOf, scenario, scenarioProblems, seconds, secondsOf, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, slicesOf, stoppingOf, timeSpread, transitionOf, unitOf, upgrade, upgradeCell };
399
419
  //# sourceMappingURL=index.d.ts.map
package/build/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as REDIS, A as deletionOf, B as requestsOf, C as unitOf, D as quantileOf, E as middle, F as kernelCounts, G as sliceDropOf, H as rulesOf, I as outcomesOf, J as transitionOf, K as sliceEmptyOf, L as podsAt, M as emptyOf, N as failuresOf, O as backendsAt, P as intakeOf, Q as PORT, R as podsOf, S as reusesReset, T as medianInterval, U as signalOf, V as restartsAt, W as signalsOf, X as HEALTH_PORT, Y as CLIENT, Z as LOG_LABEL, _ as checkRun, a as calibrationOf, b as propagationOf, c as predictionText, d as FORMAT_V1, et as SERVER, f as judge, g as LAST_SLACK, h as KIND_NOISE, i as readRecord, it as showBand, j as dropOf, k as byNode, l as timeSpread, m as upgradeCell, n as kindCellRecordSchema, nt as seconds, o as cellsTable, p as upgrade, q as slicesOf, r as kindCellRecordV1Schema, rt as secondsOf, s as medianText, t as STAGES, tt as bandOf, u as FORMAT, v as factsOf, w as RELATIVE, x as referenceOf, y as judgeKind, z as recordsOf } from "./judge-C1ssYP7m.js";
1
+ import { $ as LOG_LABEL, A as byNode, B as recordsOf, C as unitOf, D as quantileOf, E as middle, F as intakeOf, G as signalsOf, H as restartsAt, I as kernelCounts, J as slicesOf, K as sliceDropOf, L as outcomesOf, M as dropOf, N as emptyOf, O as backendsAt, P as failuresOf, Q as HEALTH_PORT, R as podsAt, S as reusesReset, T as medianInterval, U as rulesOf, V as requestsOf, W as signalOf, X as transitionOf, Y as stoppingOf, Z as CLIENT, _ as checkRun, a as calibrationOf, at as secondsOf, b as propagationOf, c as predictionText, d as FORMAT_V1, et as PORT, f as judge, g as LAST_SLACK, h as KIND_NOISE, i as readRecord, it as seconds, j as deletionOf, k as byClients, l as timeSpread, m as upgradeCell, n as kindCellRecordSchema, nt as SERVER, o as cellsTable, ot as showBand, p as upgrade, q as sliceEmptyOf, r as kindCellRecordV1Schema, rt as bandOf, s as medianText, t as STAGES, tt as REDIS, u as FORMAT, v as factsOf, w as RELATIVE, x as referenceOf, y as judgeKind, z as podsOf } from "./judge-DlmmZHcI.js";
2
2
  import { valueIn } from "@mark1russell7/quantity";
3
3
  import { execFile, execFileSync } from "node:child_process";
4
4
  import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
@@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url";
9
9
  import { failSpan, lint, service } from "@mark1russell7/service";
10
10
  import { createRequire } from "node:module";
11
11
  import { tmpdir } from "node:os";
12
- import { harnessImage } from "@mark1russell7/harness";
12
+ import { harnessImage, imageOf } from "@mark1russell7/harness";
13
13
  import { redisClient } from "@mark1russell7/service/redis";
14
14
  //#region src/backends.ts
15
15
  const hexIp = (hex) => [
@@ -520,6 +520,8 @@ const POD = "{pod}";
520
520
  const NODE = "{node}";
521
521
  /** This value marks the next version of the Deployment, for a rollout. */
522
522
  const VERSION = "{version}";
523
+ /** This annotation of a pod is its deletion cost for its ReplicaSet: a lower cost goes first at a scale-in. */
524
+ const DELETION_COST = "controller.kubernetes.io/pod-deletion-cost";
523
525
  /** This function gives the fault plan of a cell. */
524
526
  const planOf = (cell) => {
525
527
  const ns = ["-n", "{namespace}"];
@@ -577,7 +579,17 @@ const planOf = (cell) => {
577
579
  }]
578
580
  };
579
581
  case "scale-in": return {
580
- inject: [{
582
+ inject: [...cell.stack === "grpc" ? [{
583
+ do: "kubectl",
584
+ args: [
585
+ ...ns,
586
+ "annotate",
587
+ "pod",
588
+ POD,
589
+ `${DELETION_COST}=-1`,
590
+ "--overwrite"
591
+ ]
592
+ }] : [], {
581
593
  do: "kubectl",
582
594
  args: [
583
595
  ...ns,
@@ -800,6 +812,7 @@ const FILES = [
800
812
  ["npm-lock.json", "image/npm-lock.json"],
801
813
  ["app.mjs", "image/app.mjs"],
802
814
  ["client.mjs", "image/client.mjs"],
815
+ ["client-grpc.mjs", "image/client-grpc.mjs"],
803
816
  ["agent.mjs", "image/agent.mjs"],
804
817
  ["watch.mjs", "image/watch.mjs"],
805
818
  ["compose.ts", "src/compose.ts"],
@@ -1220,6 +1233,14 @@ const localPin = async (image) => {
1220
1233
  };
1221
1234
  //#endregion
1222
1235
  //#region src/manifests.ts
1236
+ /**
1237
+ * This module generates the Kubernetes objects of a cell. They are its namespace, its Redis, the
1238
+ * server as a Deployment behind a Service, and one client on each node. The server is the service
1239
+ * or grpc-go, by the stack of the cell. Each setting of the pod comes from the cell. The rollout
1240
+ * strategy and the probe fields state the defaults of Kubernetes as values.
1241
+ */
1242
+ /** This function gives the stack of a cell: the service, when the cell names no stack. */
1243
+ const stackOf = (cell) => cell.stack ?? "service";
1223
1244
  /** This image is Redis 7.4, the stock target of the harness, pinned by digest. */
1224
1245
  const REDIS_IMAGE = "redis:7.4@sha256:c6eabf748fc7a61dbb5a705c78bcf3d6377b1127a97d0ce965c11c44ba46896f";
1225
1246
  const whole = (amount, what) => {
@@ -1229,18 +1250,28 @@ const whole = (amount, what) => {
1229
1250
  };
1230
1251
  /** This function gives the namespace of a cell: `lab-` and 10 digits of the sha256 of its name. */
1231
1252
  const namespaceOf = (cell) => `lab-${createHash("sha256").update(cell.name).digest("hex").slice(0, 10)}`;
1232
- /** This function gives a probe of the pod: an HTTP GET of the health server, with each field of the cell. */
1253
+ /**
1254
+ * This function gives a probe of the pod, with each field of the cell. The probe of the service is an
1255
+ * HTTP GET of its health server (`path`). The probe of grpc-go (`path` null) is the gRPC probe of the
1256
+ * kubelet. It is a Check of the service "" on the port of the server, and the answer is SERVING until
1257
+ * GracefulStop.
1258
+ */
1233
1259
  const probeOf = (path, probe) => ({
1234
- httpGet: {
1260
+ ...path === null ? { grpc: { port: PORT } } : { httpGet: {
1235
1261
  path,
1236
1262
  port: HEALTH_PORT
1237
- },
1263
+ } },
1238
1264
  initialDelaySeconds: 0,
1239
- periodSeconds: whole(probe.period, `the period of ${path}`),
1240
- timeoutSeconds: whole(probe.timeout, `the timeout of ${path}`),
1265
+ periodSeconds: whole(probe.period, `the period of ${path ?? "the gRPC probe"}`),
1266
+ timeoutSeconds: whole(probe.timeout, `the timeout of ${path ?? "the gRPC probe"}`),
1241
1267
  failureThreshold: probe.threshold,
1242
1268
  successThreshold: 1
1243
1269
  });
1270
+ /** This variable is the name of the pod, from the API: the service and grpc-go put it in their lines. */
1271
+ const POD_ENV = {
1272
+ name: "POD_NAME",
1273
+ valueFrom: { fieldRef: { fieldPath: "metadata.name" } }
1274
+ };
1244
1275
  /** This function gives the environment of the app: its config as JSON, and the name of its pod. */
1245
1276
  const appEnv = (cell) => [{
1246
1277
  name: "CONFIG",
@@ -1250,10 +1281,44 @@ const appEnv = (cell) => [{
1250
1281
  healthPort: HEALTH_PORT,
1251
1282
  declaration: cell.declaration
1252
1283
  })
1253
- }, {
1254
- name: "POD_NAME",
1255
- valueFrom: { fieldRef: { fieldPath: "metadata.name" } }
1256
- }];
1284
+ }, POD_ENV];
1285
+ /**
1286
+ * This function gives the container of the server of a cell. The service runs `app.mjs` of the lab
1287
+ * image with its declaration. The server grpc-go runs from the image of the harness. Its lines name
1288
+ * the pod and stamp in milliseconds. Its replies name the pod in the header `x-pod`. Kubernetes
1289
+ * expands `$(POD_NAME)` in the command.
1290
+ */
1291
+ const serverContainer = (cell, version) => {
1292
+ const env = {
1293
+ name: "LAB_VERSION",
1294
+ value: String(version)
1295
+ };
1296
+ if (stackOf(cell) === "service") return {
1297
+ command: ["node", "app.mjs"],
1298
+ env: [...appEnv(cell), env],
1299
+ ports: [{ containerPort: PORT }, { containerPort: HEALTH_PORT }],
1300
+ ...cell.pod.startup === null ? {} : { startupProbe: probeOf("/startup", cell.pod.startup) },
1301
+ readinessProbe: probeOf("/readiness", cell.pod.readiness),
1302
+ livenessProbe: probeOf("/liveness", cell.pod.liveness)
1303
+ };
1304
+ if (cell.trigger.kind === "liveness-restart") throw new Error(`${cell.name}: grpc-go has no seam for liveness-restart`);
1305
+ return {
1306
+ command: [
1307
+ "grpc-go-server",
1308
+ "--port",
1309
+ String(PORT),
1310
+ "--name",
1311
+ "$(POD_NAME)",
1312
+ "--clock",
1313
+ "ms"
1314
+ ],
1315
+ env: [POD_ENV, env],
1316
+ ports: [{ containerPort: PORT }],
1317
+ ...cell.pod.startup === null ? {} : { startupProbe: probeOf(null, cell.pod.startup) },
1318
+ readinessProbe: probeOf(null, cell.pod.readiness),
1319
+ livenessProbe: probeOf(null, cell.pod.liveness)
1320
+ };
1321
+ };
1257
1322
  /** These are the objects of Redis: one replica behind a Service, with no persistence. */
1258
1323
  const redisObjects = (namespace, image) => [{
1259
1324
  apiVersion: "apps/v1",
@@ -1308,7 +1373,10 @@ const redisObjects = (namespace, image) => [{
1308
1373
  }]
1309
1374
  }
1310
1375
  }];
1311
- /** These are the objects of the service: a Service of type ClusterIP, and the Deployment with the settings of the cell. */
1376
+ /**
1377
+ * These are the objects of the server: a Service of type ClusterIP, and the Deployment with the
1378
+ * settings of the cell. The image is the lab image for the service, or the image of grpc-go.
1379
+ */
1312
1380
  const serverObjects = (cell, namespace, image, version = 1) => {
1313
1381
  const preStop = whole(cell.pod.preStop, "the preStop sleep");
1314
1382
  return [{
@@ -1321,7 +1389,7 @@ const serverObjects = (cell, namespace, image, version = 1) => {
1321
1389
  spec: {
1322
1390
  selector: { app: "lab" },
1323
1391
  ports: [{
1324
- name: "http",
1392
+ name: stackOf(cell) === "grpc" ? "grpc" : "http",
1325
1393
  port: 80,
1326
1394
  targetPort: PORT
1327
1395
  }]
@@ -1364,15 +1432,7 @@ const serverObjects = (cell, namespace, image, version = 1) => {
1364
1432
  name: "app",
1365
1433
  image,
1366
1434
  imagePullPolicy: "Never",
1367
- command: ["node", "app.mjs"],
1368
- env: [...appEnv(cell), {
1369
- name: "LAB_VERSION",
1370
- value: String(version)
1371
- }],
1372
- ports: [{ containerPort: PORT }, { containerPort: HEALTH_PORT }],
1373
- ...cell.pod.startup === null ? {} : { startupProbe: probeOf("/startup", cell.pod.startup) },
1374
- readinessProbe: probeOf("/readiness", cell.pod.readiness),
1375
- livenessProbe: probeOf("/liveness", cell.pod.liveness),
1435
+ ...serverContainer(cell, version),
1376
1436
  ...preStop === 0 ? {} : { lifecycle: { preStop: { sleep: { seconds: preStop } } } }
1377
1437
  }]
1378
1438
  }
@@ -1380,7 +1440,11 @@ const serverObjects = (cell, namespace, image, version = 1) => {
1380
1440
  }
1381
1441
  }];
1382
1442
  };
1383
- /** These are the objects of the clients: a DaemonSet with one client on each node, the control plane too. */
1443
+ /**
1444
+ * These are the objects of the clients: a DaemonSet with one client on each node, the control plane
1445
+ * too. The client of the service is `client.mjs`, and the client of grpc-go is `client-grpc.mjs`.
1446
+ * Each one is in the lab image, and each one writes the same lines.
1447
+ */
1384
1448
  const clientObjects = (cell, namespace, image, target) => [{
1385
1449
  apiVersion: "apps/v1",
1386
1450
  kind: "DaemonSet",
@@ -1407,7 +1471,7 @@ const clientObjects = (cell, namespace, image, target) => [{
1407
1471
  name: CLIENT,
1408
1472
  image,
1409
1473
  imagePullPolicy: "Never",
1410
- command: ["node", "client.mjs"],
1474
+ command: ["node", stackOf(cell) === "grpc" ? "client-grpc.mjs" : "client.mjs"],
1411
1475
  env: [
1412
1476
  {
1413
1477
  name: "TARGET",
@@ -1694,6 +1758,7 @@ const lintCell = (description, cell, propagation) => lint(description, scenarioO
1694
1758
  const verdictsOf = (findings) => Object.fromEntries(findings.map((finding) => [finding.rule, finding.verdict]));
1695
1759
  /** This function gives the description of the service of a cell, as the app of the image composes it. Nothing connects. */
1696
1760
  const describeCell = (cell) => {
1761
+ if (cell.declaration === null) throw new Error(`${cell.name}: the server is not the service, thus the cell has no declaration to describe`);
1697
1762
  const redis = cell.declaration.redis;
1698
1763
  const client = redis === null ? void 0 : redisClient("redis", {
1699
1764
  host: "redis",
@@ -1713,12 +1778,16 @@ const RULES_EVERY = {
1713
1778
  };
1714
1779
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1715
1780
  const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1781
+ /** This function tells if a cell has Redis: only a declaration of the service can have it. */
1782
+ const hasRedis = (cell) => cell.declaration !== null && cell.declaration.redis !== null;
1716
1783
  /** This tracker keeps the latest state of the cell from the lines: each pod, each slice, the backends of each node and each client. */
1717
1784
  var Tracker = class {
1718
1785
  pods = /* @__PURE__ */ new Map();
1719
1786
  slices = /* @__PURE__ */ new Map();
1720
1787
  backends = /* @__PURE__ */ new Map();
1721
1788
  clients = /* @__PURE__ */ new Map();
1789
+ /** This map is the backend of the last answer of each client, by client. */
1790
+ answers = /* @__PURE__ */ new Map();
1722
1791
  last = 0;
1723
1792
  see = (line) => {
1724
1793
  if (typeof line.t === "number" && line.t > this.last && line.ev === "req") this.last = line.t;
@@ -1740,6 +1809,7 @@ var Tracker = class {
1740
1809
  if (request.o === "ok") client.ok = Math.max(client.ok, request.t);
1741
1810
  else client.failed = Math.max(client.failed, request.t);
1742
1811
  this.clients.set(request.p, client);
1812
+ if (request.o === "ok" && request.b !== void 0) this.answers.set(request.p, request.b);
1743
1813
  }
1744
1814
  };
1745
1815
  servers() {
@@ -1754,7 +1824,7 @@ const unsteady = (ctx) => {
1754
1824
  const busy = servers.find((pod) => pod.del || !pod.ready);
1755
1825
  if (busy !== void 0) return `${busy.name} is not ready`;
1756
1826
  if (new Set(servers.map((pod) => pod.labels["pod-template-hash"])).size > 1) return "two versions of the replicas";
1757
- if (cell.declaration.redis !== null) {
1827
+ if (hasRedis(cell)) {
1758
1828
  const redis = [...tracker.pods.values()].filter((pod) => pod.labels["app"] === REDIS);
1759
1829
  if (redis.length !== 1 || !redis[0].ready || redis[0].del) return "Redis is not ready";
1760
1830
  }
@@ -1819,16 +1889,20 @@ const quiet = async (ctx, ours) => {
1819
1889
  await sleep(3e4);
1820
1890
  }
1821
1891
  };
1822
- /** This function deploys a cell in its namespace, with the watcher, the poll of the rules and the clients. */
1823
- const deploy = async (lab, cluster, cell, namespace, redisImage) => {
1892
+ /**
1893
+ * This function deploys a cell in its namespace, with the watcher, the poll of the rules and the
1894
+ * clients. A client of gRPC starts after the server is ready: each failed connect of a channel makes
1895
+ * the backoff of grpc-js longer, up to 120 s.
1896
+ */
1897
+ const deploy = async (lab, cluster, cell, namespace, images) => {
1824
1898
  await cluster.apply([{
1825
1899
  apiVersion: "v1",
1826
1900
  kind: "Namespace",
1827
1901
  metadata: { name: namespace }
1828
1902
  }]);
1829
1903
  await lab.watch([namespace]);
1830
- if (cell.declaration.redis !== null) {
1831
- await cluster.apply(redisObjects(namespace, redisImage));
1904
+ if (hasRedis(cell)) {
1905
+ await cluster.apply(redisObjects(namespace, images.redis));
1832
1906
  await cluster.kubectl([
1833
1907
  "-n",
1834
1908
  namespace,
@@ -1838,7 +1912,9 @@ const deploy = async (lab, cluster, cell, namespace, redisImage) => {
1838
1912
  "--timeout=180s"
1839
1913
  ]);
1840
1914
  }
1841
- await cluster.apply(serverObjects(cell, namespace, lab.image));
1915
+ const grpc = stackOf(cell) === "grpc";
1916
+ if (grpc && images.grpc === void 0) throw new Error(`${cell.name}: the cluster has no image of grpc-go`);
1917
+ await cluster.apply(serverObjects(cell, namespace, grpc ? images.grpc : lab.image));
1842
1918
  const clusterIp = (await cluster.kubectl([
1843
1919
  "-n",
1844
1920
  namespace,
@@ -1848,7 +1924,8 @@ const deploy = async (lab, cluster, cell, namespace, redisImage) => {
1848
1924
  "-o",
1849
1925
  "jsonpath={.spec.clusterIP}"
1850
1926
  ])).trim();
1851
- await cluster.apply(clientObjects(cell, namespace, lab.image, `${clusterIp}:80`));
1927
+ const clients = clientObjects(cell, namespace, lab.image, `${clusterIp}:80`);
1928
+ if (!grpc) await cluster.apply(clients);
1852
1929
  await cluster.kubectl([
1853
1930
  "-n",
1854
1931
  namespace,
@@ -1857,6 +1934,7 @@ const deploy = async (lab, cluster, cell, namespace, redisImage) => {
1857
1934
  `deployment/lab`,
1858
1935
  "--timeout=300s"
1859
1936
  ]);
1937
+ if (grpc) await cluster.apply(clients);
1860
1938
  await cluster.kubectl([
1861
1939
  "-n",
1862
1940
  namespace,
@@ -1889,13 +1967,21 @@ const probeThePods = async (ctx) => {
1889
1967
  await ctx.lab.probe([...new Set(netns)]);
1890
1968
  ctx.probed = key;
1891
1969
  };
1892
- /** This function gives the replicas of the service at this time, in the sequence of their names. */
1893
- const replicasOf = (ctx) => ctx.tracker.servers().map((pod) => ({
1894
- pod: pod.name,
1895
- node: pod.node ?? "",
1896
- ip: pod.ip ?? "",
1897
- version: pod.labels["pod-template-hash"] ?? ""
1898
- }));
1970
+ /**
1971
+ * This function gives the replicas of the server at this time, in the sequence of their names. The
1972
+ * first one is the target of a run. A client of gRPC holds its connection to one replica, and the
1973
+ * channels go to the replica that stays after a run. Thus for the stack `grpc`, the first replica
1974
+ * is the one that answers the most clients (`byClients`).
1975
+ */
1976
+ const replicasOf = (ctx) => {
1977
+ const replicas = ctx.tracker.servers().map((pod) => ({
1978
+ pod: pod.name,
1979
+ node: pod.node ?? "",
1980
+ ip: pod.ip ?? "",
1981
+ version: pod.labels["pod-template-hash"] ?? ""
1982
+ }));
1983
+ return stackOf(ctx.cell) === "grpc" ? byClients(replicas, ctx.tracker.answers) : replicas;
1984
+ };
1899
1985
  /** This function does one step of a fault plan. It gives the stamp of its act, or null for an act without a stamp. */
1900
1986
  const doStep = async (ctx, step, replicas, background) => {
1901
1987
  const { lab, cluster, namespace } = ctx;
@@ -2063,7 +2149,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
2063
2149
  if (await steady(ctx, 3e5) !== null) return empty("the cell was not at rest after the wait for a quiet VM", await lab.now());
2064
2150
  const replicas = replicasOf(ctx);
2065
2151
  const restarts = new Map(ctx.tracker.servers().map((pod) => [pod.name, pod.cs.reduce((sum, status) => sum + status.rc, 0)]));
2066
- const target = cell.declaration.redis !== null && [
2152
+ const target = hasRedis(cell) && [
2067
2153
  "rst",
2068
2154
  "blackhole",
2069
2155
  "freeze-process",
@@ -2206,6 +2292,15 @@ const runKindCells = async (cells, options) => {
2206
2292
  const environments = options.environments ?? 2;
2207
2293
  const image = await labImage();
2208
2294
  const redisImage = await localPin(REDIS_IMAGE);
2295
+ const grpcImage = cells.some((cell) => stackOf(cell) === "grpc") ? await imageOf({
2296
+ kind: "shipped",
2297
+ name: "grpc-go"
2298
+ }) : void 0;
2299
+ const images = {
2300
+ lab: image,
2301
+ redis: redisImage,
2302
+ grpc: grpcImage
2303
+ };
2209
2304
  const groups = /* @__PURE__ */ new Map();
2210
2305
  for (const cell of cells) groups.set(`${cell.mode}-${cell.workers}`, [...groups.get(`${cell.mode}-${cell.workers}`) ?? [], cell]);
2211
2306
  const results = [];
@@ -2221,6 +2316,7 @@ const runKindCells = async (cells, options) => {
2221
2316
  try {
2222
2317
  await cluster.load(image);
2223
2318
  await cluster.load(redisImage);
2319
+ if (grpcImage !== void 0 && group.some((cell) => stackOf(cell) === "grpc")) await cluster.load(grpcImage);
2224
2320
  lab = await openKindLab(cluster, image);
2225
2321
  const { GenericContainer } = await import("testcontainers");
2226
2322
  const Shared = class extends GenericContainer {
@@ -2239,7 +2335,7 @@ const runKindCells = async (cells, options) => {
2239
2335
  const off = lab.listen(tracker.see);
2240
2336
  const loads = [];
2241
2337
  try {
2242
- const clusterIp = await deploy(lab, cluster, cell, namespace, redisImage);
2338
+ const clusterIp = await deploy(lab, cluster, cell, namespace, images);
2243
2339
  for (const name of [...lab.own(), loader.getName().replace(/^\//, "")]) ours.add(name);
2244
2340
  const ctx = {
2245
2341
  lab,
@@ -2271,7 +2367,8 @@ const runKindCells = async (cells, options) => {
2271
2367
  cluster: manifest,
2272
2368
  images: {
2273
2369
  lab: image,
2274
- redis: redisImage
2370
+ redis: redisImage,
2371
+ ...stackOf(cell) === "grpc" ? { grpc: grpcImage } : {}
2275
2372
  }
2276
2373
  });
2277
2374
  entry.loads.push(...loads);
@@ -2360,6 +2457,6 @@ const shardOf = (cells, index, count) => {
2360
2457
  return out;
2361
2458
  };
2362
2459
  //#endregion
2363
- export { CLIENT, CONTROLLER, DOCUMENTED, DRAIN_RULES, FLAG_CHECK, FORMAT, FORMAT_V1, HEALTH_PORT, KEPT_KERNEL, KIND, KIND_NOISE, KUBECTL, KUBE_PROXY_MODES, LAST_SLACK, LOG_LABEL, NODE_IMAGES, O04, O09, O10, O16, PORT, REDIS, REDIS_IMAGE, RELATIVE, ROUTE, RULES_EVERY, SERVER, STAGES, WATCHDOG, backendsAt, backendsOf, bandOf, byNode, cacheFolder, calibrationOf, capOf, cellsTable, checkRun, cidrOverlap, cidrRange, clientObjects, compose, createCluster, deletes, deletionOf, deletionWindow, describeCell, dropOf, emptyOf, ensureTool, estimatedTime, factsOf, failuresOf, goDuration, intakeAfter, intakeOf, judge, judgeKind, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, kindConfig, kubeProxyPatch, kubeletBand, labImage, latchBand, lintCell, localPin, medianInterval, medianText, middle, namespaceOf, nodeNames, openKindLab, outcomesOf, pausedWindow, planOf, platformKey, podsAt, podsOf, predictionText, probeOf, propagationOf, quantileOf, readRecord, recordsOf, redisObjects, referenceOf, removalBand, requestsOf, restartBand, restartsAt, reusesReset, rulesOf, runKindCells, scenario, scenarioOf, scenarioProblems, seconds, secondsOf, serverObjects, sha256, shardOf, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, slicesOf, stageLabImage, stallRemovalBand, timeSpread, toolPath, transitionOf, unitOf, upgrade, upgradeCell, verdictsOf, writeResult };
2460
+ export { CLIENT, CONTROLLER, DOCUMENTED, DRAIN_RULES, FLAG_CHECK, FORMAT, FORMAT_V1, HEALTH_PORT, KEPT_KERNEL, KIND, KIND_NOISE, KUBECTL, KUBE_PROXY_MODES, LAST_SLACK, LOG_LABEL, NODE_IMAGES, O04, O09, O10, O16, PORT, REDIS, REDIS_IMAGE, RELATIVE, ROUTE, RULES_EVERY, SERVER, STAGES, WATCHDOG, backendsAt, backendsOf, bandOf, byClients, byNode, cacheFolder, calibrationOf, capOf, cellsTable, checkRun, cidrOverlap, cidrRange, clientObjects, compose, createCluster, deletes, deletionOf, deletionWindow, describeCell, dropOf, emptyOf, ensureTool, estimatedTime, factsOf, failuresOf, goDuration, intakeAfter, intakeOf, judge, judgeKind, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, kindConfig, kubeProxyPatch, kubeletBand, labImage, latchBand, lintCell, localPin, medianInterval, medianText, middle, namespaceOf, nodeNames, openKindLab, outcomesOf, pausedWindow, planOf, platformKey, podsAt, podsOf, predictionText, probeOf, propagationOf, quantileOf, readRecord, recordsOf, redisObjects, referenceOf, removalBand, requestsOf, restartBand, restartsAt, reusesReset, rulesOf, runKindCells, scenario, scenarioOf, scenarioProblems, seconds, secondsOf, serverObjects, sha256, shardOf, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, slicesOf, stackOf, stageLabImage, stallRemovalBand, stoppingOf, timeSpread, toolPath, transitionOf, unitOf, upgrade, upgradeCell, verdictsOf, writeResult };
2364
2461
 
2365
2462
  //# sourceMappingURL=index.js.map