@mark1russell7/kind 0.1.6 → 0.1.7

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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/image/app.mjs +54 -72
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -96,6 +96,9 @@ The lab image (`image/`) holds the service of a cell on `@mark1russell7/service`
96
96
  - **In the workspace** of this repository, the stage builds `@mark1russell7/quantity`, `@mark1russell7/algebra` and `@mark1russell7/service`, and packs each one as npm gets it.
97
97
  - **From npm**, the stage packs the installed versions of the same packages. The package holds the files of the image, and `src/compose.ts` and `src/backends.ts`, for that.
98
98
  - **The service** (`app.mjs`) is a Node HTTP server. Its one controller needs Redis when the cell has Redis. Its health server gives the probes, and it declares each stage of the shutdown. `compose.ts` composes the service from the declaration of the cell, and `lint()` reads the same composition.
99
+ - **The HTTP edge of the service.** From 0.1.7, the service has no transport of its own. Its listener is `httpTransport` of `@mark1russell7/service/http`, and its probes are `probeServer` on the health port. Its one route is `handle()` over `life.request`.
100
+ - **The answers of the route.** A value gets 200, and a refusal or a cut of the shutdown gets 503. A failure of the route also gets 503, through the hook `failed`, as in 0.1.6. Thus the T07 and T18 cells count the same outcomes. Each 503 gets `Retry-After` of 1 s.
101
+ - **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.
99
102
  - **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.
100
103
  - **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.
101
104
 
package/image/app.mjs CHANGED
@@ -2,9 +2,11 @@
2
2
  // health server, its Redis client when the cell has Redis, and each stage of the shutdown. The
3
3
  // declaration comes from the CONFIG variable of the pod, and compose.ts is the same composition that
4
4
  // lint() reads. Each line on stdout is JSON, stamped on CLOCK_MONOTONIC (process.hrtime), the clock
5
- // of every container of the VM.
5
+ // of every container of the VM. The listener, the probes and the answers are the ./http entry of the
6
+ // service: the lab has no transport of its own.
6
7
  import { existsSync, readFileSync } from "node:fs";
7
8
  import http from "node:http";
9
+ import { handle, httpTransport, pathOf, probeServer } from "@mark1russell7/service/http";
8
10
  import { redisClient } from "@mark1russell7/service/redis";
9
11
  import { CONTROLLER, ROUTE, compose } from "./compose.ts";
10
12
 
