@mark1russell7/kind 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +142 -2
- package/build/index.d.ts +393 -0
- package/build/index.js +2325 -0
- package/build/index.js.map +1 -0
- package/build/judge-CDDCr1Z8.d.ts +695 -0
- package/build/judge-CW1IX2zP.js +1018 -0
- package/build/judge-CW1IX2zP.js.map +1 -0
- package/build/judge.d.ts +2 -0
- package/build/judge.js +1 -0
- package/image/Dockerfile +18 -0
- package/image/agent.mjs +122 -0
- package/image/app.mjs +140 -0
- package/image/client.mjs +68 -0
- package/image/npm-lock.json +106 -0
- package/image/package.json +9 -0
- package/image/watch.mjs +175 -0
- package/package.json +83 -3
- package/src/backends.ts +70 -0
- package/src/compose.ts +68 -0
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
import { format, valueIn } from "@mark1russell7/quantity";
|
|
2
|
+
import { extentSchema, quantitySchema } from "@mark1russell7/quantity/schema";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/bands.ts
|
|
5
|
+
/** This module makes and prints the bands of the predictions. It imports only the amounts. */
|
|
6
|
+
/** This function gives a time in seconds as a quantity. */
|
|
7
|
+
const seconds = (value) => ({
|
|
8
|
+
value,
|
|
9
|
+
unit: "s"
|
|
10
|
+
});
|
|
11
|
+
/** This function gives a band from its two ends in seconds. */
|
|
12
|
+
const bandOf = (lo, hi) => ({
|
|
13
|
+
lo: seconds(lo),
|
|
14
|
+
hi: seconds(hi)
|
|
15
|
+
});
|
|
16
|
+
/** This function gives the two ends of a band as numbers of seconds, for arithmetic. */
|
|
17
|
+
const secondsOf = (band) => ({
|
|
18
|
+
lo: valueIn(band.lo, "s"),
|
|
19
|
+
hi: valueIn(band.hi, "s")
|
|
20
|
+
});
|
|
21
|
+
/** This function gives a band in a short form for a name or a table: `[28, 49] s`. */
|
|
22
|
+
const showBand = (band) => {
|
|
23
|
+
const { lo, hi } = secondsOf(band);
|
|
24
|
+
return `[${lo}, ${hi}] s`;
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/names.ts
|
|
28
|
+
/** These are the ports of the app: its server, and its health server for the probes. */
|
|
29
|
+
const PORT = 8080;
|
|
30
|
+
const HEALTH_PORT = 9e3;
|
|
31
|
+
/** This label makes the watcher follow the log of a pod. */
|
|
32
|
+
const LOG_LABEL = "lab.systems/log";
|
|
33
|
+
/** These are the names of the objects of a cell. */
|
|
34
|
+
const SERVER = "lab";
|
|
35
|
+
const CLIENT = "client";
|
|
36
|
+
const REDIS = "redis";
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/observe.ts
|
|
39
|
+
const of = (lines, ev) => lines.filter((line) => line.ev === ev);
|
|
40
|
+
const requestsOf = (lines) => of(lines, "req");
|
|
41
|
+
const podsOf = (lines) => of(lines, "pod").sort((a, b) => a.t - b.t);
|
|
42
|
+
const slicesOf = (lines, svc) => of(lines, "slice").filter((line) => line.svc === svc);
|
|
43
|
+
const rulesOf = (lines) => of(lines, "rules").sort((a, b) => a.t - b.t);
|
|
44
|
+
const recordsOf = (lines) => of(lines, "rec");
|
|
45
|
+
/** This function gives the requests that did not succeed, in sequence of their start. */
|
|
46
|
+
const failuresOf = (lines) => requestsOf(lines).filter((request) => request.o !== "ok").sort((a, b) => a.t - b.t);
|
|
47
|
+
/** This function counts the outcomes of the requests, by outcome. */
|
|
48
|
+
const outcomesOf = (requests) => {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const request of requests) out[request.o] = (out[request.o] ?? 0) + 1;
|
|
51
|
+
return out;
|
|
52
|
+
};
|
|
53
|
+
/** This function gives the requests of each node, by the name of the node. */
|
|
54
|
+
const byNode = (requests) => {
|
|
55
|
+
const out = /* @__PURE__ */ new Map();
|
|
56
|
+
for (const request of requests) out.set(request.n, [...out.get(request.n) ?? [], request]);
|
|
57
|
+
return out;
|
|
58
|
+
};
|
|
59
|
+
/** This function gives the time that the watcher first received a deletion timestamp of one of the pods, after a time. */
|
|
60
|
+
const deletionOf = (lines, pods, after) => podsOf(lines).find((pod) => pods.includes(pod.name) && pod.del && pod.t >= after)?.t ?? null;
|
|
61
|
+
/** This function gives the stamp of a transition of the service in a pod: the first matching record of a machine, after a time. */
|
|
62
|
+
const transitionOf = (lines, pod, machine, match, after) => recordsOf(lines).find((record) => record.p === pod && record.m === machine && record.at >= after && match(record))?.at ?? null;
|
|
63
|
+
/** This function gives when the service of a pod stopped its intake: the start of the phase `stop intake`. */
|
|
64
|
+
const intakeOf = (lines, pod, after) => transitionOf(lines, pod, "drain", (record) => record.to === "stop intake", after);
|
|
65
|
+
/** This function gives when the service of a pod received its exit signal. */
|
|
66
|
+
const signalOf = (lines, pod, after) => transitionOf(lines, pod, "exit", (record) => record.on === "exit.request", after);
|
|
67
|
+
/**
|
|
68
|
+
* This function gives when the rules of a node stopped the selection of a backend: the first poll
|
|
69
|
+
* after a time without it. The change came after `prev`, the poll before it, and at the latest at `t`.
|
|
70
|
+
*/
|
|
71
|
+
const dropOf = (lines, node, backend, after) => rulesOf(lines).find((line) => line.node === node && line.t >= after && !line.b.includes(backend)) ?? null;
|
|
72
|
+
/** This function gives when the rules of a node had no backend of the Service, after a time. */
|
|
73
|
+
const emptyOf = (lines, node, after) => rulesOf(lines).find((line) => line.node === node && line.t >= after && line.b.length === 0) ?? null;
|
|
74
|
+
/** This function gives when an EndpointSlice of the Service first had no ready endpoint, after a time. */
|
|
75
|
+
const sliceEmptyOf = (lines, svc, after) => slicesOf(lines, svc).find((slice) => slice.t >= after && !slice.eps.some((endpoint) => endpoint.r === true))?.t ?? null;
|
|
76
|
+
/** This function gives when the endpoint of an address was first not ready in the EndpointSlice of the Service, after a time. */
|
|
77
|
+
const sliceDropOf = (lines, svc, ip, after) => slicesOf(lines, svc).find((slice) => slice.t >= after && !slice.eps.some((endpoint) => endpoint.ip === ip && endpoint.r === true))?.t ?? null;
|
|
78
|
+
/** This function gives the latest state of each pod at a time, by its name. A deleted pod is not in it. */
|
|
79
|
+
const podsAt = (lines, at) => {
|
|
80
|
+
const out = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const pod of podsOf(lines)) {
|
|
82
|
+
if (pod.t > at) break;
|
|
83
|
+
if (pod.type === "DELETED") out.delete(pod.name);
|
|
84
|
+
else out.set(pod.name, pod);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
};
|
|
88
|
+
/** This function gives the latest backends of each node at a time, by node. */
|
|
89
|
+
const backendsAt = (lines, at) => {
|
|
90
|
+
const out = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const line of rulesOf(lines)) {
|
|
92
|
+
if (line.t > at) break;
|
|
93
|
+
out.set(line.node, line.b);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
};
|
|
97
|
+
/** This function gives the kernel records of the kinds that the lab keeps, counted by kind. */
|
|
98
|
+
const kernelCounts = (lines) => {
|
|
99
|
+
const out = {};
|
|
100
|
+
for (const line of lines) if (line.ev.startsWith("kernel.")) out[line.ev.slice(7)] = (out[line.ev.slice(7)] ?? 0) + 1;
|
|
101
|
+
return out;
|
|
102
|
+
};
|
|
103
|
+
/** This function gives the SIGTERM of each pod in a set after a time, by pod. A pod without one is not in it. */
|
|
104
|
+
const signalsOf = (lines, pods, after) => {
|
|
105
|
+
const out = /* @__PURE__ */ new Map();
|
|
106
|
+
for (const pod of pods) {
|
|
107
|
+
const at = signalOf(lines, pod, after);
|
|
108
|
+
if (at !== null) out.set(pod, at);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
};
|
|
112
|
+
/** This function gives the restart count of each container of each pod at a time, summed by pod. */
|
|
113
|
+
const restartsAt = (lines, at) => {
|
|
114
|
+
const out = /* @__PURE__ */ new Map();
|
|
115
|
+
for (const [name, pod] of podsAt(lines, at)) out.set(name, pod.cs.reduce((sum, status) => sum + status.rc, 0));
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/stats.ts
|
|
120
|
+
/** This fraction, δ_rel, is how far the median of a cell can be outside its band and stay equivalent. */
|
|
121
|
+
const RELATIVE = .05;
|
|
122
|
+
/** This function gives the median of sorted values: the middle value, or the mean of the two middle values. */
|
|
123
|
+
const middle = (sorted) => {
|
|
124
|
+
const half = Math.floor(sorted.length / 2);
|
|
125
|
+
return sorted.length % 2 === 1 ? sorted[half] : (sorted[half - 1] + sorted[half]) / 2;
|
|
126
|
+
};
|
|
127
|
+
/** This function gives the value at quantile p of sorted values, by nearest rank: x_(⌈p·n⌉). */
|
|
128
|
+
const quantileOf = (sorted, p) => sorted[Math.min(sorted.length, Math.max(1, Math.ceil(p * sorted.length))) - 1];
|
|
129
|
+
/** This function gives P(X ≤ j) for X ~ Binomial(n, 1/2), exactly. */
|
|
130
|
+
const below = (n, j) => {
|
|
131
|
+
let term = 1;
|
|
132
|
+
let sum = 0;
|
|
133
|
+
for (let i = 0; i <= j; i += 1) {
|
|
134
|
+
sum += term;
|
|
135
|
+
term = term * (n - i) / (i + 1);
|
|
136
|
+
}
|
|
137
|
+
return sum / 2 ** n;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* This function gives the distribution-free interval of a median: [x_(k), x_(n+1−k)], for the
|
|
141
|
+
* largest k whose two tails each hold 2.5 % or less. For eleven runs, it gives [x_(2), x_(10)],
|
|
142
|
+
* at 98.8 %.
|
|
143
|
+
*/
|
|
144
|
+
const medianInterval = (sorted) => {
|
|
145
|
+
const n = sorted.length;
|
|
146
|
+
let k = 0;
|
|
147
|
+
while (k + 1 <= n / 2 && below(n, k) <= .025) k += 1;
|
|
148
|
+
return k === 0 ? [sorted[0], sorted[n - 1]] : [sorted[k - 1], sorted[n - k]];
|
|
149
|
+
};
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/verdict.ts
|
|
152
|
+
/**
|
|
153
|
+
* This time is δ for kind: each time of a run lands in its band ± δ. Its parts are the interval of
|
|
154
|
+
* the poll of the rules (50 ms) and the delay of the watcher. The watcher receives each change after
|
|
155
|
+
* the API server writes it.
|
|
156
|
+
*/
|
|
157
|
+
const KIND_NOISE = {
|
|
158
|
+
value: 100,
|
|
159
|
+
unit: "ms"
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* This time is how much earlier than the end of its window the last failed request can start. A
|
|
163
|
+
* request goes to the closed pod by the choice of the rules, thus the last ones before the end can
|
|
164
|
+
* go to another pod. At 50 requests a second and two pods, 12 requests in sequence miss the closed
|
|
165
|
+
* pod with a chance of 1 in 4096.
|
|
166
|
+
*/
|
|
167
|
+
const LAST_SLACK = {
|
|
168
|
+
value: 250,
|
|
169
|
+
unit: "ms"
|
|
170
|
+
};
|
|
171
|
+
/** These are δ and the slack in milliseconds, the unit of the stamps. */
|
|
172
|
+
const NOISE = valueIn(KIND_NOISE, "ms");
|
|
173
|
+
const SLACK = valueIn(LAST_SLACK, "ms");
|
|
174
|
+
/**
|
|
175
|
+
* This function gives the unit of the main value of a cell, from its prediction:
|
|
176
|
+
* - `s` for a time: the removal, the departure or the restart of a band, and the end of a stall.
|
|
177
|
+
* - `{request}` for the failed requests: no failure, a window, a fleet that stays, and a measurement.
|
|
178
|
+
* - `{replica}` for the replicas that received SIGTERM, when no replica must restart.
|
|
179
|
+
*/
|
|
180
|
+
const unitOf = (prediction) => {
|
|
181
|
+
switch (prediction.kind) {
|
|
182
|
+
case "removal":
|
|
183
|
+
case "leaves":
|
|
184
|
+
case "stalls": return "s";
|
|
185
|
+
case "restarts": return prediction.band === null ? "{replica}" : "s";
|
|
186
|
+
default: return "{request}";
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const sec = (ms) => Math.round(ms) / 1e3;
|
|
190
|
+
const inBand = (value, band) => value >= band.lo - NOISE / 1e3 && value <= band.hi + NOISE / 1e3;
|
|
191
|
+
const time$2 = (value) => value === null ? null : seconds(value);
|
|
192
|
+
/** This function gives a reference event of a run, as a stamp, or null when the run does not have it. */
|
|
193
|
+
const referenceOf = (run, from) => {
|
|
194
|
+
const lines = run.lines;
|
|
195
|
+
if (from === "fault") return run.acts.fault;
|
|
196
|
+
if (from === "resume") return run.acts.resume;
|
|
197
|
+
if (run.target === null) return null;
|
|
198
|
+
const deletion = deletionOf(lines, [run.target.pod], run.start);
|
|
199
|
+
if (from === "deletion") return deletion;
|
|
200
|
+
return intakeOf(lines, run.target.pod, run.start);
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* This function gives the propagation of each node for the target of a run. It is the time from
|
|
204
|
+
* its deletion (or its crash) to the first poll of the rules of the node without it. The change
|
|
205
|
+
* came between the poll before (`after`) and that poll (`by`).
|
|
206
|
+
*/
|
|
207
|
+
const propagationOf = (run, from) => {
|
|
208
|
+
const out = {};
|
|
209
|
+
if (run.target === null || from === null) return out;
|
|
210
|
+
const nodes = [...new Set(run.lines.filter((line) => line.ev === "rules").map((line) => String(line["node"])))].sort();
|
|
211
|
+
for (const node of nodes) {
|
|
212
|
+
const drop = dropOf(run.lines, node, `${run.target.ip}:${PORT}`, from);
|
|
213
|
+
out[node] = drop === null ? null : {
|
|
214
|
+
after: drop.prev === null ? null : seconds(sec(Math.max(0, drop.prev - from))),
|
|
215
|
+
by: seconds(sec(drop.t - from))
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
};
|
|
220
|
+
/** This function gives the facts of a run that each table reads: its failures by outcome and node, and the times of its target. */
|
|
221
|
+
const factsOf = (cell, run) => {
|
|
222
|
+
const lines = run.lines;
|
|
223
|
+
const failures = failuresOf(lines).filter((request) => request.t >= run.start && request.t <= run.end);
|
|
224
|
+
const deletion = referenceOf(run, "deletion");
|
|
225
|
+
const intake = referenceOf(run, "intake");
|
|
226
|
+
const crash = cell.trigger.kind === "crash" ? run.acts.fault : null;
|
|
227
|
+
const from = deletion ?? crash;
|
|
228
|
+
return {
|
|
229
|
+
failures: failures.length,
|
|
230
|
+
outcomes: outcomesOf(failures),
|
|
231
|
+
byNode: Object.fromEntries([...byNode(failures)].map(([node, list]) => [node, list.length])),
|
|
232
|
+
first: failures.length === 0 ? null : failures[0].t,
|
|
233
|
+
last: failures.length === 0 ? null : failures.at(-1).t,
|
|
234
|
+
deletion,
|
|
235
|
+
intakeAfterDeletion: deletion === null || intake === null ? null : seconds(sec(intake - deletion)),
|
|
236
|
+
sliceAfter: run.target === null || from === null ? null : time$2((() => {
|
|
237
|
+
const at = sliceDropOf(lines, "lab", run.target.ip, from);
|
|
238
|
+
return at === null ? null : sec(at - from);
|
|
239
|
+
})()),
|
|
240
|
+
propagation: propagationOf(run, from)
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
/** This function judges one run against the prediction of its cell. */
|
|
244
|
+
const checkRun = (cell, run) => {
|
|
245
|
+
const lines = run.lines;
|
|
246
|
+
const facts = factsOf(cell, run);
|
|
247
|
+
const failures = failuresOf(lines).filter((request) => request.t >= run.start && request.t <= run.end);
|
|
248
|
+
const prediction = cell.prediction;
|
|
249
|
+
const unit = unitOf(prediction);
|
|
250
|
+
const done = (ok, why, value, more = {}) => ({
|
|
251
|
+
ok,
|
|
252
|
+
why,
|
|
253
|
+
value: value === null ? null : {
|
|
254
|
+
value,
|
|
255
|
+
unit
|
|
256
|
+
},
|
|
257
|
+
facts: {
|
|
258
|
+
...facts,
|
|
259
|
+
...more
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
switch (prediction.kind) {
|
|
263
|
+
case "none": {
|
|
264
|
+
const excluded = prediction.outcomes;
|
|
265
|
+
const counted = excluded === void 0 ? failures : failures.filter((request) => excluded.includes(request.o));
|
|
266
|
+
const what = excluded === void 0 ? "requests failed" : `requests ended ${excluded.join(", ")}`;
|
|
267
|
+
return done(counted.length === 0, counted.length === 0 ? `no ${excluded === void 0 ? "request failed" : `request ended ${excluded.join(", ")}`}` : `${counted.length} ${what}: ${JSON.stringify(facts.outcomes)}`, counted.length);
|
|
268
|
+
}
|
|
269
|
+
case "window": {
|
|
270
|
+
const start = referenceOf(run, prediction.start.from);
|
|
271
|
+
const end = prediction.end === null ? null : referenceOf(run, prediction.end.from);
|
|
272
|
+
if (start === null || prediction.end !== null && end === null) return done(false, `the run has no ${start === null ? prediction.start.from : prediction.end.from} reference`, null);
|
|
273
|
+
const opens = secondsOf(prediction.start.band);
|
|
274
|
+
const closes = prediction.end === null ? null : secondsOf(prediction.end.band);
|
|
275
|
+
const lo = start + opens.lo * 1e3 - NOISE;
|
|
276
|
+
const hi = end === null ? Number.POSITIVE_INFINITY : end + closes.hi * 1e3 + NOISE;
|
|
277
|
+
const relevant = prediction.nodes === "paused" ? failures.filter((request) => request.n === run.paused) : failures;
|
|
278
|
+
const others = prediction.nodes === "paused" ? failures.filter((request) => request.n !== run.paused) : [];
|
|
279
|
+
const outside = relevant.filter((request) => request.t < lo || request.t > hi);
|
|
280
|
+
const problems = [];
|
|
281
|
+
if (outside.length > 0) problems.push(`${outside.length} failed requests outside [${sec(lo - start)}, ${sec(hi - start)}] s after ${prediction.start.from}`);
|
|
282
|
+
if (others.length > 0) problems.push(`${others.length} failed requests on nodes whose kube-proxy did not pause`);
|
|
283
|
+
if (prediction.certain && relevant.length === 0) problems.push("no request failed in a window that the law opens");
|
|
284
|
+
if (prediction.certain && end !== null && relevant.length > 0) {
|
|
285
|
+
const last = relevant.at(-1).t;
|
|
286
|
+
const earliest = end + closes.lo * 1e3 - NOISE - SLACK;
|
|
287
|
+
if (last < earliest) problems.push(`the last failed request started ${sec(earliest - last)} s before the window can end`);
|
|
288
|
+
}
|
|
289
|
+
const span = relevant.length === 0 ? 0 : sec(relevant.at(-1).t - relevant[0].t);
|
|
290
|
+
return done(problems.length === 0, problems.length === 0 ? `${relevant.length} failed requests, all in the window (span ${span} s)` : problems.join("; "), relevant.length);
|
|
291
|
+
}
|
|
292
|
+
case "removal": {
|
|
293
|
+
const fault = run.acts.fault;
|
|
294
|
+
if (fault === null) return done(false, "the run has no fault stamp", null);
|
|
295
|
+
const empty = sliceEmptyOf(lines, "lab", fault);
|
|
296
|
+
if (empty === null) return done(false, "the Service kept a ready endpoint", null);
|
|
297
|
+
const value = sec(empty - fault);
|
|
298
|
+
const band = secondsOf(prediction.band);
|
|
299
|
+
const problems = [];
|
|
300
|
+
if (!inBand(value, band)) problems.push(`the EndpointSlice had no ready endpoint at ${value} s`);
|
|
301
|
+
const nodes = [...new Set(lines.filter((line) => line.ev === "rules").map((line) => String(line["node"])))].sort();
|
|
302
|
+
const rules = {};
|
|
303
|
+
for (const node of nodes) {
|
|
304
|
+
const drop = emptyOf(lines, node, fault);
|
|
305
|
+
rules[node] = drop === null ? null : seconds(sec(drop.t - fault));
|
|
306
|
+
if (drop === null) problems.push(`the rules of ${node} kept a backend`);
|
|
307
|
+
else if (!inBand(sec(drop.t - fault), {
|
|
308
|
+
lo: band.lo,
|
|
309
|
+
hi: band.hi + .05
|
|
310
|
+
})) problems.push(`the rules of ${node} had no backend at ${sec(drop.t - fault)} s`);
|
|
311
|
+
}
|
|
312
|
+
return done(problems.length === 0, problems.length === 0 ? `the Service was empty at ${value} s` : problems.join("; "), value, {
|
|
313
|
+
sliceEmpty: seconds(value),
|
|
314
|
+
rules
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
case "leaves": {
|
|
318
|
+
const fault = run.acts.fault;
|
|
319
|
+
if (fault === null || run.target === null) return done(false, "the run has no fault stamp or no target", null);
|
|
320
|
+
const at = sliceDropOf(lines, "lab", run.target.ip, fault);
|
|
321
|
+
if (at === null) return done(false, `${run.target.pod} stayed ready in the EndpointSlice`, null);
|
|
322
|
+
const value = sec(at - fault);
|
|
323
|
+
const band = secondsOf(prediction.band);
|
|
324
|
+
const problems = [];
|
|
325
|
+
if (!inBand(value, band)) problems.push(`${run.target.pod} left the EndpointSlice at ${value} s`);
|
|
326
|
+
const propagation = propagationOf(run, fault);
|
|
327
|
+
for (const [node, drop] of Object.entries(propagation)) {
|
|
328
|
+
const by = drop === null ? null : valueIn(drop.by, "s");
|
|
329
|
+
if (by === null) problems.push(`the rules of ${node} kept ${run.target.pod}`);
|
|
330
|
+
else if (!inBand(by, {
|
|
331
|
+
lo: band.lo,
|
|
332
|
+
hi: band.hi + .05
|
|
333
|
+
})) problems.push(`the rules of ${node} dropped it at ${by} s`);
|
|
334
|
+
}
|
|
335
|
+
return done(problems.length === 0, problems.length === 0 ? `${run.target.pod} left at ${value} s` : problems.join("; "), value, {
|
|
336
|
+
left: seconds(value),
|
|
337
|
+
propagation
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
case "restarts": {
|
|
341
|
+
const fault = run.acts.fault;
|
|
342
|
+
if (fault === null) return done(false, "the run has no fault stamp", null);
|
|
343
|
+
const until = run.acts.heal ?? run.end;
|
|
344
|
+
const pods = run.replicas.map((replica) => replica.pod);
|
|
345
|
+
const signals = signalsOf(lines, pods, fault);
|
|
346
|
+
const inside = [...signals].filter(([, at]) => at <= until);
|
|
347
|
+
if (prediction.band === null) return done(inside.length === 0, inside.length === 0 ? "no replica received SIGTERM" : `${inside.length} replicas received SIGTERM`, inside.length);
|
|
348
|
+
const band = secondsOf(prediction.band);
|
|
349
|
+
const times = pods.map((pod) => {
|
|
350
|
+
const at = signals.get(pod);
|
|
351
|
+
return at === void 0 ? null : sec(at - fault);
|
|
352
|
+
});
|
|
353
|
+
const missing = times.filter((each) => each === null).length;
|
|
354
|
+
const off = times.filter((each) => each !== null && !inBand(each, band));
|
|
355
|
+
const known = times.filter((each) => each !== null).sort((a, b) => a - b);
|
|
356
|
+
const ok = missing === 0 && off.length === 0;
|
|
357
|
+
return done(ok, ok ? `each replica received SIGTERM, at ${known.join(", ")} s` : `${missing} replicas without SIGTERM, ${off.length} outside the band (${known.join(", ")} s)`, known.length === 0 ? null : middle(known), { signals: times.map(time$2) });
|
|
358
|
+
}
|
|
359
|
+
case "stays": {
|
|
360
|
+
const fault = run.acts.fault;
|
|
361
|
+
if (fault === null) return done(false, "the run has no fault stamp", null);
|
|
362
|
+
const until = run.end;
|
|
363
|
+
const dropped = run.replicas.filter((replica) => {
|
|
364
|
+
const at = sliceDropOf(lines, "lab", replica.ip, fault);
|
|
365
|
+
return at !== null && at <= until;
|
|
366
|
+
});
|
|
367
|
+
const refused = failures.filter((request) => request.o === "refused" && request.t >= fault);
|
|
368
|
+
const problems = [];
|
|
369
|
+
if (dropped.length > 0) problems.push(`${dropped.length} replicas left the EndpointSlice`);
|
|
370
|
+
if (refused.length > 0) problems.push(`${refused.length} requests were refused`);
|
|
371
|
+
return done(problems.length === 0, problems.length === 0 ? `the replicas stayed ready, ${failures.length} requests failed by their dependency` : problems.join("; "), failures.length);
|
|
372
|
+
}
|
|
373
|
+
case "stalls": {
|
|
374
|
+
const fault = run.acts.fault;
|
|
375
|
+
const heal = run.acts.heal;
|
|
376
|
+
if (fault === null || heal === null) return done(false, "the run has no fault or heal stamp", null);
|
|
377
|
+
const old = new Set(run.replicas.map((replica) => replica.pod));
|
|
378
|
+
const servers = podsOf(lines).filter((pod) => pod.labels["app"] === "lab");
|
|
379
|
+
const pods = servers.filter((pod) => pod.t >= fault && pod.t <= heal);
|
|
380
|
+
const deleted = pods.filter((pod) => old.has(pod.name) && pod.del);
|
|
381
|
+
const newReady = pods.filter((pod) => !old.has(pod.name) && pod.ready);
|
|
382
|
+
const after = servers.filter((pod) => pod.t > heal);
|
|
383
|
+
const firstReady = after.find((pod) => !old.has(pod.name) && pod.ready);
|
|
384
|
+
const finished = firstReady !== void 0 && after.some((pod) => old.has(pod.name) && pod.type === "DELETED");
|
|
385
|
+
const problems = [];
|
|
386
|
+
if (deleted.length > 0) problems.push(`${new Set(deleted.map((pod) => pod.name)).size} old pods were deleted while Redis was down`);
|
|
387
|
+
if (newReady.length > 0) problems.push("a new pod became ready while Redis was down");
|
|
388
|
+
if (!finished) problems.push("the rollout did not continue after Redis came back");
|
|
389
|
+
if (problems.length === 0) return done(true, `the rollout waited for Redis, and its first new pod was ready ${sec(firstReady.t - heal)} s after Redis came back`, sec(firstReady.t - heal));
|
|
390
|
+
return done(false, problems.join("; "), null);
|
|
391
|
+
}
|
|
392
|
+
case "measured": return done(true, "measured", facts.failures);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
/** This function judges a cell on its runs: each counted run must agree, and a median in a band must be equivalent to it. */
|
|
396
|
+
const judgeKind = (cell, runs) => {
|
|
397
|
+
const counted = runs.filter((run) => !run.warmup && run.void === null);
|
|
398
|
+
const voided = runs.filter((run) => !run.warmup && run.void !== null).length;
|
|
399
|
+
const checks = counted.map((run) => checkRun(cell, run));
|
|
400
|
+
const values = checks.flatMap((check) => check.value === null ? [] : [check.value.value]).sort((a, b) => a - b);
|
|
401
|
+
const median = values.length === 0 ? null : {
|
|
402
|
+
value: middle(values),
|
|
403
|
+
unit: unitOf(cell.prediction)
|
|
404
|
+
};
|
|
405
|
+
const failed = checks.filter((check) => !check.ok).length;
|
|
406
|
+
if (counted.length === 0) return {
|
|
407
|
+
decision: "void",
|
|
408
|
+
counted: 0,
|
|
409
|
+
voided,
|
|
410
|
+
failed: 0,
|
|
411
|
+
median: null,
|
|
412
|
+
why: "no counted run",
|
|
413
|
+
checks
|
|
414
|
+
};
|
|
415
|
+
if (cell.calibrates) return {
|
|
416
|
+
decision: "calibration",
|
|
417
|
+
counted: counted.length,
|
|
418
|
+
voided,
|
|
419
|
+
failed,
|
|
420
|
+
median,
|
|
421
|
+
why: "a calibration of the propagation",
|
|
422
|
+
checks
|
|
423
|
+
};
|
|
424
|
+
if (cell.prediction.kind === "measured") return {
|
|
425
|
+
decision: "measured",
|
|
426
|
+
counted: counted.length,
|
|
427
|
+
voided,
|
|
428
|
+
failed: 0,
|
|
429
|
+
median,
|
|
430
|
+
why: cell.prediction.why,
|
|
431
|
+
checks
|
|
432
|
+
};
|
|
433
|
+
const prediction = cell.prediction;
|
|
434
|
+
const band = prediction.kind === "removal" || prediction.kind === "leaves" || prediction.kind === "restarts" ? prediction.band : null;
|
|
435
|
+
if (failed > 0) {
|
|
436
|
+
const first = checks.find((check) => !check.ok);
|
|
437
|
+
return {
|
|
438
|
+
decision: "fails",
|
|
439
|
+
counted: counted.length,
|
|
440
|
+
voided,
|
|
441
|
+
failed,
|
|
442
|
+
median,
|
|
443
|
+
why: `${failed} of ${counted.length} runs disagree, the first: ${first.why}`,
|
|
444
|
+
checks
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (band !== null && values.length > 0) {
|
|
448
|
+
const ends = secondsOf(band);
|
|
449
|
+
const [from, to] = medianInterval(values);
|
|
450
|
+
const wide = {
|
|
451
|
+
lo: ends.lo * .95,
|
|
452
|
+
hi: ends.hi * 1.05
|
|
453
|
+
};
|
|
454
|
+
if (to < wide.lo || from > wide.hi) return {
|
|
455
|
+
decision: "fails",
|
|
456
|
+
counted: counted.length,
|
|
457
|
+
voided,
|
|
458
|
+
failed,
|
|
459
|
+
median,
|
|
460
|
+
why: `the median interval [${from}, ${to}] s is outside the band`,
|
|
461
|
+
checks
|
|
462
|
+
};
|
|
463
|
+
if (from < wide.lo || to > wide.hi) return {
|
|
464
|
+
decision: "extend",
|
|
465
|
+
counted: counted.length,
|
|
466
|
+
voided,
|
|
467
|
+
failed,
|
|
468
|
+
median,
|
|
469
|
+
why: `the median interval [${from}, ${to}] s crosses the edge of the band`,
|
|
470
|
+
checks
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
decision: "holds",
|
|
475
|
+
counted: counted.length,
|
|
476
|
+
voided,
|
|
477
|
+
failed,
|
|
478
|
+
median,
|
|
479
|
+
why: `${counted.length} of ${counted.length} runs agree`,
|
|
480
|
+
checks
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/record.ts
|
|
485
|
+
/**
|
|
486
|
+
* This module is the raw record of a kind cell: its formats, and the upgrade from the format `/1`
|
|
487
|
+
* to the format `/2`. The lab writes `/2`, and it does not write `/1` again. A reader reads a record
|
|
488
|
+
* of `/1` through `upgrade`, and the meaning of each value stays the same.
|
|
489
|
+
*/
|
|
490
|
+
/** This format is the format that the lab writes. Each amount in it is a quantity. */
|
|
491
|
+
const FORMAT = "systems.kind-cell-record/2";
|
|
492
|
+
/** This format is the first format. Its bands, its medians and its times are numbers. */
|
|
493
|
+
const FORMAT_V1 = "systems.kind-cell-record/1";
|
|
494
|
+
const time$1 = (value) => value === null ? null : seconds(value);
|
|
495
|
+
const band = (old) => ({
|
|
496
|
+
lo: seconds(old.lo),
|
|
497
|
+
hi: seconds(old.hi)
|
|
498
|
+
});
|
|
499
|
+
/** The format `/1` gives each part of `cpu.max` in microseconds. */
|
|
500
|
+
const microseconds = (value) => ({
|
|
501
|
+
value,
|
|
502
|
+
unit: "us"
|
|
503
|
+
});
|
|
504
|
+
const upgradeTrigger = (trigger) => {
|
|
505
|
+
const { cpuMax, ...rest } = trigger;
|
|
506
|
+
return cpuMax === void 0 ? rest : {
|
|
507
|
+
...rest,
|
|
508
|
+
cpuMax: {
|
|
509
|
+
quota: microseconds(cpuMax.quota),
|
|
510
|
+
period: microseconds(cpuMax.period)
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
const upgradePrediction = (prediction) => {
|
|
515
|
+
switch (prediction.kind) {
|
|
516
|
+
case "window": return {
|
|
517
|
+
...prediction,
|
|
518
|
+
start: {
|
|
519
|
+
from: prediction.start.from,
|
|
520
|
+
band: band(prediction.start.band)
|
|
521
|
+
},
|
|
522
|
+
end: prediction.end === null ? null : {
|
|
523
|
+
from: prediction.end.from,
|
|
524
|
+
band: band(prediction.end.band)
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
case "removal":
|
|
528
|
+
case "leaves": return {
|
|
529
|
+
...prediction,
|
|
530
|
+
band: band(prediction.band)
|
|
531
|
+
};
|
|
532
|
+
case "restarts": return {
|
|
533
|
+
...prediction,
|
|
534
|
+
band: prediction.band === null ? null : band(prediction.band)
|
|
535
|
+
};
|
|
536
|
+
default: return prediction;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
/** This function gives a cell of the format `/1` in the format `/2`: its bands in seconds, and its `cpu.max` in microseconds. */
|
|
540
|
+
const upgradeCell = (cell) => ({
|
|
541
|
+
...cell,
|
|
542
|
+
trigger: upgradeTrigger(cell.trigger),
|
|
543
|
+
prediction: upgradePrediction(cell.prediction)
|
|
544
|
+
});
|
|
545
|
+
const upgradeFacts = (facts) => {
|
|
546
|
+
const propagated = (old) => old === null ? null : {
|
|
547
|
+
after: time$1(old.after),
|
|
548
|
+
by: seconds(old.by)
|
|
549
|
+
};
|
|
550
|
+
const { sliceEmpty, rules, left, signals, ...rest } = facts;
|
|
551
|
+
return {
|
|
552
|
+
...rest,
|
|
553
|
+
intakeAfterDeletion: time$1(facts.intakeAfterDeletion),
|
|
554
|
+
sliceAfter: time$1(facts.sliceAfter),
|
|
555
|
+
propagation: Object.fromEntries(Object.entries(facts.propagation).map(([node, old]) => [node, propagated(old)])),
|
|
556
|
+
...sliceEmpty === void 0 ? {} : { sliceEmpty: seconds(sliceEmpty) },
|
|
557
|
+
...rules === void 0 ? {} : { rules: Object.fromEntries(Object.entries(rules).map(([node, old]) => [node, time$1(old)])) },
|
|
558
|
+
...left === void 0 ? {} : { left: seconds(left) },
|
|
559
|
+
...signals === void 0 ? {} : { signals: signals.map((old) => time$1(old)) }
|
|
560
|
+
};
|
|
561
|
+
};
|
|
562
|
+
/**
|
|
563
|
+
* This function gives a record in the format `/2`. A record of `/2` comes back as it is. A record
|
|
564
|
+
* of `/1` gets each amount as a quantity, and each value stays the same:
|
|
565
|
+
* - Each band and each time of a fact is in seconds, as `/1` gives it.
|
|
566
|
+
* - The `cpu.max` of a throttle is in microseconds, as `/1` gives it.
|
|
567
|
+
* - The median and the main value of each run get the unit of the prediction (`unitOf`): seconds,
|
|
568
|
+
* failed requests (`{request}`) or replicas (`{replica}`).
|
|
569
|
+
*/
|
|
570
|
+
const upgrade = (record) => {
|
|
571
|
+
if (record.format === "systems.kind-cell-record/2") return record;
|
|
572
|
+
const unit = unitOf(record.cell.prediction);
|
|
573
|
+
const amount = (value) => value === null ? null : {
|
|
574
|
+
value,
|
|
575
|
+
unit
|
|
576
|
+
};
|
|
577
|
+
const verdict = record.verdict;
|
|
578
|
+
return {
|
|
579
|
+
format: FORMAT,
|
|
580
|
+
cell: upgradeCell(record.cell),
|
|
581
|
+
runs: record.runs,
|
|
582
|
+
manifests: record.manifests,
|
|
583
|
+
loads: record.loads,
|
|
584
|
+
verdict: {
|
|
585
|
+
...verdict,
|
|
586
|
+
median: amount(verdict.median),
|
|
587
|
+
checks: verdict.checks.map((check) => ({
|
|
588
|
+
...check,
|
|
589
|
+
value: amount(check.value),
|
|
590
|
+
facts: upgradeFacts(check.facts)
|
|
591
|
+
}))
|
|
592
|
+
},
|
|
593
|
+
at: record.at
|
|
594
|
+
};
|
|
595
|
+
};
|
|
596
|
+
/**
|
|
597
|
+
* This function judges a record again, from its cell and its runs, with the judge of this version.
|
|
598
|
+
* It reads a record of `/1` through `upgrade`. The verdict is in the format `/2`.
|
|
599
|
+
*/
|
|
600
|
+
const judge = (record) => {
|
|
601
|
+
const current = upgrade(record);
|
|
602
|
+
return judgeKind(current.cell, current.runs);
|
|
603
|
+
};
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region src/report.ts
|
|
606
|
+
/** This function gives the spread of a set of times, or null for none. The percentile is by nearest rank. */
|
|
607
|
+
const timeSpread = (times) => {
|
|
608
|
+
if (times.length === 0) return null;
|
|
609
|
+
const sorted = times.map((each) => valueIn(each, "s")).sort((a, b) => a - b);
|
|
610
|
+
const half = Math.floor(sorted.length / 2);
|
|
611
|
+
const median = sorted.length % 2 === 1 ? sorted[half] : (sorted[half - 1] + sorted[half]) / 2;
|
|
612
|
+
return {
|
|
613
|
+
count: sorted.length,
|
|
614
|
+
min: seconds(sorted[0]),
|
|
615
|
+
median: seconds(Math.round(median * 1e3) / 1e3),
|
|
616
|
+
p99: seconds(quantileOf(sorted, .99)),
|
|
617
|
+
max: seconds(sorted.at(-1))
|
|
618
|
+
};
|
|
619
|
+
};
|
|
620
|
+
/** This function gives the prediction of a cell in a short form for a table. */
|
|
621
|
+
const predictionText = (prediction) => {
|
|
622
|
+
switch (prediction.kind) {
|
|
623
|
+
case "none": return prediction.outcomes === void 0 ? "no failure" : `no ${prediction.outcomes.join(", ")}`;
|
|
624
|
+
case "window": return `failures only from the ${prediction.start.from}${prediction.end === null ? "" : ` to the ${prediction.end.from} + ${showBand(prediction.end.band)}`}${prediction.nodes === "paused" ? ", on the paused node" : ""}${prediction.certain ? ", at least one" : ""}`;
|
|
625
|
+
case "removal": return `Service empty in ${showBand(prediction.band)}`;
|
|
626
|
+
case "leaves": return `the replica leaves in ${showBand(prediction.band)}`;
|
|
627
|
+
case "restarts": return prediction.band === null ? "no restart" : `each replica SIGTERM in ${showBand(prediction.band)}`;
|
|
628
|
+
case "stays": return "stays ready, no refusal";
|
|
629
|
+
case "stalls": return "the rollout waits for Redis";
|
|
630
|
+
case "measured": return "measured";
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
/**
|
|
634
|
+
* This function gives the main measurement of a cell in a short form: its median and what it
|
|
635
|
+
* counts. For failed requests, it is the middle count of the counted runs.
|
|
636
|
+
*/
|
|
637
|
+
const medianText = (judged) => {
|
|
638
|
+
const { cell, verdict } = judged;
|
|
639
|
+
if (verdict.median === null) return "";
|
|
640
|
+
if (verdict.median.unit === "{request}") {
|
|
641
|
+
const failures = judged.runs.filter((run) => !run.warmup && run.void === null).map((run) => factsOf(cell, run).failures).sort((a, b) => a - b);
|
|
642
|
+
return `${failures.length === 0 ? 0 : failures[Math.floor(failures.length / 2)]} failed requests`;
|
|
643
|
+
}
|
|
644
|
+
return format(verdict.median, { digits: 4 });
|
|
645
|
+
};
|
|
646
|
+
/** This function gives the table of the cells in Markdown. Each row has the cell, its plan cell, its mode, its trigger, the prediction, the verdict, its runs and its median. */
|
|
647
|
+
const cellsTable = (judged) => [
|
|
648
|
+
"| Cell | Plan id | Mode | Trigger | Prediction | Verdict | Runs | Median |",
|
|
649
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
650
|
+
...judged.map((each) => {
|
|
651
|
+
const { cell, verdict } = each;
|
|
652
|
+
const runs = verdict.voided === 0 ? `${verdict.counted}` : `${verdict.counted} (+${verdict.voided} void)`;
|
|
653
|
+
return `| ${cell.name} | ${cell.id} | ${cell.mode} | ${cell.trigger.kind} | ${predictionText(cell.prediction)} | ${verdict.decision} | ${runs} | ${medianText(each)} |`;
|
|
654
|
+
})
|
|
655
|
+
].join("\n");
|
|
656
|
+
/**
|
|
657
|
+
* This function gives the propagation of each calibration, by node. It is the time from the
|
|
658
|
+
* deletion to the poll of the rules of each node without the pod. It also gives the time to the
|
|
659
|
+
* EndpointSlice and to the stop of the intake.
|
|
660
|
+
*/
|
|
661
|
+
const calibrationOf = (judged) => {
|
|
662
|
+
const byNode = {};
|
|
663
|
+
const slice = [];
|
|
664
|
+
const intake = [];
|
|
665
|
+
for (const run of judged.runs.filter((each) => !each.warmup && each.void === null)) {
|
|
666
|
+
const deletion = referenceOf(run, "deletion");
|
|
667
|
+
for (const [node, drop] of Object.entries(propagationOf(run, deletion))) if (drop !== null) (byNode[node] ??= []).push(drop.by);
|
|
668
|
+
const facts = factsOf(judged.cell, run);
|
|
669
|
+
if (facts.sliceAfter !== null) slice.push(facts.sliceAfter);
|
|
670
|
+
if (facts.intakeAfterDeletion !== null) intake.push(facts.intakeAfterDeletion);
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
nodes: Object.fromEntries(Object.entries(byNode).map(([node, values]) => [node, timeSpread(values)])),
|
|
674
|
+
all: timeSpread(Object.values(byNode).flat()),
|
|
675
|
+
slice: timeSpread(slice),
|
|
676
|
+
intake: timeSpread(intake)
|
|
677
|
+
};
|
|
678
|
+
};
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/schema.ts
|
|
681
|
+
/**
|
|
682
|
+
* This module holds the zod schemas of a kind cell record, in the formats `/1` and `/2`. It names
|
|
683
|
+
* the stages of the shutdown itself, thus it does not load the service. A test holds the names to
|
|
684
|
+
* the service.
|
|
685
|
+
*/
|
|
686
|
+
/** These are the stages of the shutdown of the service, in their sequence. */
|
|
687
|
+
const STAGES = [
|
|
688
|
+
"fail readiness",
|
|
689
|
+
"stop intake",
|
|
690
|
+
"drain",
|
|
691
|
+
"close transports",
|
|
692
|
+
"stop the poller",
|
|
693
|
+
"tear down dependencies",
|
|
694
|
+
"close the health server"
|
|
695
|
+
];
|
|
696
|
+
const TRIGGERS = [
|
|
697
|
+
"pod-deletion",
|
|
698
|
+
"rolling-update",
|
|
699
|
+
"node-drain",
|
|
700
|
+
"scale-in",
|
|
701
|
+
"crash",
|
|
702
|
+
"liveness-restart",
|
|
703
|
+
"freeze-process",
|
|
704
|
+
"rst",
|
|
705
|
+
"blackhole",
|
|
706
|
+
"peer-gone-without-close",
|
|
707
|
+
"app-stall",
|
|
708
|
+
"rollout",
|
|
709
|
+
"cpu-throttle"
|
|
710
|
+
];
|
|
711
|
+
const MODES = [
|
|
712
|
+
"iptables",
|
|
713
|
+
"ipvs",
|
|
714
|
+
"nftables"
|
|
715
|
+
];
|
|
716
|
+
const time = quantitySchema("time");
|
|
717
|
+
const extent = extentSchema("time");
|
|
718
|
+
const count = z.number().int().nonnegative();
|
|
719
|
+
const gate = z.enum(["readiness", "liveness"]);
|
|
720
|
+
const reference = z.enum([
|
|
721
|
+
"deletion",
|
|
722
|
+
"intake",
|
|
723
|
+
"fault",
|
|
724
|
+
"resume"
|
|
725
|
+
]);
|
|
726
|
+
const probe = z.object({
|
|
727
|
+
period: time,
|
|
728
|
+
threshold: count,
|
|
729
|
+
timeout: time
|
|
730
|
+
});
|
|
731
|
+
const timing = z.discriminatedUnion("type", [z.object({
|
|
732
|
+
type: z.literal("debounce"),
|
|
733
|
+
interval: time,
|
|
734
|
+
failAfter: count,
|
|
735
|
+
recoverAfter: count
|
|
736
|
+
}), z.object({
|
|
737
|
+
type: z.literal("window"),
|
|
738
|
+
failAfter: time,
|
|
739
|
+
recoverAfter: time.optional()
|
|
740
|
+
})]);
|
|
741
|
+
const knobs = z.object({
|
|
742
|
+
connect_bound: time,
|
|
743
|
+
request_timeout: time,
|
|
744
|
+
driver_writeoff_time: extent,
|
|
745
|
+
keepalive_idle: extent,
|
|
746
|
+
heartbeat_interval: extent,
|
|
747
|
+
heartbeat_timeout: time,
|
|
748
|
+
client_retry_window: extent,
|
|
749
|
+
retry_base: time,
|
|
750
|
+
retry_cap: time,
|
|
751
|
+
close_bound: time,
|
|
752
|
+
max_connection_age: extent,
|
|
753
|
+
retrans_giveup_time: extent
|
|
754
|
+
});
|
|
755
|
+
const declaration = z.object({
|
|
756
|
+
poll: z.object({
|
|
757
|
+
interval: time,
|
|
758
|
+
jitter: z.number().min(0).lt(1),
|
|
759
|
+
timeout: time
|
|
760
|
+
}),
|
|
761
|
+
readiness: timing,
|
|
762
|
+
liveness: timing,
|
|
763
|
+
startup: time,
|
|
764
|
+
phases: z.array(z.tuple([z.enum(STAGES), time])),
|
|
765
|
+
cooperativeWindow: extent,
|
|
766
|
+
couplings: z.array(z.object({
|
|
767
|
+
from: gate,
|
|
768
|
+
to: gate,
|
|
769
|
+
effect: z.literal("fails")
|
|
770
|
+
})),
|
|
771
|
+
redis: z.object({
|
|
772
|
+
gates: gate.nullable(),
|
|
773
|
+
knobs,
|
|
774
|
+
misses: count
|
|
775
|
+
}).nullable(),
|
|
776
|
+
flag: gate.nullable(),
|
|
777
|
+
watchdog: z.object({
|
|
778
|
+
tick: time,
|
|
779
|
+
threshold: time
|
|
780
|
+
}).nullable()
|
|
781
|
+
});
|
|
782
|
+
const replica = z.object({
|
|
783
|
+
pod: z.string(),
|
|
784
|
+
node: z.string(),
|
|
785
|
+
ip: z.string(),
|
|
786
|
+
version: z.string()
|
|
787
|
+
});
|
|
788
|
+
/** A line keeps each field that its source wrote. The judge reads `ev`, `t` and the fields of its kind. */
|
|
789
|
+
const line = z.object({
|
|
790
|
+
ev: z.string(),
|
|
791
|
+
t: z.number()
|
|
792
|
+
}).catchall(z.unknown());
|
|
793
|
+
const run = z.object({
|
|
794
|
+
env: count,
|
|
795
|
+
index: z.number().int(),
|
|
796
|
+
warmup: z.boolean(),
|
|
797
|
+
start: z.number(),
|
|
798
|
+
end: z.number(),
|
|
799
|
+
acts: z.object({
|
|
800
|
+
fault: z.number().nullable(),
|
|
801
|
+
heal: z.number().nullable(),
|
|
802
|
+
resume: z.number().nullable()
|
|
803
|
+
}),
|
|
804
|
+
target: replica.nullable(),
|
|
805
|
+
paused: z.string().nullable(),
|
|
806
|
+
replicas: z.array(replica),
|
|
807
|
+
void: z.string().nullable(),
|
|
808
|
+
lines: z.array(line),
|
|
809
|
+
dropped: z.record(z.string(), count),
|
|
810
|
+
notes: z.array(z.string()).optional()
|
|
811
|
+
});
|
|
812
|
+
const manifest = z.object({
|
|
813
|
+
env: count,
|
|
814
|
+
scenario: z.object({
|
|
815
|
+
name: z.string(),
|
|
816
|
+
kubernetes: z.string(),
|
|
817
|
+
workers: count,
|
|
818
|
+
kubeProxy: z.object({
|
|
819
|
+
mode: z.enum(MODES),
|
|
820
|
+
minSyncPeriod: time,
|
|
821
|
+
syncPeriod: time
|
|
822
|
+
}),
|
|
823
|
+
cni: z.literal("kindnet"),
|
|
824
|
+
podSubnet: z.string(),
|
|
825
|
+
serviceSubnet: z.string()
|
|
826
|
+
}),
|
|
827
|
+
cluster: z.object({
|
|
828
|
+
kind: z.string(),
|
|
829
|
+
nodeImage: z.string(),
|
|
830
|
+
server: z.string(),
|
|
831
|
+
mode: z.string(),
|
|
832
|
+
docker: z.string(),
|
|
833
|
+
nodes: z.array(z.object({
|
|
834
|
+
name: z.string(),
|
|
835
|
+
kubelet: z.string(),
|
|
836
|
+
kernel: z.string(),
|
|
837
|
+
runtime: z.string(),
|
|
838
|
+
os: z.string()
|
|
839
|
+
}))
|
|
840
|
+
}),
|
|
841
|
+
images: z.record(z.string(), z.string())
|
|
842
|
+
});
|
|
843
|
+
const load = z.object({
|
|
844
|
+
t: z.number(),
|
|
845
|
+
idle: z.number(),
|
|
846
|
+
top: z.array(z.string()),
|
|
847
|
+
foreign: z.array(z.string())
|
|
848
|
+
});
|
|
849
|
+
/**
|
|
850
|
+
* This function gives the schema of a record whose amounts have one form. The schema `a` is for a
|
|
851
|
+
* time. The schema `main` is for the main value of a run: a time or a count.
|
|
852
|
+
*/
|
|
853
|
+
const recordOf = (a, main) => {
|
|
854
|
+
const band = z.object({
|
|
855
|
+
lo: a,
|
|
856
|
+
hi: a
|
|
857
|
+
});
|
|
858
|
+
const prediction = z.discriminatedUnion("kind", [
|
|
859
|
+
z.object({
|
|
860
|
+
kind: z.literal("none"),
|
|
861
|
+
outcomes: z.array(z.string()).optional(),
|
|
862
|
+
why: z.string()
|
|
863
|
+
}),
|
|
864
|
+
z.object({
|
|
865
|
+
kind: z.literal("window"),
|
|
866
|
+
nodes: z.enum(["all", "paused"]),
|
|
867
|
+
start: z.object({
|
|
868
|
+
from: reference,
|
|
869
|
+
band
|
|
870
|
+
}),
|
|
871
|
+
end: z.object({
|
|
872
|
+
from: reference,
|
|
873
|
+
band
|
|
874
|
+
}).nullable(),
|
|
875
|
+
certain: z.boolean(),
|
|
876
|
+
why: z.string()
|
|
877
|
+
}),
|
|
878
|
+
z.object({
|
|
879
|
+
kind: z.literal("removal"),
|
|
880
|
+
band,
|
|
881
|
+
why: z.string()
|
|
882
|
+
}),
|
|
883
|
+
z.object({
|
|
884
|
+
kind: z.literal("leaves"),
|
|
885
|
+
band,
|
|
886
|
+
why: z.string()
|
|
887
|
+
}),
|
|
888
|
+
z.object({
|
|
889
|
+
kind: z.literal("restarts"),
|
|
890
|
+
band: band.nullable(),
|
|
891
|
+
why: z.string()
|
|
892
|
+
}),
|
|
893
|
+
z.object({
|
|
894
|
+
kind: z.literal("stays"),
|
|
895
|
+
why: z.string()
|
|
896
|
+
}),
|
|
897
|
+
z.object({
|
|
898
|
+
kind: z.literal("stalls"),
|
|
899
|
+
why: z.string()
|
|
900
|
+
}),
|
|
901
|
+
z.object({
|
|
902
|
+
kind: z.literal("measured"),
|
|
903
|
+
why: z.string()
|
|
904
|
+
})
|
|
905
|
+
]);
|
|
906
|
+
const cell = z.object({
|
|
907
|
+
id: z.string(),
|
|
908
|
+
name: z.string(),
|
|
909
|
+
template: z.enum(["T07", "T18"]),
|
|
910
|
+
substitutes: z.string().nullable(),
|
|
911
|
+
mode: z.enum(MODES),
|
|
912
|
+
workers: count,
|
|
913
|
+
trigger: z.object({
|
|
914
|
+
kind: z.enum(TRIGGERS),
|
|
915
|
+
hold: time.optional(),
|
|
916
|
+
lead: time.optional(),
|
|
917
|
+
cpuMax: z.object({
|
|
918
|
+
quota: a,
|
|
919
|
+
period: a
|
|
920
|
+
}).optional()
|
|
921
|
+
}),
|
|
922
|
+
pod: z.object({
|
|
923
|
+
replicas: count,
|
|
924
|
+
grace: time,
|
|
925
|
+
preStop: time,
|
|
926
|
+
readiness: probe,
|
|
927
|
+
liveness: probe,
|
|
928
|
+
startup: probe.nullable()
|
|
929
|
+
}),
|
|
930
|
+
declaration,
|
|
931
|
+
workload: z.object({
|
|
932
|
+
every: time,
|
|
933
|
+
timeout: time
|
|
934
|
+
}),
|
|
935
|
+
prediction,
|
|
936
|
+
calibrates: z.boolean(),
|
|
937
|
+
runs: count
|
|
938
|
+
});
|
|
939
|
+
const facts = z.object({
|
|
940
|
+
failures: count,
|
|
941
|
+
outcomes: z.record(z.string(), count),
|
|
942
|
+
byNode: z.record(z.string(), count),
|
|
943
|
+
first: z.number().nullable(),
|
|
944
|
+
last: z.number().nullable(),
|
|
945
|
+
deletion: z.number().nullable(),
|
|
946
|
+
intakeAfterDeletion: a.nullable(),
|
|
947
|
+
sliceAfter: a.nullable(),
|
|
948
|
+
propagation: z.record(z.string(), z.object({
|
|
949
|
+
after: a.nullable(),
|
|
950
|
+
by: a
|
|
951
|
+
}).nullable()),
|
|
952
|
+
sliceEmpty: a.optional(),
|
|
953
|
+
rules: z.record(z.string(), a.nullable()).optional(),
|
|
954
|
+
left: a.optional(),
|
|
955
|
+
signals: z.array(a.nullable()).optional()
|
|
956
|
+
});
|
|
957
|
+
return {
|
|
958
|
+
cell,
|
|
959
|
+
verdict: z.object({
|
|
960
|
+
decision: z.enum([
|
|
961
|
+
"holds",
|
|
962
|
+
"fails",
|
|
963
|
+
"extend",
|
|
964
|
+
"measured",
|
|
965
|
+
"calibration",
|
|
966
|
+
"void"
|
|
967
|
+
]),
|
|
968
|
+
counted: count,
|
|
969
|
+
voided: count,
|
|
970
|
+
failed: count,
|
|
971
|
+
median: main.nullable(),
|
|
972
|
+
why: z.string(),
|
|
973
|
+
checks: z.array(z.object({
|
|
974
|
+
ok: z.boolean(),
|
|
975
|
+
why: z.string(),
|
|
976
|
+
value: main.nullable(),
|
|
977
|
+
facts
|
|
978
|
+
}))
|
|
979
|
+
}),
|
|
980
|
+
runs: z.array(run),
|
|
981
|
+
manifests: z.array(manifest),
|
|
982
|
+
loads: z.array(load),
|
|
983
|
+
at: z.string()
|
|
984
|
+
};
|
|
985
|
+
};
|
|
986
|
+
const v2 = recordOf(time, quantitySchema(["time", "count"]));
|
|
987
|
+
const v1 = recordOf(z.number(), z.number());
|
|
988
|
+
/**
|
|
989
|
+
* This schema accepts a record in the format `/2`. The median and the main value of each run must
|
|
990
|
+
* have the unit of the prediction of the cell (`unitOf`).
|
|
991
|
+
*/
|
|
992
|
+
const kindCellRecordSchema = z.object({
|
|
993
|
+
format: z.literal(FORMAT),
|
|
994
|
+
...v2
|
|
995
|
+
}).superRefine((record, ctx) => {
|
|
996
|
+
const unit = unitOf(record.cell.prediction);
|
|
997
|
+
const values = [record.verdict.median, ...record.verdict.checks.map((check) => check.value)];
|
|
998
|
+
for (const value of values) if (value !== null && value.unit !== unit) ctx.addIssue({
|
|
999
|
+
code: "custom",
|
|
1000
|
+
message: `the main value is in ${value.unit}, and the prediction ${record.cell.prediction.kind} gives ${unit}`
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
/** This schema accepts a record in the format `/1`. */
|
|
1004
|
+
const kindCellRecordV1Schema = z.object({
|
|
1005
|
+
format: z.literal(FORMAT_V1),
|
|
1006
|
+
...v1
|
|
1007
|
+
});
|
|
1008
|
+
/** This function reads a record of either format from parsed JSON, and gives it in the format `/2`. */
|
|
1009
|
+
const readRecord = (data) => {
|
|
1010
|
+
const format = typeof data === "object" && data !== null ? data.format : void 0;
|
|
1011
|
+
if (format === "systems.kind-cell-record/1") return upgrade(kindCellRecordV1Schema.parse(data));
|
|
1012
|
+
if (format === "systems.kind-cell-record/2") return kindCellRecordSchema.parse(data);
|
|
1013
|
+
throw new Error(`a kind cell record has the format ${FORMAT} or ${FORMAT_V1}, not ${String(format)}`);
|
|
1014
|
+
};
|
|
1015
|
+
//#endregion
|
|
1016
|
+
export { SERVER as $, dropOf as A, restartsAt as B, RELATIVE as C, backendsAt as D, quantileOf as E, outcomesOf as F, sliceEmptyOf as G, signalOf as H, podsAt as I, CLIENT as J, slicesOf as K, podsOf as L, failuresOf as M, intakeOf as N, byNode as O, kernelCounts as P, REDIS as Q, recordsOf as R, unitOf as S, middle as T, signalsOf as U, rulesOf as V, sliceDropOf as W, LOG_LABEL as X, HEALTH_PORT as Y, PORT as Z, checkRun as _, calibrationOf as a, propagationOf as b, predictionText as c, FORMAT_V1 as d, bandOf as et, judge as f, LAST_SLACK as g, KIND_NOISE as h, readRecord as i, emptyOf as j, deletionOf as k, timeSpread as l, upgradeCell as m, kindCellRecordSchema as n, secondsOf as nt, cellsTable as o, upgrade as p, transitionOf as q, kindCellRecordV1Schema as r, showBand as rt, medianText as s, STAGES as t, seconds as tt, FORMAT as u, factsOf as v, medianInterval as w, referenceOf as x, judgeKind as y, requestsOf as z };
|
|
1017
|
+
|
|
1018
|
+
//# sourceMappingURL=judge-CW1IX2zP.js.map
|