@mark1russell7/kind 0.1.8 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -79,6 +79,15 @@ declare const cidrOverlap: (a: string, b: string) => boolean;
79
79
  //#region src/cell.d.ts
80
80
  /** A template is the topology of a cell: T07, the endpoint lifecycle of a Service, or T18, a health-gated fleet. */
81
81
  type Template = "T07" | "T18";
82
+ /**
83
+ * A stack is the server and the clients of a cell:
84
+ * - `service`: the service of this repository with its declaration, and the HTTP client, which opens a
85
+ * new connection for each request. Thus kube-proxy picks a backend for each request.
86
+ * - `grpc`: the server of grpc-go v1.84.0 of the harness, and a client of grpc-js 1.14.6 with one channel
87
+ * at its defaults. The channel holds one connection. It connects again only when that connection
88
+ * ends: at a GOAWAY of the server, or at the end of its socket.
89
+ */
90
+ type Stack = "service" | "grpc";
82
91
  /**
83
92
  * A trigger is the event or the fault of a cell, by the name of the plan:
84
93
  * - `pod-deletion`, `rolling-update`, `node-drain` and `scale-in` stop a pod with its grace.
@@ -157,11 +166,17 @@ interface Declaration {
157
166
  readonly threshold: Quantity;
158
167
  } | null;
159
168
  }
160
- /** A workload is the clients: one pod on each node, each with requests on new connections to the ClusterIP. */
169
+ /**
170
+ * A workload is the clients: one pod on each node, each with requests to the ClusterIP. The HTTP client
171
+ * opens a new connection for each request. The gRPC client sends each call on its one channel.
172
+ */
161
173
  interface Workload {
162
174
  /** This time is the interval between two requests of one client. The clients do not wait for an answer. */
163
175
  readonly every: Quantity;
164
- /** This time is how long a client waits for an answer before the request is a timeout. */
176
+ /**
177
+ * This time is how long a client waits for an answer before the request is a timeout. A call of
178
+ * gRPC has no deadline at the defaults of grpc-js, thus the client cancels it at this time.
179
+ */
165
180
  readonly timeout: Quantity;
166
181
  }
167
182
  /**
@@ -197,6 +212,8 @@ interface ResetReuse<A = Quantity> {
197
212
  * - `window`: the requests that fail start in a window. The window starts at `start` and ends in
198
213
  * `end`. With `certain`, at least one request fails. With `afterReset`, a request outside the
199
214
  * window can also fail, on a port that a reset closed up to `life` before it (`ResetReuse`).
215
+ * With `each`, each replica of the run that gets a deletion has a window of its own, from its own
216
+ * references. A request is late only when it is outside each of these windows.
200
217
  * - `removal`: the Service has no ready endpoint, and the rules of each node have no backend, in the band.
201
218
  * - `leaves`: the target replica leaves the EndpointSlice and the rules of each node in the band.
202
219
  * - `restarts`: each replica receives SIGTERM in the band, or no replica does.
@@ -222,6 +239,12 @@ type Prediction<A = Quantity> = {
222
239
  } | null;
223
240
  readonly certain: boolean;
224
241
  readonly afterReset?: ResetReuse<A> | undefined;
242
+ /**
243
+ * This flag gives a window to each replica of the run (`replicas`) that gets a deletion after the
244
+ * start of the run. The references `deletion` and `intake` are then those of that replica. A
245
+ * rolling update deletes each old replica in turn. Without the flag, the window is that of the target.
246
+ */
247
+ readonly each?: boolean | undefined;
225
248
  readonly why: string;
