@mark1russell7/kind 0.5.0 → 0.6.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.
@@ -118,16 +118,17 @@ type Stack = "service" | "grpc";
118
118
  * - `rollout` is a rolling update while Redis is down.
119
119
  * - `cpu-throttle` sets `cpu.max` in the cgroup of one replica.
120
120
  * - `authorization-policy` applies an AuthorizationPolicy of Istio that denies the clients, for the hold (T15).
121
+ * - `idle` stops the calls of each client with SIGUSR1, and starts them again with SIGUSR2 after the hold: a gap (T15).
121
122
  * - `none` injects nothing: a calibration measures the path itself.
122
123
  */
123
- type TriggerKind = "pod-deletion" | "rolling-update" | "node-drain" | "scale-in" | "crash" | "liveness-restart" | "freeze-process" | "rst" | "blackhole" | "peer-gone-without-close" | "app-stall" | "rollout" | "cpu-throttle" | "authorization-policy";
124
+ type TriggerKind = "pod-deletion" | "rolling-update" | "node-drain" | "scale-in" | "crash" | "liveness-restart" | "freeze-process" | "rst" | "blackhole" | "peer-gone-without-close" | "app-stall" | "rollout" | "cpu-throttle" | "authorization-policy" | "idle";
124
125
  /**
125
126
  * A trigger is its kind and the settings of its fault. The parameter `A` is the form of an amount:
126
127
  * a quantity in the format `/2`, a number in the format `/1`.
127
128
  */