@@ -23,6 +25,7 @@ const clock = {
23
25
  const say = (line) => process.stdout.write(`${JSON.stringify({ ...line, t: now(), p: pod })}\n`);
24
26
  const record = (event) =>
25
27
  say({ ev: "rec", at: event.at, m: event.machine, i: event.instance, from: event.from, to: event.to, on: event.on, d: event.detail ?? null });
28
+ const s = (value) => ({ value, unit: "s" });
26
29
 
27
30
  // the seam of the stall: SIGUSR2 blocks the event loop for the milliseconds in /tmp/lab-stall
28
31
  process.on("SIGUSR2", () => {
@@ -54,85 +57,64 @@ const redis =
54
57
  const FLAG = process.env.LAB_FLAG ?? "/tmp/lab-fail";
55
58
  const flag = () => (existsSync(FLAG) ? "error" : "ok");
56
59
 
57
- // the server transport: a new connection for each request, and Connection: close while the service is not ready.
58
- // The lifecycle comes with the start of the transport, thus a request before the end of the startup
59
- // gets the refusal of the service: 503, retryable
60
- const sockets = new Set();
61
- let inFlight = 0;
62
- const serve = (life) => async (req, res) => {
63
- inFlight += 1;
64
- try {
65
- // a unary work is a stream of one item: the item is the answer, else the end gives the status
66
- const { items, end } = await life
67
- .request(CONTROLLER, ROUTE, async (signal) => {
68
- if (redis !== undefined) {
69
- const sent = await redis.send("GET", ["lab"], { signal });
70
- if (sent.outcome !== "ok") throw new Error(`redis ${sent.outcome}: ${sent.error}`);
71
- }
72
- return pod;
73
- })
74
- .collect();
75
- const headers = { "content-type": "text/plain" };
76
- if (!life.keepAlive()) headers.connection = "close";
77
- if (end.kind === "done") res.writeHead(200, headers).end(String(items[0]));
78
- else if (end.kind === "failed") res.writeHead(503, headers).end(`failed: ${end.error?.message ?? end.error}`);
79
- else res.writeHead(end.status ?? 500, headers).end(`${end.kind}: ${end.cause}`);
80
- } finally {
81
- inFlight -= 1;
82
- }
83
- };
60
+ // the knobs of the listener. The real idle close is the one of Node 24.21 before the ./http entry:
61
+ // keepAliveTimeout 5 s and its buffer of 1 s. The clients send each request on a new connection, with
62
+ // Connection: close. The sweep comes each second after the stop of the intake (S6 A8), and each 503
63
+ // tells a client to try again after 1 s
64
+ const KNOBS = { server_idle_close: s(6), server_idle_sweep_interval: s(1), server_retry_after: s(1) };
65
+
66
+ // the server transport: the transport gets the lifecycle at its start, thus a request before the end
67
+ // of the startup gets the refusal of the service (503, retryable), and Connection: close while the
68
+ // service is not ready
84
69
  const server = http.createServer();
85
- server.on("connection", (socket) => {
86
- sockets.add(socket);
87
- socket.on("close", () => sockets.delete(socket));
88
- });
89
- const transport = {
90
- start: (_signal, life) =>
91
- new Promise((resolve, reject) => {
92
- server.on("request", serve(life));
93
- server.once("error", reject);
94
- server.listen(config.port, () => {
95
- say({ ev: "app", what: "listening", port: config.port });
96
- resolve();
97
- });
98
- }),
70
+ server.on("listening", () => say({ ev: "app", what: "listening", port: config.port }));
71
+ const transport = httpTransport(server, KNOBS, { listen: { port: config.port }, clock });
72
+ // each request of the port is the one route. A failure of the route answers 503, as the lab always
73
+ // did: the T07 and T18 cells count the 503s, and T18 predicts them while Redis is down. Each 503 gets
74
+ // Retry-After from the rule of the transport
75
+ transport.mount(
76
+ handle(
77
+ CONTROLLER,
78
+ () => ROUTE,
79
+ async (_req, signal) => {
80
+ if (redis !== undefined) {
81
+ const sent = await redis.send("GET", ["lab"], { signal });
82
+ if (sent.outcome !== "ok") throw new Error(`redis ${sent.outcome}: ${sent.error}`);
83
+ }
84
+ return pod;
85
+ },
86
+ {
87
+ failed: (error) => ({
88
+ status: 503,
89
+ headers: { "content-type": "text/plain; charset=utf-8" },
90
+ body: `failed: ${error?.message ?? error}`,
91
+ }),
92
+ },
93
+ ),
94
+ );
95
+ // the lifecycle stops the intake of the transport, and the line tells when
96
+ const lab = {
97
+ start: (signal, life) => transport.start(signal, life),
99
98
  stopIntake: () => {
100
- // the listener closes: a new connection is refused from here on
101
- server.close();
102
- server.closeIdleConnections();
99
+ transport.stopIntake();
103
100
  say({ ev: "app", what: "intake-stopped" });
104
101
  },
105
- drain: async (deadline) => {
106
- while (inFlight > 0 && now() < deadline) await new Promise((resolve) => setTimeout(resolve, 10));
107
- },
108
- inFlight: () => inFlight,
109
- close: () => {
110
- for (const socket of sockets) socket.destroy();
111
- },
102
+ drain: (deadline, signal) => transport.drain(deadline, signal),
103
+ inFlight: () => transport.inFlight(),
104
+ close: () => transport.close(),
112
105
  };
113
106
 
114
- // the health server: each probe answers from the cache, and each answer is a line
115
- const probes = http.createServer((req, res) => {
116
- const kind = (req.url ?? "/").slice(1).split("?")[0];
117
- if (!["startup", "readiness", "liveness", "report"].includes(kind) || health === null) {
118
- res.writeHead(404).end();
119
- return;
120
- }
121
- const answer = health.probe(kind);
122
- say({ ev: "probe", k: kind, s: answer.status });
123
- res.writeHead(answer.status, { "content-type": answer.contentType }).end(JSON.stringify(answer.body));
107
+ // the health server: each probe answers from the cache on its own port, and each answer is a line
108
+ const KINDS = new Set(["startup", "readiness", "liveness", "report"]);
109
+ const probesServer = http.createServer();
110
+ const probes = probeServer({ port: config.healthPort, server: probesServer, server_retry_after: KNOBS.server_retry_after });
111
+ // the line of each probe comes when its answer goes, and it writes nothing itself
112
+ probesServer.on("request", (req, res) => {
113
+ const kind = pathOf(req.url).slice(1);
114
+ if (KINDS.has(kind)) res.once("finish", () => say({ ev: "probe", k: kind, s: res.statusCode }));
124
115
  });
125
- let health = null;
126
- const healthServer = {
127
- listen: (runtime) =>
128
- new Promise((resolve) => {
129
- health = runtime;
130
- probes.listen(config.healthPort, resolve);
131
- }),
132
- close: () => new Promise((resolve) => probes.close(() => resolve())),
133
- };
134
116
 
135
- const svc = compose(config.name, declaration, { redis, flag, http: transport, health: healthServer });
117
+ const svc = compose(config.name, declaration, { redis, flag, http: lab, health: probes });
136
118
  const exits = { on: (signal, handler) => (process.on(signal, handler), () => process.off(signal, handler)), exit: (code) => {
137
119
  say({ ev: "app", what: "exit", code, errors: svc.lifecycle?.errors ?? [] });
138
120
  process.exit(code);
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.1.6",
4
+ "version": "0.1.7",
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",
@@ -62,7 +62,7 @@
62
62
  "dependencies": {
63
63
  "@mark1russell7/harness": "^0.11.1",
64
64
  "@mark1russell7/quantity": "^0.2.0",
65
- "@mark1russell7/service": "^0.4.1",
65
+ "@mark1russell7/service": "^0.5.0",
66
66
  "testcontainers": "12.2.0"
67
67
  },
68
68
  "peerDependencies": {