226
249
  } | {
227
250
  readonly kind: "removal";
@@ -295,13 +318,16 @@ interface KindCell<A = Quantity> {
295
318
  readonly id: string;
296
319
  readonly name: string;
297
320
  readonly template: Template;
321
+ /** This stack is the server and the clients of the cell. Without it, the cell has the service (`service`). */
322
+ readonly stack?: Stack | undefined;
298
323
  /** This text names a system of the plan that the cell replaces with the service of this repository, or null. */
299
324
  readonly substitutes: string | null;
300
325
  readonly mode: KubeProxyMode;
301
326
  readonly workers: number;
302
327
  readonly trigger: Trigger<A>;
303
328
  readonly pod: PodSettings;
304
- readonly declaration: Declaration;
329
+ /** This declaration is the settings of the service, or null for a server that is not the service (the stack `grpc`). */
330
+ readonly declaration: Declaration | null;
305
331
  readonly workload: Workload;
306
332
  readonly prediction: Prediction<A>;
307
333
  /** This flag marks a calibration: its runs measure the propagation of the endpoints, and nothing fails. */
@@ -342,7 +368,7 @@ declare const REDIS = "redis";
342
368
  */
343
369
  /**
344
370
  * A line is one JSON line of a source of a run, with its stamp `t` in milliseconds on
345
- * CLOCK_MONOTONIC. It is a request of a client, a record of the service or a probe answer. It is
371
+ * CLOCK_MONOTONIC. It is a request of a client, a record of the service, a line of grpc-go or a probe answer. It is
346
372
  * also a pod, an EndpointSlice or an event of the API. Or it is the backends of the rules of a
347
373
  * node, a kernel record, or an act of the lab. A record keeps each line as its source wrote it.
348
374
  */
@@ -430,15 +456,32 @@ declare const recordsOf: (lines: readonly Line[]) => RecordLine[];
430
456
  declare const failuresOf: (lines: readonly Line[]) => Request[];
431
457
  /** This function counts the outcomes of the requests, by outcome. */
432
458
  declare const outcomesOf: (requests: readonly Request[]) => Record<string, number>;
459
+ /**
460
+ * This function orders the replicas by how many clients each one answered last, the most first,
461
+ * then by name. `answers` gives the backend of the last answer of each client, by client. A client
462
+ * of gRPC holds its connection to one replica, thus the first replica is the one whose stop the
463
+ * most clients see.
464
+ */
465
+ declare const byClients: <T extends {
466
+ readonly pod: string;
467
+ }>(replicas: readonly T[], answers: ReadonlyMap<string, string>) => T[];
433
468
  /** This function gives the requests of each node, by the name of the node. */
434
469
  declare const byNode: (requests: readonly Request[]) => Map<string, Request[]>;
435
470
  /** This function gives the time that the watcher first received a deletion timestamp of one of the pods, after a time. */
436
471
  declare const deletionOf: (lines: readonly Line[], pods: readonly string[], after: number) => number | null;
437
472
  /** This function gives the stamp of a transition of the service in a pod: the first matching record of a machine, after a time. */
438
473
  declare const transitionOf: (lines: readonly Line[], pod: string, machine: string, match: (record: RecordLine) => boolean, after: number) => number | null;
439
- /** This function gives when the service of a pod stopped its intake: the start of the phase `stop intake`. */
474
+ /**
475
+ * This function gives the line `stopping` of grpc-go in a pod, after a time. The server writes it at
476
+ * SIGTERM. Then GracefulStop closes its listener and sends GOAWAY on each connection.
477
+ */
478
+ declare const stoppingOf: (lines: readonly Line[], pod: string, after: number) => number | null;
479
+ /**
480
+ * This function gives when the server of a pod stopped its intake. That is the start of the phase
481
+ * `stop intake` of the service, or the line `stopping` of grpc-go.
482
+ */
440
483
  declare const intakeOf: (lines: readonly Line[], pod: string, after: number) => number | null;
441
- /** This function gives when the service of a pod received its exit signal. */
484
+ /** This function gives when the server of a pod received its exit signal: a record of the service, or the line `stopping` of grpc-go. */
442
485
  declare const signalOf: (lines: readonly Line[], pod: string, after: number) => number | null;
443
486
  /**
444
487
  * This function gives when the rules of a node stopped the selection of a backend: the first poll
@@ -506,6 +549,17 @@ interface RunFacts<A = Quantity> {
506
549
  readonly left?: A | undefined;
507
550
  /** These times are the SIGTERM of each replica, after the fault (`restarts`). */
508
551
  readonly signals?: ReadonlyArray<A | null> | undefined;
552
+ /**
553
+ * These are the replicas of a window for each deleted replica (`each`). Each one is a replica of the
554
+ * run with a deletion after its start, with the stamps of its intake and of its deletion.
555
+ */
556
+ readonly deletions?: readonly Deleted[] | undefined;
557
+ }
558
+ /** A deleted replica is a replica of a run that got a deletion. It gives its pod, and the stamps of its intake (or null) and of its deletion. */
559
+ interface Deleted {
560
+ readonly pod: string;
561
+ readonly intake: number | null;
562
+ readonly deletion: number;
509
563
  }
510
564
  /**
511
565
  * A run check is the judgement of one run: if it agrees with the prediction, why, its main value,
@@ -552,14 +606,22 @@ declare const unitOf: (prediction: {
552
606
  * `outcome`.
553
607
  */
554
608
  declare const reusesReset: (request: Request, requests: readonly Request[], reuse: ResetReuse) => boolean;
555
- /** This function gives a reference event of a run, as a stamp, or null when the run does not have it. */
556
- declare const referenceOf: (run: KindRun, from: Reference) => number | null;
557
609
  /**
558
- * This function gives the propagation of each node for the target of a run. It is the time from
559
- * its deletion (or its crash) to the first poll of the rules of the node without it. The change
560
- * came between the poll before (`after`) and that poll (`by`).
610
+ * This function gives a reference event of a run, as a stamp, or null when the run does not have it.
611
+ * The references `deletion` and `intake` are those of the target, or of the replica `pod`.
612
+ */
613
+ declare const referenceOf: (run: KindRun, from: Reference, pod?: string | undefined) => number | null;
614
+ /**
615
+ * This function gives the propagation of each node for the target of a run, or for the replica of
616
+ * the address `ip`. It is the time from its deletion (or its crash) to the first poll of the rules
617
+ * of the node without it. The change came between the poll before (`after`) and that poll (`by`).
618
+ */
619
+ declare const propagationOf: (run: KindRun, from: number | null, ip?: string | undefined) => Record<string, Propagated | null>;
620
+ /**
621
+ * This function gives each replica of a run that got a deletion after the start of the run, in the
622
+ * sequence of the deletions. A rolling update deletes each old replica in turn.
561
623
  */
562
- declare const propagationOf: (run: KindRun, from: number | null) => Record<string, Propagated | null>;
624
+ declare const deletionsOf: (run: KindRun) => Deleted[];
563
625
  /** This function gives the facts of a run that each table reads: its failures by outcome and node, and the times of its target. */
564
626
  declare const factsOf: (cell: KindCell, run: KindRun) => RunFacts;
565
627
  /** This function judges one run against the prediction of its cell. */
@@ -722,5 +784,5 @@ declare const quantileOf: (sorted: readonly number[], p: number) => number;
722
784
  */
723
785
  declare const medianInterval: (sorted: readonly number[]) => [number, number];
724
786
  //#endregion
725
- export { RulesLine as $, cidrOverlap as $t, TriggerV1 as A, secondsOf as At, RunFacts as B, Reference as Bt, KindCellResult as C, HEALTH_PORT as Ct, PredictionV1 as D, SERVER as Dt, LoadSample as E, REDIS as Et, KindDecision as F, KindRun as Ft, referenceOf as G, TriggerKind as Gt, factsOf as H, ResetReuse as Ht, KindVerdict as I, KubeletProbe as It, Endpoint as J, KindConfig as Jt, reusesReset as K, Workload as Kt, LAST_SLACK as L, PodSettings as Lt, upgrade as M, Band as Mt, upgradeCell as N, Declaration as Nt, RunCheckV1 as O, bandOf as Ot, KIND_NOISE as P, KindCell as Pt, Request as Q, NodeImage as Qt, Propagated as R, Prediction as Rt, KindCellRecordV1 as S, CLIENT as St, KindVerdictV1 as T, PORT as Tt, judgeKind as U, Template as Ut, checkRun as V, Replica as Vt, propagationOf as W, Trigger as Wt, PodLine as X, KubeProxyMode as Xt, Line as Y, KindScenario as Yt, RecordLine as Z, NODE_IMAGES as Zt, ClusterManifest as _, signalsOf as _t, STAGES as a, scenario as an, emptyOf as at, FORMAT_V1 as b, slicesOf as bt, readRecord as c, kernelCounts as ct, calibrationOf as d, podsOf as dt, cidrRange as en, SliceLine as et, cellsTable as f, recordsOf as ft, BandV1 as g, signalOf as gt, timeSpread as h, rulesOf as ht, quantileOf as i, nodeNames as in, dropOf as it, judge as j, showBand as jt, RunFactsV1 as k, seconds as kt, Judged as l, outcomesOf as lt, predictionText as m, restartsAt as mt, medianInterval as n, kindConfig as nn, byNode as nt, kindCellRecordSchema as o, scenarioProblems as on, failuresOf as ot, medianText as p, requestsOf as pt, unitOf as q, KUBE_PROXY_MODES as qt, middle as r, kubeProxyPatch as rn, deletionOf as rt, kindCellRecordV1Schema as s, intakeOf as st, RELATIVE as t, goDuration as tn, backendsAt as tt, TimeSpread as u, podsAt as ut, EnvironmentManifest as v, sliceDropOf as vt, KindCellV1 as w, LOG_LABEL as wt, KindCellRecord as x, transitionOf as xt, FORMAT as y, sliceEmptyOf as yt, RunCheck as z, RedisDeclaration as zt };
726
- //# sourceMappingURL=judge-a7yCzwZc.d.ts.map
787
+ export { RecordLine as $, KindConfig as $t, TriggerV1 as A, REDIS as At, RunCheck as B, KubeletProbe as Bt, KindCellResult as C, slicesOf as Ct, PredictionV1 as D, HEALTH_PORT as Dt, LoadSample as E, CLIENT as Et, KIND_NOISE as F, showBand as Ft, judgeKind as G, Replica as Gt, checkRun as H, Prediction as Ht, KindDecision as I, Band as It, reusesReset as J, Template as Jt, propagationOf as K, ResetReuse as Kt, KindVerdict as L, Declaration as Lt, upgrade as M, bandOf as Mt, upgradeCell as N, seconds as Nt, RunCheckV1 as O, LOG_LABEL as Ot, Deleted as P, secondsOf as Pt, PodLine as Q, KUBE_PROXY_MODES as Qt, LAST_SLACK as R, KindCell as Rt, KindCellRecordV1 as S, sliceEmptyOf as St, KindVerdictV1 as T, transitionOf as Tt, deletionsOf as U, RedisDeclaration as Ut, RunFacts as V, PodSettings as Vt, factsOf as W, Reference as Wt, Endpoint as X, TriggerKind as Xt, unitOf as Y, Trigger as Yt, Line as Z, Workload as Zt, ClusterManifest as _, restartsAt as _t, STAGES as a, cidrRange as an, byNode as at, FORMAT_V1 as b, signalsOf as bt, readRecord as c, kubeProxyPatch as cn, emptyOf as ct, calibrationOf as d, scenarioProblems as dn, kernelCounts as dt, KindScenario as en, Request as et, cellsTable as f, outcomesOf as ft, BandV1 as g, requestsOf as gt, timeSpread as h, recordsOf as ht, quantileOf as i, cidrOverlap as in, byClients as it, judge as j, SERVER as jt, RunFactsV1 as k, PORT as kt, Judged as l, nodeNames as ln, failuresOf as lt, predictionText as m, podsOf as mt, medianInterval as n, NODE_IMAGES as nn, SliceLine as nt, kindCellRecordSchema as o, goDuration as on, deletionOf as ot, medianText as p, podsAt as pt, referenceOf as q, Stack as qt, middle as r, NodeImage as rn, backendsAt as rt, kindCellRecordV1Schema as s, kindConfig as sn, dropOf as st, RELATIVE as t, KubeProxyMode as tn, RulesLine as tt, TimeSpread as u, scenario as un, intakeOf as ut, EnvironmentManifest as v, rulesOf as vt, KindCellV1 as w, stoppingOf as wt, KindCellRecord as x, sliceDropOf as xt, FORMAT as y, signalOf as yt, Propagated as z, KindRun as zt };
788
+ //# sourceMappingURL=judge-Dwa6MqP4.d.ts.map
@@ -50,6 +50,16 @@ const outcomesOf = (requests) => {
50
50
  for (const request of requests) out[request.o] = (out[request.o] ?? 0) + 1;
51
51
  return out;
52
52
  };
53
+ /**
54
+ * This function orders the replicas by how many clients each one answered last, the most first,
55
+ * then by name. `answers` gives the backend of the last answer of each client, by client. A client
56
+ * of gRPC holds its connection to one replica, thus the first replica is the one whose stop the
57
+ * most clients see.
58
+ */
59
+ const byClients = (replicas, answers) => {
60
+ const count = (pod) => [...answers.values()].filter((backend) => backend === pod).length;
61
+ return [...replicas].sort((a, b) => count(b.pod) - count(a.pod) || a.pod.localeCompare(b.pod));
62
+ };
53
63
  /** This function gives the requests of each node, by the name of the node. */
54
64
  const byNode = (requests) => {
55
65
  const out = /* @__PURE__ */ new Map();
@@ -60,10 +70,18 @@ const byNode = (requests) => {
60
70
  const deletionOf = (lines, pods, after) => podsOf(lines).find((pod) => pods.includes(pod.name) && pod.del && pod.t >= after)?.t ?? null;
61
71
  /** 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
72
  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);
73
+ /**
74
+ * This function gives the line `stopping` of grpc-go in a pod, after a time. The server writes it at
75
+ * SIGTERM. Then GracefulStop closes its listener and sends GOAWAY on each connection.
76
+ */
77
+ const stoppingOf = (lines, pod, after) => lines.find((line) => line.ev === "stopping" && line["p"] === pod && line.t >= after)?.t ?? null;
78
+ /**
79
+ * This function gives when the server of a pod stopped its intake. That is the start of the phase
80
+ * `stop intake` of the service, or the line `stopping` of grpc-go.
81
+ */
82
+ const intakeOf = (lines, pod, after) => transitionOf(lines, pod, "drain", (record) => record.to === "stop intake", after) ?? stoppingOf(lines, pod, after);
83
+ /** This function gives when the server of a pod received its exit signal: a record of the service, or the line `stopping` of grpc-go. */
84
+ const signalOf = (lines, pod, after) => transitionOf(lines, pod, "exit", (record) => record.on === "exit.request", after) ?? stoppingOf(lines, pod, after);
67
85
  /**
68
86
  * This function gives when the rules of a node stopped the selection of a backend: the first poll
69
87
  * after a time without it. The change came after `prev`, the poll before it, and at the latest at `t`.
@@ -199,27 +217,29 @@ const reusesReset = (request, requests, reuse) => {
199
217
  const life = valueIn(reuse.life, "ms") + NOISE;
200
218
  return requests.some((earlier) => earlier.o === "reset" && earlier.p === request.p && earlier.lp === request.lp && earlier.t + earlier.d <= request.t && request.t - (earlier.t + earlier.d) <= life);
201
219
  };
202
- /** This function gives a reference event of a run, as a stamp, or null when the run does not have it. */
203
- const referenceOf = (run, from) => {
220
+ /**
221
+ * This function gives a reference event of a run, as a stamp, or null when the run does not have it.
222
+ * The references `deletion` and `intake` are those of the target, or of the replica `pod`.
223
+ */
224
+ const referenceOf = (run, from, pod = run.target?.pod) => {
204
225
  const lines = run.lines;
205
226
  if (from === "fault") return run.acts.fault;
206
227
  if (from === "resume") return run.acts.resume;
207
- if (run.target === null) return null;
208
- const deletion = deletionOf(lines, [run.target.pod], run.start);
209
- if (from === "deletion") return deletion;
210
- return intakeOf(lines, run.target.pod, run.start);
228
+ if (pod === void 0) return null;
229
+ if (from === "deletion") return deletionOf(lines, [pod], run.start);
230
+ return intakeOf(lines, pod, run.start);
211
231
  };
212
232
  /**
213
- * This function gives the propagation of each node for the target of a run. It is the time from
214
- * its deletion (or its crash) to the first poll of the rules of the node without it. The change
215
- * came between the poll before (`after`) and that poll (`by`).
233
+ * This function gives the propagation of each node for the target of a run, or for the replica of
234
+ * the address `ip`. It is the time from its deletion (or its crash) to the first poll of the rules
235
+ * of the node without it. The change came between the poll before (`after`) and that poll (`by`).
216
236
  */
217
- const propagationOf = (run, from) => {
237
+ const propagationOf = (run, from, ip = run.target?.ip) => {
218
238
  const out = {};
219
- if (run.target === null || from === null) return out;
239
+ if (ip === void 0 || from === null) return out;
220
240
  const nodes = [...new Set(run.lines.filter((line) => line.ev === "rules").map((line) => String(line["node"])))].sort();
221
241
  for (const node of nodes) {
222
- const drop = dropOf(run.lines, node, `${run.target.ip}:${PORT}`, from);
242
+ const drop = dropOf(run.lines, node, `${ip}:${PORT}`, from);
223
243
  out[node] = drop === null ? null : {
224
244
  after: drop.prev === null ? null : seconds(sec(Math.max(0, drop.prev - from))),
225
245
  by: seconds(sec(drop.t - from))
@@ -227,6 +247,20 @@ const propagationOf = (run, from) => {
227
247
  }
228
248
  return out;
229
249
  };
250
+ /**
251
+ * This function gives each replica of a run that got a deletion after the start of the run, in the
252
+ * sequence of the deletions. A rolling update deletes each old replica in turn.
253
+ */
254
+ const deletionsOf = (run) => run.replicas.flatMap((replica) => {
255
+ const deletion = referenceOf(run, "deletion", replica.pod);
256
+ return deletion === null ? [] : [{
257
+ pod: replica.pod,
258
+ intake: referenceOf(run, "intake", replica.pod),
259
+ deletion
260
+ }];
261
+ }).sort((a, b) => a.deletion - b.deletion);
262
+ /** This function tells if a prediction is a window for each deleted replica. */
263
+ const eachOf = (prediction) => prediction.kind === "window" && prediction.each === true;
230
264
  /** This function gives the facts of a run that each table reads: its failures by outcome and node, and the times of its target. */
231
265
  const factsOf = (cell, run) => {
232
266
  const lines = run.lines;
@@ -247,7 +281,8 @@ const factsOf = (cell, run) => {
247
281
  const at = sliceDropOf(lines, "lab", run.target.ip, from);
248
282
  return at === null ? null : sec(at - from);
249
283
  })()),
250
- propagation: propagationOf(run, from)
284
+ propagation: propagationOf(run, from),
285
+ ...eachOf(cell.prediction) ? { deletions: deletionsOf(run) } : {}
251
286
  };
252
287
  };
253
288
  /** This function judges one run against the prediction of its cell. */
@@ -277,46 +312,64 @@ const checkRun = (cell, run) => {
277
312
  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);
278
313
  }
279
314
  case "window": {
280
- const start = referenceOf(run, prediction.start.from);
281
- const end = prediction.end === null ? null : referenceOf(run, prediction.end.from);
282
- 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);
315
+ const each = prediction.each === true;
283
316
  const opens = secondsOf(prediction.start.band);
284
317
  const closes = prediction.end === null ? null : secondsOf(prediction.end.band);
285
- const lo = start + opens.lo * 1e3 - NOISE;
286
- const hi = end === null ? Number.POSITIVE_INFINITY : end + closes.hi * 1e3 + NOISE;
318
+ const droppedAfter = (origin, propagation) => (node) => {
319
+ const propagated = propagation[node];
320
+ return origin === null || propagated === void 0 || propagated === null ? null : origin + valueIn(propagated.by, "ms");
321
+ };
322
+ const pods = each ? (facts.deletions ?? []).map((deleted) => deleted.pod) : [run.target?.pod];
323
+ if (pods.length === 0) return done(false, "no replica of the run got a deletion", null);
324
+ const windows = [];
325
+ for (const pod of pods) {
326
+ const start = referenceOf(run, prediction.start.from, pod);
327
+ const end = prediction.end === null ? null : referenceOf(run, prediction.end.from, pod);
328
+ if (start === null || prediction.end !== null && end === null) return done(false, `the run has no ${start === null ? prediction.start.from : prediction.end.from} reference${each ? ` for ${pod}` : ""}`, null);
329
+ const origin = each ? referenceOf(run, "deletion", pod) : facts.deletion ?? (cell.trigger.kind === "crash" ? run.acts.fault : null);
330
+ const ip = run.replicas.find((replica) => replica.pod === pod)?.ip;
331
+ windows.push({
332
+ pod,
333
+ start,
334
+ end,
335
+ lo: start + opens.lo * 1e3 - NOISE,
336
+ hi: end === null ? Number.POSITIVE_INFINITY : end + closes.hi * 1e3 + NOISE,
337
+ dropped: droppedAfter(origin, each ? propagationOf(run, origin, ip) : facts.propagation)
338
+ });
339
+ }
287
340
  const relevant = prediction.nodes === "paused" ? failures.filter((request) => request.n === run.paused) : failures;
288
341
  const others = prediction.nodes === "paused" ? failures.filter((request) => request.n !== run.paused) : [];
289
342
  const reuse = prediction.afterReset;
290
- const late = relevant.filter((request) => request.t < lo || request.t > hi);
343
+ const late = relevant.filter((request) => windows.every((window) => request.t < window.lo || request.t > window.hi));
291
344
  const reused = reuse === void 0 ? [] : late.filter((request) => reusesReset(request, failures, reuse));
292
- const origin = facts.deletion ?? (cell.trigger.kind === "crash" ? run.acts.fault : null);
293
- const dropped = (node) => {
294
- const propagated = facts.propagation[node];
295
- return origin === null || propagated === void 0 || propagated === null ? null : origin + valueIn(propagated.by, "ms");
296
- };
297
- const tail = late.filter((request) => {
298
- const drop = request.t > hi && !reused.includes(request) ? dropped(request.n) : null;
345
+ const tailOf = (request) => reused.includes(request) ? void 0 : windows.find((window) => {
346
+ const drop = request.t > window.hi ? window.dropped(request.n) : null;
299
347
  return drop !== null && request.t <= drop + NOISE;
300
348
  });
349
+ const tail = late.filter((request) => tailOf(request) !== void 0);
301
350
  const outside = late.filter((request) => !reused.includes(request) && !tail.includes(request));
351
+ const bounds = (window) => `[${sec(window.lo - window.start)}, ${sec(window.hi - window.start)}] s`;
302
352
  const problems = [];
303
- if (outside.length > 0) problems.push(`${outside.length} failed requests outside [${sec(lo - start)}, ${sec(hi - start)}] s after ${prediction.start.from}`);
353
+ if (outside.length > 0) problems.push(each ? `${outside.length} failed requests outside the window of each deleted replica: ${windows.map((window) => `${window.pod} ${bounds(window)}`).join(", ")} after its ${prediction.start.from}` : `${outside.length} failed requests outside ${bounds(windows[0])} after ${prediction.start.from}`);
304
354
  if (others.length > 0) problems.push(`${others.length} failed requests on nodes whose kube-proxy did not pause`);
305
355
  if (prediction.certain && relevant.length === 0) problems.push("no request failed in a window that the law opens");
306
- if (prediction.certain && end !== null && relevant.length > 0) {
356
+ const final = windows.reduce((a, b) => (b.end ?? Number.NEGATIVE_INFINITY) > (a.end ?? Number.NEGATIVE_INFINITY) ? b : a);
357
+ if (prediction.certain && final.end !== null && relevant.length > 0) {
307
358
  const last = relevant.at(-1).t;
308
- const earliest = end + closes.lo * 1e3 - NOISE - SLACK;
359
+ const earliest = final.end + closes.lo * 1e3 - NOISE - SLACK;
309
360
  if (last < earliest) problems.push(`the last failed request started ${sec(earliest - last)} s before the window can end`);
310
361
  }
311
362
  const inside = relevant.filter((request) => !late.includes(request));
312
363
  const span = inside.length === 0 ? 0 : sec(inside.at(-1).t - inside[0].t);
313
- const agrees = reused.length === 0 ? `${relevant.length} failed requests, all in the window (span ${span} s)` : `${inside.length} failed requests in the window (span ${span} s), and ${reused.length} on a port that a reset closed`;
364
+ const where = each ? `the windows of ${windows.length} deleted replicas` : "the window";
365
+ const agrees = reused.length === 0 ? `${relevant.length} failed requests, all in ${where} (span ${span} s)` : `${inside.length} failed requests in ${where} (span ${span} s), and ${reused.length} on a port that a reset closed`;
314
366
  const checked = done(problems.length === 0, problems.length === 0 ? agrees : problems.join("; "), relevant.length);
315
367
  if (problems.length > 0 || tail.length === 0) return checked;
316
368
  const last = tail.at(-1);
369
+ const after = tailOf(last);
317
370
  return {
318
371
  ...checked,
319
- tail: `${tail.length} failed requests after the window, the last ${sec(last.t - hi + NOISE)} s after it on ${last.n}, before the rules of ${last.n} dropped the target`
372
+ tail: each ? `${tail.length} failed requests after a window, the last ${sec(last.t - after.hi + NOISE)} s after the window of ${after.pod} on ${last.n}, before the rules of ${last.n} dropped ${after.pod}` : `${tail.length} failed requests after the window, the last ${sec(last.t - after.hi + NOISE)} s after it on ${last.n}, before the rules of ${last.n} dropped the target`
320
373
  };
321
374
  }
322
375
  case "removal": {
@@ -673,7 +726,7 @@ const atBand = (from, band) => {
673
726
  const predictionText = (prediction) => {
674
727
  switch (prediction.kind) {
675
728
  case "none": return prediction.outcomes === void 0 ? "no failure" : `no ${prediction.outcomes.join(", ")}`;
676
- case "window": return `failures only from ${atBand(prediction.start.from, prediction.start.band)}${prediction.end === null ? "" : ` to ${atBand(prediction.end.from, prediction.end.band)}`}${prediction.nodes === "paused" ? ", on the paused node" : ""}${prediction.certain ? ", at least one" : ""}${prediction.afterReset === void 0 ? "" : `, or a ${prediction.afterReset.outcome} up to ${format(prediction.afterReset.life)} after a reset on its port`}`;
729
+ case "window": return `failures only from ${atBand(prediction.start.from, prediction.start.band)}${prediction.end === null ? "" : ` to ${atBand(prediction.end.from, prediction.end.band)}`}${prediction.each === true ? ", of each deleted replica" : ""}${prediction.nodes === "paused" ? ", on the paused node" : ""}${prediction.certain ? ", at least one" : ""}${prediction.afterReset === void 0 ? "" : `, or a ${prediction.afterReset.outcome} up to ${format(prediction.afterReset.life)} after a reset on its port`}`;
677
730
  case "removal": return `Service empty in ${showBand(prediction.band)}`;
678
731
  case "leaves": return `the replica leaves in ${showBand(prediction.band)}`;
679
732
  case "restarts": return prediction.band === null ? "no restart" : `each replica SIGTERM in ${showBand(prediction.band)}`;
@@ -930,6 +983,7 @@ const recordOf = (a, main) => {
930
983
  life: a,
931
984
  outcome: z.string()
932
985
  }).optional(),
986
+ each: z.boolean().optional(),
933
987
  why: z.string()
934
988
  }),
935
989
  z.object({
@@ -964,6 +1018,7 @@ const recordOf = (a, main) => {
964
1018
  id: z.string(),
965
1019
  name: z.string(),
966
1020
  template: z.enum(["T07", "T18"]),
1021
+ stack: z.enum(["service", "grpc"]).optional(),
967
1022
  substitutes: z.string().nullable(),
968
1023
  mode: z.enum(MODES),
969
1024
  workers: count,
@@ -984,7 +1039,7 @@ const recordOf = (a, main) => {
984
1039
  liveness: probe,
985
1040
  startup: probe.nullable()
986
1041
  }),
987
- declaration,
1042
+ declaration: declaration.nullable(),
988
1043
  workload: z.object({
989
1044
  every: time,
990
1045
  timeout: time
@@ -1009,7 +1064,12 @@ const recordOf = (a, main) => {
1009
1064
  sliceEmpty: a.optional(),
1010
1065
  rules: z.record(z.string(), a.nullable()).optional(),
1011
1066
  left: a.optional(),
1012
- signals: z.array(a.nullable()).optional()
1067
+ signals: z.array(a.nullable()).optional(),
1068
+ windows: z.array(z.object({
1069
+ pod: z.string(),
1070
+ intake: z.number().nullable(),
1071
+ deletion: z.number()
1072
+ })).optional()
1013
1073
  });
1014
1074
  return {
1015
1075
  cell,
@@ -1071,6 +1131,6 @@ const readRecord = (data) => {
1071
1131
  throw new Error(`a kind cell record has the format ${FORMAT} or ${FORMAT_V1}, not ${String(format)}`);
1072
1132
  };
1073
1133
  //#endregion
1074
- export { REDIS as $, deletionOf as A, requestsOf as B, unitOf as C, quantileOf as D, middle as E, kernelCounts as F, sliceDropOf as G, rulesOf as H, outcomesOf as I, transitionOf as J, sliceEmptyOf as K, podsAt as L, emptyOf as M, failuresOf as N, backendsAt as O, intakeOf as P, PORT as Q, podsOf as R, reusesReset as S, medianInterval as T, signalOf as U, restartsAt as V, signalsOf as W, HEALTH_PORT as X, CLIENT as Y, LOG_LABEL as Z, checkRun as _, calibrationOf as a, propagationOf as b, predictionText as c, FORMAT_V1 as d, SERVER as et, judge as f, LAST_SLACK as g, KIND_NOISE as h, readRecord as i, showBand as it, dropOf as j, byNode as k, timeSpread as l, upgradeCell as m, kindCellRecordSchema as n, seconds as nt, cellsTable as o, upgrade as p, slicesOf as q, kindCellRecordV1Schema as r, secondsOf as rt, medianText as s, STAGES as t, bandOf as tt, FORMAT as u, factsOf as v, RELATIVE as w, referenceOf as x, judgeKind as y, recordsOf as z };
1134
+ export { HEALTH_PORT as $, byClients as A, podsOf as B, reusesReset as C, middle as D, medianInterval as E, failuresOf as F, signalOf as G, requestsOf as H, intakeOf as I, sliceEmptyOf as J, signalsOf as K, kernelCounts as L, deletionOf as M, dropOf as N, quantileOf as O, emptyOf as P, CLIENT as Q, outcomesOf as R, referenceOf as S, RELATIVE as T, restartsAt as U, recordsOf as V, rulesOf as W, stoppingOf as X, slicesOf as Y, transitionOf as Z, checkRun as _, calibrationOf as a, seconds as at, judgeKind as b, predictionText as c, FORMAT_V1 as d, LOG_LABEL as et, judge as f, LAST_SLACK as g, KIND_NOISE as h, readRecord as i, bandOf as it, byNode as j, backendsAt as k, timeSpread as l, upgradeCell as m, kindCellRecordSchema as n, REDIS as nt, cellsTable as o, secondsOf as ot, upgrade as p, sliceDropOf as q, kindCellRecordV1Schema as r, SERVER as rt, medianText as s, showBand as st, STAGES as t, PORT as tt, FORMAT as u, deletionsOf as v, unitOf as w, propagationOf as x, factsOf as y, podsAt as z };
1075
1135
 
1076
- //# sourceMappingURL=judge-BbTAxRSd.js.map
1136
+ //# sourceMappingURL=judge-reWPMVr4.js.map