128
129
  interface Trigger<A = Quantity> {
129
130
  readonly kind: TriggerKind;
130
- /** This time is how long a fault holds before it ends: a stall, a pause of kube-proxy, a black hole on Redis. */
131
+ /** This time is how long a fault holds before it ends: a stall, a pause of kube-proxy, a black hole, or a gap. */
131
132
  readonly hold?: Quantity | undefined;
132
133
  /** This time is how long before the deletion a pause of kube-proxy starts (T07 `freeze-process`). */
133
134
  readonly lead?: Quantity | undefined;
@@ -245,6 +246,9 @@ interface ResetReuse<A = Quantity> {
245
246
  * - `restarts`: each replica receives SIGTERM in the band, or no replica does.
246
247
  * - `stays`: no endpoint leaves the Service, and no request is refused.
247
248
  * - `stalls`: the rollout does not continue while Redis is down, and it ends after Redis is back.
249
+ * - `ends`: the socket of each client ends in the band after the reference `from`. With no band, no
250
+ * socket of a client ends in the run. Then the first request of each client after the heal goes on
251
+ * the socket of its last request before the fault. With `window`, the failed requests also start in that window.
248
252
  * - `measured`: no document bounds the outcome. The run keeps the measurement.
249
253
  */
250
254
  type Prediction<A = Quantity> = {
@@ -252,7 +256,42 @@ type Prediction<A = Quantity> = {
252
256
  /** These outcomes are the failures that the prediction excludes. Without them, it excludes each outcome but `ok`. */
253
257
  readonly outcomes?: readonly string[] | undefined;
254
258
  readonly why: string;
259
+ } | WindowPrediction<A> | {
260
+ readonly kind: "removal";
261
+ readonly band: Band<A>;
262
+ readonly why: string;
263
+ } | {
264
+ readonly kind: "leaves";
265
+ readonly band: Band<A>;
266
+ readonly why: string;
267
+ } | {
268
+ readonly kind: "restarts";
269
+ readonly band: Band<A> | null;
270
+ readonly why: string;
271
+ } | {
272
+ readonly kind: "stays";
273
+ readonly why: string;
255
274
  } | {
275
+ readonly kind: "stalls";
276
+ readonly why: string;
277
+ } | {
278
+ readonly kind: "ends";
279
+ /** This reference is where the band of each close starts. */
280
+ readonly from: Reference;
281
+ /** This band is the close of the socket of each client after `from`, or null when no socket of a client ends. */
282
+ readonly band: Band<A> | null;
283
+ /** This window is where the failed requests start, or none when the prediction does not test them. */
284
+ readonly window?: WindowPrediction<A> | undefined;
285
+ readonly why: string;
286
+ } | {
287
+ readonly kind: "measured";
288
+ readonly why: string;
289
+ };
290
+ /**
291
+ * A window prediction is the requests that fail in a window. The window starts at `start` and ends in
292
+ * `end`. With `certain`, at least one request fails.
293
+ */
294
+ interface WindowPrediction<A = Quantity> {
256
295
  readonly kind: "window";
257
296
  readonly nodes: "all" | "paused";
258
297
  readonly start: {
@@ -284,28 +323,7 @@ type Prediction<A = Quantity> = {
284
323
  /** These outcomes are the only failures that the window permits. A failure with another outcome fails the run. Without them, each outcome but `ok` can fail. */
285
324
  readonly outcomes?: readonly string[] | undefined;
286
325
  readonly why: string;
287
- } | {
288
- readonly kind: "removal";
289
- readonly band: Band<A>;
290
- readonly why: string;
291
- } | {
292
- readonly kind: "leaves";
293
- readonly band: Band<A>;
294
- readonly why: string;
295
- } | {
296
- readonly kind: "restarts";
297
- readonly band: Band<A> | null;
298
- readonly why: string;
299
- } | {
300
- readonly kind: "stays";
301
- readonly why: string;
302
- } | {
303
- readonly kind: "stalls";
304
- readonly why: string;
305
- } | {
306
- readonly kind: "measured";
307
- readonly why: string;
308
- };
326
+ }
309
327
  /** A replica is one pod of the service at the start of a run. */
310
328
  interface Replica {
311
329
  readonly pod: string;
@@ -498,11 +516,25 @@ interface RecordLine {
498
516
  readonly on: string;
499
517
  readonly d: string | null;
500
518
  }
519
+ /**
520
+ * A socket line is the connect or the close of a socket of the channel of a gRPC client
521
+ * (`client-lines.ts`). It gives the client (`p`) and its node (`n`), and the local port of the socket.
522
+ * A close also gives the code of an error of the socket (`err`), or null for a close with no error.
523
+ */
524
+ interface SocketLine {
525
+ readonly t: number;
526
+ readonly what: "connect" | "close";
527
+ readonly p: string;
528
+ readonly n: string;
529
+ readonly lp: number | null;
530
+ readonly err?: string | null;
531
+ }
501
532
  declare const requestsOf: (lines: readonly Line[]) => Request[];
502
533
  declare const podsOf: (lines: readonly Line[]) => PodLine[];
503
534
  declare const slicesOf: (lines: readonly Line[], svc: string) => SliceLine[];
504
535
  declare const rulesOf: (lines: readonly Line[], source?: PropagationSource) => RulesLine[];
505
536
  declare const recordsOf: (lines: readonly Line[]) => RecordLine[];
537
+ declare const socketsOf: (lines: readonly Line[]) => SocketLine[];
506
538
  /** This function gives the requests that did not succeed, in sequence of their start. */
507
539
  declare const failuresOf: (lines: readonly Line[]) => Request[];
508
540
  /** This function counts the outcomes of the requests, by outcome. */
@@ -610,6 +642,8 @@ interface RunFacts<A = Quantity> {
610
642
  readonly updates?: Readonly<Record<string, Propagated<A> | null>> | undefined;
611
643
  /** These are the first LDS updates of the sidecar of each server pod after the heal, by pod. */
612
644
  readonly healUpdates?: Readonly<Record<string, Propagated<A> | null>> | undefined;
645
+ /** These are the first close of the socket of each client after the reference (`ends`), by client, or null for none. */
646
+ readonly closes?: Readonly<Record<string, A | null>> | undefined;
613
647
  }
614
648
  /** 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. */
615
649
  interface Deleted {
@@ -648,9 +682,10 @@ interface KindVerdict<A = Quantity> {
648
682
  }
649
683
  /**
650
684
  * This function gives the unit of the main value of a cell, from its prediction:
651
- * - `s` for a time: the removal, the departure or the restart of a band, and the end of a stall.
685
+ * - `s` for a time: the removal, the departure or the restart of a band, the end of a stall, or a close.
652
686
  * - `{request}` for the failed requests: no failure, a window, a fleet that stays, and a measurement.
653
687
  * - `{replica}` for the replicas that received SIGTERM, when no replica must restart.
688
+ * - `{socket}` for the sockets of the clients that ended, when no socket must end.
654
689
  */
655
690
  declare const unitOf: (prediction: {
656
691
  readonly kind: Prediction["kind"];
@@ -697,7 +732,7 @@ declare const deletionsOf: (run: KindRun) => Deleted[];
697
732
  * This function gives how long a run observes after the end reference of its last window, or null
698
733
  * when its prediction sets no such time. The judge closes a window `end.band.hi` + δ after its end
699
734
  * reference. A request that starts by then ends at most the timeout of the workload later: the
700
- * client cancels it then. A prediction that is not a window, or a window with no end, sets none.
735
+ * client cancels it then. A prediction with no window, or a window with no end, sets none.
701
736
  */
702
737
  declare const observationAfter: (cell: Pick<KindCell, "prediction" | "workload">) => Quantity | null;
703
738
  /**
@@ -707,6 +742,8 @@ declare const observationAfter: (cell: Pick<KindCell, "prediction" | "workload">
707
742
  * it can miss its own failures. In a rolling update of gRPC, a call waits in the queue of its channel
708
743
  * for a stalled connect. It fails only at its timeout (kind run 40128267098). A reference that is not
709
744
  * in the lines yet does not count.
745
+ *
746
+ * The band of `ends` also sets an end: its reference, the end of the band and δ.
710
747
  */
711
748
  declare const observationEnd: (cell: KindCell, run: Pick<KindRun, "start" | "acts" | "target" | "replicas" | "lines">) => number | null;
712
749
  /** This function gives the facts of a run that each table reads: its failures by outcome and node, and the times of its target. */
@@ -818,7 +855,7 @@ interface TimeSpread {
818
855
  }
819
856
  /** This function gives the spread of a set of times, or null for none. The percentile is by nearest rank. */
820
857
  declare const timeSpread: (times: readonly Quantity[]) => TimeSpread | null;
821
- /** This function gives the prediction of a cell in a short form for a table. */
858
+ /** This function gives the prediction of a cell in a short form for a table. A prediction `ends` gives its window after its band. */
822
859
  declare const predictionText: (prediction: Prediction) => string;
823
860
  /**
824
861
  * This function gives the main measurement of a cell in a short form: its median and what it
@@ -876,5 +913,5 @@ declare const quantileOf: (sorted: readonly number[], p: number) => number;
876
913
  */
877
914
  declare const medianInterval: (sorted: readonly number[]) => [number, number];
878
915
  //#endregion
879
- export { updatesOf as $, Replica as $t, TriggerV1 as A, transitionOf as At, RunCheck as B, showBand as Bt, KindCellResult as C, rulesOf as Ct, PredictionV1 as D, sliceEmptyOf as Dt, LoadSample as E, sliceDropOf as Et, KIND_NOISE as F, REDIS as Ft, judgeKind as G, KubeletProbe as Gt, checkRun as H, Declaration as Ht, KindDecision as I, SERVER as It, propagationOf as J, MeshMode as Jt, observationAfter as K, Mesh as Kt, KindVerdict as L, bandOf as Lt, upgrade as M, HEALTH_PORT as Mt, upgradeCell as N, LOG_LABEL as Nt, RunCheckV1 as O, slicesOf as Ot, Deleted as P, PORT as Pt, unitOf as Q, Reference as Qt, LAST_SLACK as R, seconds as Rt, KindCellRecordV1 as S, restartsAt as St, KindVerdictV1 as T, signalsOf as Tt, deletionsOf as U, KindCell as Ut, RunFacts as V, Band as Vt, factsOf as W, KindRun as Wt, referenceOf as X, Prediction as Xt, propagationSource as Y, PodSettings as Yt, reusesReset as Z, RedisDeclaration as Zt, ClusterManifest as _, nodeNames as _n, outcomesOf as _t, STAGES as a, Workload as an, Request as at, FORMAT_V1 as b, recordsOf as bt, readRecord as c, KindScenario as cn, backendsAt as ct, calibrationOf as d, NodeImage as dn, deletionOf as dt, ResetReuse as en, Endpoint as et, cellsTable as f, cidrOverlap as fn, dropOf as ft, BandV1 as g, kubeProxyPatch as gn, kernelCounts as gt, timeSpread as h, kindConfig as hn, intakeOf as ht, quantileOf as i, TriggerKind as in, RecordLine as it, judge as j, CLIENT as jt, RunFactsV1 as k, stoppingOf as kt, Judged as l, KubeProxyMode as ln, byClients as lt, predictionText as m, goDuration as mn, failuresOf as mt, medianInterval as n, Template as nn, PodLine as nt, kindCellRecordSchema as o, KUBE_PROXY_MODES as on, RulesLine as ot, medianText as p, cidrRange as pn, emptyOf as pt, observationEnd as q, MeshFlavor as qt, middle as r, Trigger as rn, PropagationSource as rt, kindCellRecordV1Schema as s, KindConfig as sn, SliceLine as st, RELATIVE as t, Stack as tn, Line as tt, TimeSpread as u, NODE_IMAGES as un, byNode as ut, EnvironmentManifest as v, scenario as vn, podsAt as vt, KindCellV1 as w, signalOf as wt, KindCellRecord as x, requestsOf as xt, FORMAT as y, scenarioProblems as yn, podsOf as yt, Propagated as z, secondsOf as zt };
880
- //# sourceMappingURL=judge-BS9pk96o.d.ts.map
916
+ export { updatesOf as $, RedisDeclaration as $t, TriggerV1 as A, socketsOf as At, RunCheck as B, seconds as Bt, KindCellResult as C, restartsAt as Ct, PredictionV1 as D, sliceDropOf as Dt, LoadSample as E, signalsOf as Et, KIND_NOISE as F, LOG_LABEL as Ft, judgeKind as G, KindCell as Gt, checkRun as H, showBand as Ht, KindDecision as I, PORT as It, propagationOf as J, Mesh as Jt, observationAfter as K, KindRun as Kt, KindVerdict as L, REDIS as Lt, upgrade as M, transitionOf as Mt, upgradeCell as N, CLIENT as Nt, RunCheckV1 as O, sliceEmptyOf as Ot, Deleted as P, HEALTH_PORT as Pt, unitOf as Q, Prediction as Qt, LAST_SLACK as R, SERVER as Rt, KindCellRecordV1 as S, scenarioProblems as Sn, requestsOf as St, KindVerdictV1 as T, signalOf as Tt, deletionsOf as U, Band as Ut, RunFacts as V, secondsOf as Vt, factsOf as W, Declaration as Wt, referenceOf as X, MeshMode as Xt, propagationSource as Y, MeshFlavor as Yt, reusesReset as Z, PodSettings as Zt, ClusterManifest as _, goDuration as _n, kernelCounts as _t, STAGES as a, Trigger as an, Request as at, FORMAT_V1 as b, nodeNames as bn, podsOf as bt, readRecord as c, Workload as cn, SocketLine as ct, calibrationOf as d, KindScenario as dn, byNode as dt, Reference as en, Endpoint as et, cellsTable as f, KubeProxyMode as fn, deletionOf as ft, BandV1 as g, cidrRange as gn, intakeOf as gt, timeSpread as h, cidrOverlap as hn, failuresOf as ht, quantileOf as i, Template as in, RecordLine as it, judge as j, stoppingOf as jt, RunFactsV1 as k, slicesOf as kt, Judged as l, KUBE_PROXY_MODES as ln, backendsAt as lt, predictionText as m, NodeImage as mn, emptyOf as mt, medianInterval as n, ResetReuse as nn, PodLine as nt, kindCellRecordSchema as o, TriggerKind as on, RulesLine as ot, medianText as p, NODE_IMAGES as pn, dropOf as pt, observationEnd as q, KubeletProbe as qt, middle as r, Stack as rn, PropagationSource as rt, kindCellRecordV1Schema as s, WindowPrediction as sn, SliceLine as st, RELATIVE as t, Replica as tn, Line as tt, TimeSpread as u, KindConfig as un, byClients as ut, EnvironmentManifest as v, kindConfig as vn, outcomesOf as vt, KindCellV1 as w, rulesOf as wt, KindCellRecord as x, scenario as xn, recordsOf as xt, FORMAT as y, kubeProxyPatch as yn, podsAt as yt, Propagated as z, bandOf as zt };
917
+ //# sourceMappingURL=judge-B9kVpFE-.d.ts.map
@@ -42,6 +42,7 @@ const podsOf = (lines) => of(lines, "pod").sort((a, b) => a.t - b.t);
42
42
  const slicesOf = (lines, svc) => of(lines, "slice").filter((line) => line.svc === svc);
43
43
  const rulesOf = (lines, source = "rules") => of(lines, source).sort((a, b) => a.t - b.t);
44
44
  const recordsOf = (lines) => of(lines, "rec");
45
+ const socketsOf = (lines) => of(lines, "sock").sort((a, b) => a.t - b.t);
45
46
  /** This function gives the requests that did not succeed, in sequence of their start. */
46
47
  const failuresOf = (lines) => requestsOf(lines).filter((request) => request.o !== "ok").sort((a, b) => a.t - b.t);
47
48
  /** This function counts the outcomes of the requests, by outcome. */
@@ -192,9 +193,10 @@ const NOISE = valueIn(KIND_NOISE, "ms");
192
193
  const SLACK = valueIn(LAST_SLACK, "ms");
193
194
  /**
194
195
  * This function gives the unit of the main value of a cell, from its prediction:
195
- * - `s` for a time: the removal, the departure or the restart of a band, and the end of a stall.
196
+ * - `s` for a time: the removal, the departure or the restart of a band, the end of a stall, or a close.
196
197
  * - `{request}` for the failed requests: no failure, a window, a fleet that stays, and a measurement.
197
198
  * - `{replica}` for the replicas that received SIGTERM, when no replica must restart.
199
+ * - `{socket}` for the sockets of the clients that ended, when no socket must end.
198
200
  */
199
201
  const unitOf = (prediction) => {
200
202
  switch (prediction.kind) {
@@ -202,6 +204,7 @@ const unitOf = (prediction) => {
202
204
  case "leaves":
203
205
  case "stalls": return "s";
204
206
  case "restarts": return prediction.band === null ? "{replica}" : "s";
207
+ case "ends": return prediction.band === null ? "{socket}" : "s";
205
208
  default: return "{request}";
206
209
  }
207
210
  };
@@ -296,16 +299,18 @@ const deletionsOf = (run) => run.replicas.flatMap((replica) => {
296
299
  }).sort((a, b) => a.deletion - b.deletion);
297
300
  /** This function tells if a prediction is a window for each deleted replica. */
298
301
  const eachOf = (prediction) => prediction.kind === "window" && prediction.each === true;
302
+ /** This function gives the window of the failed requests of a prediction: a window, the window of `ends`, or null. */
303
+ const windowOf = (prediction) => prediction.kind === "window" ? prediction : prediction.kind === "ends" ? prediction.window ?? null : null;
299
304
  /**
300
305
  * This function gives how long a run observes after the end reference of its last window, or null
301
306
  * when its prediction sets no such time. The judge closes a window `end.band.hi` + δ after its end
302
307
  * reference. A request that starts by then ends at most the timeout of the workload later: the
303
- * client cancels it then. A prediction that is not a window, or a window with no end, sets none.
308
+ * client cancels it then. A prediction with no window, or a window with no end, sets none.
304
309
  */
305
310
  const observationAfter = (cell) => {
306
- const prediction = cell.prediction;
307
- if (prediction.kind !== "window" || prediction.end === null) return null;
308
- return seconds(sec(valueIn(prediction.end.band.hi, "ms") + valueIn(cell.workload.timeout, "ms") + NOISE));
311
+ const window = windowOf(cell.prediction);
312
+ if (window === null || window.end === null) return null;
313
+ return seconds(sec(valueIn(window.end.band.hi, "ms") + valueIn(cell.workload.timeout, "ms") + NOISE));
309
314
  };
310
315
  /**
311
316
  * This function gives the earliest end of the observation of a run, as a stamp, or null when its
@@ -314,17 +319,27 @@ const observationAfter = (cell) => {
314
319
  * it can miss its own failures. In a rolling update of gRPC, a call waits in the queue of its channel
315
320
  * for a stalled connect. It fails only at its timeout (kind run 40128267098). A reference that is not
316
321
  * in the lines yet does not count.
322
+ *
323
+ * The band of `ends` also sets an end: its reference, the end of the band and δ.
317
324
  */
318
325
  const observationEnd = (cell, run) => {
319
- const after = observationAfter(cell);
326
+ const ends = [];
320
327
  const prediction = cell.prediction;
321
- if (after === null || prediction.kind !== "window" || prediction.end === null) return null;
322
- const from = prediction.end.from;
323
- const references = (prediction.each === true ? run.replicas.map((replica) => replica.pod) : [run.target?.pod]).flatMap((pod) => {
324
- const at = referenceOf(run, from, pod);
325
- return at === null ? [] : [at];
326
- });
327
- return references.length === 0 ? null : Math.max(...references) + valueIn(after, "ms");
328
+ if (prediction.kind === "ends" && prediction.band !== null) {
329
+ const at = referenceOf(run, prediction.from);
330
+ if (at !== null) ends.push(at + valueIn(prediction.band.hi, "ms") + NOISE);
331
+ }
332
+ const after = observationAfter(cell);
333
+ const window = windowOf(prediction);
334
+ if (after !== null && window !== null && window.end !== null) {
335
+ const from = window.end.from;
336
+ const references = (window.each === true ? run.replicas.map((replica) => replica.pod) : [run.target?.pod]).flatMap((pod) => {
337
+ const at = referenceOf(run, from, pod);
338
+ return at === null ? [] : [at];
339
+ });
340
+ if (references.length > 0) ends.push(Math.max(...references) + valueIn(after, "ms"));
341
+ }
342
+ return ends.length === 0 ? null : Math.max(...ends);
328
343
  };
329
344
  /** This function gives the facts of a run that each table reads: its failures by outcome and node, and the times of its target. */
330
345
  const factsOf = (cell, run) => {
@@ -599,9 +614,83 @@ const checkRun = (cell, run) => {
599
614
  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));
600
615
  return done(false, problems.join("; "), null);
601
616
  }
617
+ case "ends": return checkEnds(cell, run, prediction, done);
602
618
  case "measured": return done(true, "measured", facts.failures);
603
619
  }
604
620
  };
621
+ /**
622
+ * This function judges a run against a prediction `ends`. Each client with an answer before the
623
+ * reference is a client of the run. Its socket is the port of that answer (`lp`).
624
+ * - With a band, the first close of a socket of each client after the reference must be in the band
625
+ * ± δ. It must be the close of that socket. The main value is the middle of these times.
626
+ * - With no band, no socket of a client can close after the reference. The first request of each client
627
+ * after the heal must be `ok` on that socket. The main value is the count of the closes.
628
+ *
629
+ * With a window, the failed requests of the run must also agree with that window (`checkRun`).
630
+ */
631
+ const checkEnds = (cell, run, prediction, done) => {
632
+ const from = referenceOf(run, prediction.from);
633
+ if (from === null) return done(false, `the run has no ${prediction.from} reference`, null);
634
+ const lines = run.lines;
635
+ const calls = requestsOf(lines).filter((request) => request.t >= run.start && request.t <= run.end).sort((a, b) => a.t - b.t);
636
+ const last = /* @__PURE__ */ new Map();
637
+ for (const request of calls) if (request.t < from && request.o === "ok") last.set(request.p, request);
638
+ if (last.size === 0) return done(false, `no client had an answer before the ${prediction.from}`, null);
639
+ const clients = [...last.keys()].sort();
640
+ const shut = socketsOf(lines).filter((line) => line.what === "close" && line.t >= from && line.t <= run.end);
641
+ const closes = {};
642
+ const times = [];
643
+ const problems = [];
644
+ for (const client of clients) {
645
+ const close = shut.find((line) => line.p === client);
646
+ closes[client] = close === void 0 ? null : seconds(sec(close.t - from));
647
+ if (close !== void 0) times.push(sec(close.t - from));
648
+ }
649
+ let agrees;
650
+ let value;
651
+ if (prediction.band !== null) {
652
+ const band = secondsOf(prediction.band);
653
+ for (const client of clients) {
654
+ const close = shut.find((line) => line.p === client);
655
+ const port = last.get(client).lp ?? null;
656
+ if (close === void 0) problems.push(`the socket of ${client} did not end`);
657
+ else if (port !== null && close.lp !== null && close.lp !== port) problems.push(`the first socket of ${client} to end had the port ${close.lp}, not the port ${port} of its last answer`);
658
+ else if (!inBand(sec(close.t - from), band)) problems.push(`the socket of ${client} ended at ${sec(close.t - from)} s`);
659
+ }
660
+ const sorted = [...times].sort((a, b) => a - b);
661
+ value = sorted.length === 0 ? null : middle(sorted);
662
+ agrees = `the socket of each of ${clients.length} clients ended, at ${sorted.join(", ")} s`;
663
+ } else {
664
+ for (const client of clients) {
665
+ const close = shut.find((line) => line.p === client);
666
+ if (close !== void 0) problems.push(`the socket of ${client} ended ${sec(close.t - from)} s after the ${prediction.from}`);
667
+ }
668
+ const resumed = run.acts.heal ?? from;
669
+ for (const client of clients) {
670
+ const port = last.get(client).lp ?? null;
671
+ const next = calls.find((request) => request.p === client && request.t >= resumed);
672
+ if (next === void 0) problems.push(`${client} sent no call after the heal`);
673
+ else if (next.o !== "ok") problems.push(`the first call of ${client} after the heal ended ${next.o}`);
674
+ else if ((next.lp ?? null) !== port) problems.push(`the first call of ${client} after the heal went on the port ${next.lp ?? "none"}, not ${port ?? "none"}`);
675
+ }
676
+ value = times.length;
677
+ agrees = `no socket of ${clients.length} clients ended, and the first call of each one after the heal went on its socket`;
678
+ }
679
+ const inner = prediction.window === void 0 ? null : checkRun({
680
+ ...cell,
681
+ prediction: prediction.window
682
+ }, run);
683
+ if (inner !== null && !inner.ok) problems.push(inner.why);
684
+ const why = problems.length > 0 ? problems.join("; ") : inner === null ? agrees : `${agrees}; ${inner.why}`;
685
+ const checked = done(problems.length === 0, why, value, {
686
+ ...inner === null ? {} : inner.facts,
687
+ closes
688
+ });
689
+ return inner?.tail === void 0 ? checked : {
690
+ ...checked,
691
+ tail: inner.tail
692
+ };
693
+ };
605
694
  /** This function judges a cell on its runs: each counted run must agree, and a median in a band must be equivalent to it. */
606
695
  const judgeKind = (cell, runs) => {
607
696
  const counted = runs.filter((run) => !run.warmup && run.void === null);
@@ -641,7 +730,7 @@ const judgeKind = (cell, runs) => {
641
730
  checks
642
731
  };
643
732
  const prediction = cell.prediction;
644
- const band = prediction.kind === "removal" || prediction.kind === "leaves" || prediction.kind === "restarts" ? prediction.band : null;
733
+ const band = prediction.kind === "removal" || prediction.kind === "leaves" || prediction.kind === "restarts" || prediction.kind === "ends" ? prediction.band : null;
645
734
  if (failed > 0) {
646
735
  const first = checks.find((check) => !check.ok);
647
736
  return {
@@ -731,25 +820,34 @@ const upgradeTrigger = (trigger) => {
731
820
  }
732
821
  };
733
822
  };
823
+ const upgradeWindow = (window) => {
824
+ const { afterReset, propagation, ...rest } = window;
825
+ return {
826
+ ...rest,
827
+ start: {
828
+ from: window.start.from,
829
+ band: band(window.start.band)
830
+ },
831
+ end: window.end === null ? null : {
832
+ from: window.end.from,
833
+ band: band(window.end.band)
834
+ },
835
+ ...afterReset === void 0 ? {} : { afterReset: {
836
+ life: seconds(afterReset.life),
837
+ outcome: afterReset.outcome
838
+ } },
839
+ ...propagation === void 0 ? {} : { propagation: band(propagation) }
840
+ };
841
+ };
734
842
  const upgradePrediction = (prediction) => {
735
843
  switch (prediction.kind) {
736
- case "window": {
737
- const { afterReset, propagation, ...rest } = prediction;
844
+ case "window": return upgradeWindow(prediction);
845
+ case "ends": {
846
+ const { window, ...rest } = prediction;
738
847
  return {
739
848
  ...rest,
740
- start: {
741
- from: prediction.start.from,
742
- band: band(prediction.start.band)
743
- },
744
- end: prediction.end === null ? null : {
745
- from: prediction.end.from,
746
- band: band(prediction.end.band)
747
- },
748
- ...afterReset === void 0 ? {} : { afterReset: {
749
- life: seconds(afterReset.life),
750
- outcome: afterReset.outcome
751
- } },
752
- ...propagation === void 0 ? {} : { propagation: band(propagation) }
849
+ band: prediction.band === null ? null : band(prediction.band),
850
+ ...window === void 0 ? {} : { window: upgradeWindow(window) }
753
851
  };
754
852
  }
755
853
  case "removal":
@@ -776,7 +874,7 @@ const upgradeFacts = (facts) => {
776
874
  by: seconds(old.by)
777
875
  };
778
876
  const byKey = (record) => Object.fromEntries(Object.entries(record).map(([key, old]) => [key, propagated(old)]));
779
- const { sliceEmpty, rules, left, signals, updates, healUpdates, ...rest } = facts;
877
+ const { sliceEmpty, rules, left, signals, updates, healUpdates, closes, ...rest } = facts;
780
878
  return {
781
879
  ...rest,
782
880
  intakeAfterDeletion: time$1(facts.intakeAfterDeletion),
@@ -787,7 +885,8 @@ const upgradeFacts = (facts) => {
787
885
  ...sliceEmpty === void 0 ? {} : { sliceEmpty: seconds(sliceEmpty) },
788
886
  ...rules === void 0 ? {} : { rules: Object.fromEntries(Object.entries(rules).map(([node, old]) => [node, time$1(old)])) },
789
887
  ...left === void 0 ? {} : { left: seconds(left) },
790
- ...signals === void 0 ? {} : { signals: signals.map((old) => time$1(old)) }
888
+ ...signals === void 0 ? {} : { signals: signals.map((old) => time$1(old)) },
889
+ ...closes === void 0 ? {} : { closes: Object.fromEntries(Object.entries(closes).map(([client, old]) => [client, time$1(old)])) }
791
890
  };
792
891
  };
793
892
  /**
@@ -853,7 +952,7 @@ const atBand = (from, band) => {
853
952
  const { lo, hi } = secondsOf(band);
854
953
  return lo === 0 && hi === 0 ? `the ${from}` : `the ${from} + ${showBand(band)}`;
855
954
  };
856
- /** This function gives the prediction of a cell in a short form for a table. */
955
+ /** This function gives the prediction of a cell in a short form for a table. A prediction `ends` gives its window after its band. */
857
956
  const predictionText = (prediction) => {
858
957
  switch (prediction.kind) {
859
958
  case "none": return prediction.outcomes === void 0 ? "no failure" : `no ${prediction.outcomes.join(", ")}`;
@@ -863,6 +962,10 @@ const predictionText = (prediction) => {
863
962
  case "restarts": return prediction.band === null ? "no restart" : `each replica SIGTERM in ${showBand(prediction.band)}`;
864
963
  case "stays": return "stays ready, no refusal";
865
964
  case "stalls": return "the rollout waits for Redis";
965
+ case "ends": {
966
+ const closes = prediction.band === null ? "no socket of a client ends, and each one calls again on its socket" : `the socket of each client ends in ${atBand(prediction.from, prediction.band)}`;
967
+ return prediction.window === void 0 ? closes : `${closes}; ${predictionText(prediction.window)}`;
968
+ }
866
969
  case "measured": return "measured";
867
970
  }
868
971
  };
@@ -964,7 +1067,8 @@ const TRIGGERS = [
964
1067
  "app-stall",
965
1068
  "rollout",
966
1069
  "cpu-throttle",
967
- "authorization-policy"
1070
+ "authorization-policy",
1071
+ "idle"
968
1072
  ];
969
1073
  const MODES = [
970
1074
  "iptables",
@@ -1114,33 +1218,34 @@ const recordOf = (a, main) => {
1114
1218
  lo: a,
1115
1219
  hi: a
1116
1220
  });
1221
+ const window = z.object({
1222
+ kind: z.literal("window"),
1223
+ nodes: z.enum(["all", "paused"]),
1224
+ start: z.object({
1225
+ from: reference,
1226
+ band
1227
+ }),
1228
+ end: z.object({
1229
+ from: reference,
1230
+ band
1231
+ }).nullable(),
1232
+ certain: z.boolean(),
1233
+ afterReset: z.object({
1234
+ life: a,
1235
+ outcome: z.string()
1236
+ }).optional(),
1237
+ each: z.boolean().optional(),
1238
+ propagation: band.optional(),
1239
+ outcomes: z.array(z.string()).optional(),
1240
+ why: z.string()
1241
+ });
1117
1242
  const prediction = z.discriminatedUnion("kind", [
1118
1243
  z.object({
1119
1244
  kind: z.literal("none"),
1120
1245
  outcomes: z.array(z.string()).optional(),
1121
1246
  why: z.string()
1122
1247
  }),
1123
- z.object({
1124
- kind: z.literal("window"),
1125
- nodes: z.enum(["all", "paused"]),
1126
- start: z.object({
1127
- from: reference,
1128
- band
1129
- }),
1130
- end: z.object({
1131
- from: reference,
1132
- band
1133
- }).nullable(),
1134
- certain: z.boolean(),
1135
- afterReset: z.object({
1136
- life: a,
1137
- outcome: z.string()
1138
- }).optional(),
1139
- each: z.boolean().optional(),
1140
- propagation: band.optional(),
1141
- outcomes: z.array(z.string()).optional(),
1142
- why: z.string()
1143
- }),
1248
+ window,
1144
1249
  z.object({
1145
1250
  kind: z.literal("removal"),
1146
1251
  band,
@@ -1164,6 +1269,13 @@ const recordOf = (a, main) => {
1164
1269
  kind: z.literal("stalls"),
1165
1270
  why: z.string()
1166
1271
  }),
1272
+ z.object({
1273
+ kind: z.literal("ends"),
1274
+ from: reference,
1275
+ band: band.nullable(),
1276
+ window: window.optional(),
1277
+ why: z.string()
1278
+ }),
1167
1279
  z.object({
1168
1280
  kind: z.literal("measured"),
1169
1281
  why: z.string()
@@ -1241,7 +1353,8 @@ const recordOf = (a, main) => {
1241
1353
  healUpdates: z.record(z.string(), z.object({
1242
1354
  after: a.nullable(),
1243
1355
  by: a
1244
- }).nullable()).optional()
1356
+ }).nullable()).optional(),
1357
+ closes: z.record(z.string(), a.nullable()).optional()
1245
1358
  });
1246
1359
  return {
1247
1360
  cell,
@@ -1303,6 +1416,6 @@ const readRecord = (data) => {
1303
1416
  throw new Error(`a kind cell record has the format ${FORMAT} or ${FORMAT_V1}, not ${String(format)}`);
1304
1417
  };
1305
1418
  //#endregion
1306
- export { slicesOf as $, medianInterval as A, intakeOf as B, propagationOf as C, unitOf as D, reusesReset as E, byNode as F, recordsOf as G, outcomesOf as H, deletionOf as I, rulesOf as J, requestsOf as K, dropOf as L, quantileOf as M, backendsAt as N, updatesOf as O, byClients as P, sliceEmptyOf as Q, emptyOf as R, observationEnd as S, referenceOf as T, podsAt as U, kernelCounts as V, podsOf as W, signalsOf as X, signalOf as Y, sliceDropOf as Z, checkRun as _, calibrationOf as a, PORT as at, judgeKind as b, predictionText as c, bandOf as ct, FORMAT_V1 as d, showBand as dt, stoppingOf as et, judge as f, LAST_SLACK as g, KIND_NOISE as h, readRecord as i, LOG_LABEL as it, middle as j, RELATIVE as k, timeSpread as l, seconds as lt, upgradeCell as m, kindCellRecordSchema as n, CLIENT as nt, cellsTable as o, REDIS as ot, upgrade as p, restartsAt as q, kindCellRecordV1Schema as r, HEALTH_PORT as rt, medianText as s, SERVER as st, STAGES as t, transitionOf as tt, FORMAT as u, secondsOf as ut, deletionsOf as v, propagationSource as w, observationAfter as x, factsOf as y, failuresOf as z };
1419
+ export { slicesOf as $, medianInterval as A, intakeOf as B, propagationOf as C, unitOf as D, reusesReset as E, byNode as F, recordsOf as G, outcomesOf as H, deletionOf as I, rulesOf as J, requestsOf as K, dropOf as L, quantileOf as M, backendsAt as N, updatesOf as O, byClients as P, sliceEmptyOf as Q, emptyOf as R, observationEnd as S, referenceOf as T, podsAt as U, kernelCounts as V, podsOf as W, signalsOf as X, signalOf as Y, sliceDropOf as Z, checkRun as _, calibrationOf as a, LOG_LABEL as at, judgeKind as b, predictionText as c, SERVER as ct, FORMAT_V1 as d, secondsOf as dt, socketsOf as et, judge as f, showBand as ft, LAST_SLACK as g, KIND_NOISE as h, readRecord as i, HEALTH_PORT as it, middle as j, RELATIVE as k, timeSpread as l, bandOf as lt, upgradeCell as m, kindCellRecordSchema as n, transitionOf as nt, cellsTable as o, PORT as ot, upgrade as p, restartsAt as q, kindCellRecordV1Schema as r, CLIENT as rt, medianText as s, REDIS as st, STAGES as t, stoppingOf as tt, FORMAT as u, seconds as ut, deletionsOf as v, propagationSource as w, observationAfter as x, factsOf as y, failuresOf as z };
1307
1420
 
1308
- //# sourceMappingURL=judge-Bkbs7VVG.js.map
1421
+ //# sourceMappingURL=judge-DNCI5vvt.js.map