@mark1russell7/service 0.5.14 → 0.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,9 +15,14 @@ This release has the declaration, its export, the law, the health runtime, the l
15
15
  - **Keyed operations.** A keyed operation has the window that the server keeps its key for. `mayReissue` answers from the declaration of an operation. An operation without a declaration does not go again.
16
16
  - **The law.** `evaluate` and `verdictOf` decide one rule of the timer-ordering law on one binding. A rule is data: linear inequalities over `symbol@hop`, with a margin. A verdict is `holds`, `violated`, `unbounded` (it holds only because a timer is absent) or `depends` (nothing is bound).
17
17
  - **The linter.** `lint(description, scenario, rules)` applies the service rules (`SERVICE_RULES`) to a declaration. The declaration binds the symbols of the service: the polls of its checks, the windows of its policies, its drain. The scenario binds the symbols of the platform and of each dependency. A `depends` finding names the symbols that have no bound.
18
+ - **The claims of a scenario.** A default is true only for a version: the keep-alive of Node is 5 s, but 65 s on main. Thus a value of the scenario can be a claim that names its epoch: `{ epoch: "node@22", value }`. Each finding then gives the epoch of each value that its rule reads (`epochs`).
19
+ - **A measured value.** A claim can give a distribution by its statistics: `{ epoch, distribution: { p50, p99 } }`. The law binds the p99. A distribution without a p99 binds nothing, thus each rule that reads it depends.
20
+ - **The four ways that a pod stops.** `STOPS` gives the steps of each way. A deletion and a liveness restart have the preStop hook and the drain. A crash and an OOM stop have neither. `lintStop(description, scenario, rules, way)` applies only the rules of the drain that guard that way: O04 for a deletion, O09 for a liveness restart. For a crash or an OOM stop, a finding of `STOP_RULE` depends on the time until the kubelet finds the exit.
18
21
  - **The scopes of a rule.** A rule about a client applies one time for each dependency. A rule about a consumer, for example O20, applies one time for each consumer and each worker.
19
22
  - **The loops of a progress check.** A check of the step `progress` can name the loops that mark it, with its stall time: `marks: { loops, stall }`. Each loop is a check or a worker of the service.
20
23
  - **The stall time.** `lint()` compares the stall time with the longest gap between two marks of the slowest loop (`PROGRESS_RULE`). A loop marks at the end of each turn, also a turn that fails. The gap of a check is `interval × (1 + jitter) + timeout`. The gap of a worker is its sleep or its longest backoff, the longer one, plus `unit_processing_time`. At 300 s ± 20 % and a timeout of 10 s, a poll gives 370 s.
24
+ - **The marks of the timers.** A harness that runs a service k times faster scales its timers. Each timer has a mark: `scalable`, `fixed`, `stepped` (only multiples of a step) or `floored` (a peer sets a floor).
25
+ - **The compression of a service.** `compressionOf(description, scenario, rules, k, marks)` gives each rule and scope whose verdict the compression changes. It also names the symbols that have no mark. `PLATFORM_MARKS` marks the timers of Kubernetes. For example, the propagation of the endpoints is `fixed`, thus a compression of the hold of O04 can break O04.
21
26
  - **A name that is not a loop.** A name that is not a check or a worker gets a `depends` finding. Without `marks`, a check gets no finding of this rule.
22
27
  - **Keyed limiters.** `svc.limiter(name, spec)` declares a rate limit for each key, as data. `keyedLimiter` makes its runtime on a store ([The keyed limiter](#the-keyed-limiter)).
23
28
  - **The hold.** The cap of the `fail readiness` phase binds `readiness_hold@process`, and a service without that phase holds for zero. The hold keeps the listener open after the signal, as a preStop sleep does. Thus O04 can compare the sum of the two with the propagation of the endpoints.
package/build/index.d.ts CHANGED
@@ -84,6 +84,55 @@ export declare const evaluate: (rule: Rule, bind: Binding) => Evaluated[];
84
84
  * applies, the arithmetic cannot decide, and the verdict is "depends".
85
85
  */
86
86
  export declare const verdictOf: (rule: Rule, bind: Binding, evaluated: readonly Evaluated[]) => Verdict;
87
+ /**
88
+ * A mark tells how a timer moves when a compression scales time by k (E10). A "scalable" timer
89
+ * becomes k times its value. A "fixed" timer keeps its value. A "stepped" timer takes only
90
+ * multiples of its step, thus it rounds up. A "floored" timer has a floor that a peer sets, for
91
+ * example the minimum retransmission timeout of the kernel. The step and the floor are in seconds.
92
+ */
93
+ type TimerMark = {
94
+ readonly kind: "scalable";
95
+ } | {
96
+ readonly kind: "fixed";
97
+ } | {
98
+ readonly kind: "stepped";
99
+ readonly step: number;
100
+ } | {
101
+ readonly kind: "floored";
102
+ readonly floor: number;
103
+ readonly by: string;
104
+ };
105
+ /** The marks map `symbol@hop` to a mark. A bare symbol marks the symbol at each hop. */
106
+ type TimerMarks = Readonly<Record<string, TimerMark | undefined>>;
107
+ /** A compressed binding, and the symbols that have no mark. */
108
+ interface Compressed {
109
+ readonly bind: Binding;
110
+ /** These symbols have no mark. They keep their values, thus the compression proves nothing about them. */
111
+ readonly unmarked: readonly string[];
112
+ }
113
+ /** A change is a rule whose verdict a compression changes. */
114
+ interface Change {
115
+ readonly rule: string;
116
+ readonly before: Verdict;
117
+ readonly after: Verdict;
118
+ }
119
+ /** The result of a compression: the rules whose verdict it changes, and the symbols of the rules that have no mark. */
120
+ interface Compression {
121
+ readonly changed: readonly Change[];
122
+ readonly unmarked: readonly string[];
123
+ }
124
+ /**
125
+ * This function scales each timer of a binding by k, by its mark (E10). An absent timer ("inf" or
126
+ * "off") stays absent. A chain scales each value. The margins of the binding and the constants of
127
+ * the rules do not scale: they are tolerances, not timers. A symbol with no mark keeps its value.
128
+ */
129
+ export declare const compress: (bind: Binding, k: number, marks: TimerMarks) => Compressed;
130
+ /**
131
+ * This function tells which rules a compression by k changes. A compression keeps each ordering
132
+ * when no verdict changes. A change in either direction counts: a violation that the compression
133
+ * hides is as wrong as one that it makes. The unmarked symbols are the symbols that the rules read.
134
+ */
135
+ export declare const compression: (rules: readonly Rule[], bind: Binding, k: number, marks: TimerMarks) => Compression;
87
136
  //#endregion
88
137
  //#region src/lint.d.ts
89
138
  /**
@@ -96,10 +145,31 @@ export declare const verdictOf: (rule: Rule, bind: Binding, evaluated: readonly
96
145
  * A rule about a client applies one time for each dependency. A rule about a consumer applies one
97
146
  * time for each consumer and each worker. A rule about a limiter applies one time for each limiter.
98
147
  */
148
+ /** A statistic of a measured distribution. The law binds p99 (`LAW_STATISTIC`). */
149
+ type Statistic = "p50" | "p90" | "p99" | "p999" | "max";
150
+ /** The law binds this statistic of a distribution. */
151
+ export declare const LAW_STATISTIC: Statistic;
152
+ /**
153
+ * A claim is a value of the scenario that names its epoch: the component and the version that give
154
+ * it, for example "node@22" (E2). A default is true only for a version: the keep-alive of Node is
155
+ * 5 s, but 65 s on main. A claim gives a set value, or a measured distribution by its statistics.
156
+ */
157
+ type Claim = {
158
+ readonly epoch: string;
159
+ readonly value: Extent;
160
+ } | {
161
+ readonly epoch: string;
162
+ readonly distribution: Readonly<Partial<Record<Statistic, Quantity>>>;
163
+ };
164
+ /**
165
+ * A value of the scenario is an amount, or a claim. An amount names no epoch, thus the finding of
166
+ * each rule that reads it gives null for its epoch.
167
+ */
168
+ type ScenarioValue = Extent | Claim;
99
169
  /** A scenario gives the values of the platform and of each dependency, by the `symbol@hop` of the law. */
100
170
  interface Scenario {
101
171
  /** These are the values of the platform: prestop_sleep@pod, termination_grace@pod, probe_period@pod, propagation_bound@cluster. */
102
- readonly platform: Readonly<Record<string, Extent>>;
172
+ readonly platform: Readonly<Record<string, ScenarioValue>>;
103
173
  /**
104
174
  * These are the probe windows of the kubelet, period × failure threshold. They add to the
105
175
  * windows of the declaration. A probe reads the cached verdict, thus detection is the latch,
@@ -110,16 +180,18 @@ interface Scenario {
110
180
  readonly liveness: Quantity;
111
181
  };
112
182
  /** These are the values of each dependency, by its name: failover_time@dependency, server_idle_close@server. */
113
- readonly dependencies?: Readonly<Record<string, Readonly<Record<string, Extent>>>> | undefined;
183
+ readonly dependencies?: Readonly<Record<string, Readonly<Record<string, ScenarioValue>>>> | undefined;
114
184
  /** These are the values of the source of each consumer and worker, by its name and by the `symbol@hop` of the law: lease_duration@broker. */
115
- readonly transports?: Readonly<Record<string, Readonly<Record<string, Extent>>>> | undefined;
185
+ readonly transports?: Readonly<Record<string, Readonly<Record<string, ScenarioValue>>>> | undefined;
116
186
  /**
117
187
  * These are the values of the callers of each limiter, by its name and by the `symbol@hop` of the
118
188
  * law: client_retry_window@caller, retry_delay@caller. A symbol without a hop binds at `caller`.
119
189
  */
120
- readonly limiters?: Readonly<Record<string, Readonly<Record<string, Extent>>>> | undefined;
190
+ readonly limiters?: Readonly<Record<string, Readonly<Record<string, ScenarioValue>>>> | undefined;
121
191
  }
122
- /** A finding is the verdict of one rule for one scope: the service, or the client of a dependency. */
192
+ /** The epochs of a scope map each symbol that the scenario binds to the epoch of its value, or null when the value names none. */
193
+ type Epochs = Readonly<Record<string, string | null>>;
194
+ /** A finding is the verdict of one rule for one scope: the service, or the client of a dependency. When a value of the scenario names an epoch, the finding gives the epoch of each value that the rule reads. */
123
195
  interface Finding {
124
196
  readonly rule: string;
125
197
  readonly scope: string;
@@ -129,6 +201,8 @@ interface Finding {
129
201
  readonly evaluated: readonly Evaluated[];
130
202
  /** These are the symbols that have no bound. A "depends" verdict waits for them. */
131
203
  readonly unbound: readonly string[];
204
+ /** These are the epochs of the symbols of the rule that the scenario binds (E2). Thus a finding is true for these versions. */
205
+ readonly epochs?: Epochs | undefined;
132
206
  }
133
207
  /** These are the identifiers of the rules that each declaration gets. */
134
208
  export declare const SERVICE_RULES: readonly string[];
@@ -150,11 +224,31 @@ export declare const LIMITER_RULES: readonly Rule[];
150
224
  export declare const serviceBindings: (svc: ServiceDescription, scenario: Scenario) => {
151
225
  readonly bind: Record<string, Bound>;
152
226
  readonly from: Record<string, string>;
227
+ readonly epochs: Epochs;
153
228
  };
154
229
  /** This identifier is the rule of the findings about a field that the declaration does not give. */
155
230
  export declare const DECLARATION_RULE = "declaration";
156
231
  /** This identifier is the rule of the findings about the stall time of a progress check (`Check.marks`). */
157
232
  export declare const PROGRESS_RULE = "progress";
233
+ /**
234
+ * These are the marks of the timers of the platform (E10). Kubernetes takes the probe period, the
235
+ * preStop sleep and the termination grace in whole seconds, thus each is stepped by 1 s. The
236
+ * cluster sets the propagation of the endpoints, thus a compression of the service cannot scale it.
237
+ */
238
+ export declare const PLATFORM_MARKS: TimerMarks;
239
+ /** A change of a compression of a service: a rule and a scope whose verdict the compression changes. */
240
+ interface ScopedChange extends Change {
241
+ readonly scope: string;
242
+ }
243
+ /**
244
+ * This function tells which findings of `lint` a compression by k changes (E10). It scales the
245
+ * bindings of each scope by the marks. Thus a harness that runs the service k times faster learns
246
+ * the orderings that its run breaks. The unmarked symbols are the symbols of the rules with no mark.
247
+ */
248
+ export declare const compressionOf: (svc: ServiceDescription, scenario: Scenario, rules: readonly Rule[], k: number, marks: TimerMarks) => {
249
+ readonly changed: readonly ScopedChange[];
250
+ readonly unmarked: readonly string[];
251
+ };
158
252
  /**
159
253
  * This function applies each service rule to a declaration and a scenario. A rule applies one time
160
254
  * for the service, for each client, for each consumer and worker, or for each limiter. The function
@@ -163,6 +257,52 @@ export declare const PROGRESS_RULE = "progress";
163
257
  */
164
258
  export declare const lint: (svc: ServiceDescription, scenario: Scenario, rules: readonly Rule[]) => Finding[];
165
259
  //#endregion
260
+ //#region src/stops.d.ts
261
+ /**
262
+ * This module keeps apart the four ways that a pod stops (B11). Each way runs a different part of
263
+ * the shutdown, thus a different set of the rules of the drain guards it.
264
+ */
265
+ /** A way that a pod stops: a deletion, a liveness restart, a crash, or a stop for lack of memory (OOM). */
266
+ type StopWay = "deletion" | "liveness-restart" | "crash" | "oom";
267
+ /** What a way of stopping runs, and the rules of the drain that guard it. */
268
+ interface Stop {
269
+ readonly way: StopWay;
270
+ /** This is the signal that the process gets first, or null when the process exits by itself. */
271
+ readonly signal: "SIGTERM" | "SIGKILL" | null;
272
+ /** This flag tells if the kubelet runs the preStop hook. */
273
+ readonly preStop: boolean;
274
+ /** This flag tells if the process runs its shutdown phases. */
275
+ readonly drain: boolean;
276
+ /**
277
+ * This is how the pod leaves its endpoints: by its deletion, when its readiness fails, or when
278
+ * the kubelet finds that the container exited.
279
+ */
280
+ readonly leaves: "deletion" | "readiness" | "exit";
281
+ /** These are the identifiers of the rules of the drain that guard this way. */
282
+ readonly rules: readonly string[];
283
+ }
284
+ /** These are the rules of the drain. Each way of stopping applies only its own rules of this set. */
285
+ export declare const DRAIN_RULE_IDS: readonly string[];
286
+ /**
287
+ * These are the four ways that a pod stops.
288
+ * - A deletion runs the preStop hook, then SIGTERM. The endpoints drop the pod at its deletion,
289
+ * thus the preStop sleep and the hold must cover the propagation (O04).
290
+ * - A liveness restart also runs the preStop hook and SIGTERM, with the grace. The pod stays in its
291
+ * endpoints until its readiness fails, thus readiness must fail first (O09).
292
+ * - A crash and an OOM stop run no hook and no drain. The kubelet finds the exit at its relist, and
293
+ * no rule bounds that time. A client that can reissue a request is the only guard.
294
+ */
295
+ export declare const STOPS: Readonly<Record<StopWay, Stop>>;
296
+ /** The identifier of the finding of a way of stopping that runs no drain. */
297
+ export declare const STOP_RULE = "stop";
298
+ /**
299
+ * This function applies the rules to a declaration for one way of stopping. A rule of the drain
300
+ * applies only when it guards that way. The other rules apply as `lint()` applies them. A way with
301
+ * no drain also gets a finding of `STOP_RULE` that depends. No rule bounds the time until the
302
+ * kubelet finds the exit, and the work in flight is lost.
303
+ */
304
+ export declare const lintStop: (svc: ServiceDescription, scenario: Scenario, rules: readonly Rule[], way: StopWay) => Finding[];
305
+ //#endregion
166
306
  //#region src/worker.d.ts
167
307
  /**
168
308
  * These are the knobs of a worker loop, by the names of the ordering law where the law has them.
@@ -292,5 +432,5 @@ export declare const drainMachine: (description: ServiceDescription) => Machine;
292
432
  /** These are the static machines of the runtime, by the name that their events carry. */
293
433
  export declare const MACHINES: Readonly<Record<Exclude<MachineName, "drain" | "client">, Machine>>;
294
434
  //#endregion
295
- export { type Allowed, type Backoff, type Binding, type Bound, type Cancel, type Check, type CheckFn, type CheckMetrics, type CheckVerdict, type Clock, type Controller, type ControllerBuilder, type CooperativeFloor, type Coupling, DURATION_FRACTIONS, type Decision, type DelayBand, type Dependency, type DependencyHandle, type DurationHistogram, EPISODES_KEPT, EXIT_SIGNALS, type EmbeddedProcess, type Episode, type Evaluated, type ExitCodes, type ExitSignal, type ExitTrigger, FORMAT, type FailsBy, type Finding, type Gate, type Health, type HealthMetrics, type HealthServer, type HealthVerdict, type Heartbeat, type HeartbeatOptions, type HostStream, type Idempotency, type Inequality, type IterationContext, type Jitter, type JobAction, type JobContext, type JobFailure, type JobSettle, type JobSource, type KeyState, type KeyedLimiter, type Lifecycle, type LifecycleEvent, type LifecycleState, type LimitPolicy, type LimitRule, type Limiter, type LimiterEviction, type LimiterSpec, type LimiterState, type LimiterStore, type LimiterStoreSpec, type MachineName, type MemoryStore, type MessageShape, type MessageTrigger, type Operation, type OperationOf, type Outcome, PROCESS_TRIGGERS, type Phase, type PhaseContext, type PhaseWork, type PlatformClockOptions, type PodTiming, type Poll, type ProbeAnswer, type ProbeKind, type ProcessHost, type ProcessPort, type ProcessTrigger, type Profile, type ProgressMarks, type QueueDrain, type QueueDrainOptions, type QueueKnobs, type Recorder, type RedeliveryPolicy, type RefusalCause, type Refused, type RepeatSignal, type RequestEnd, type RequestEvent, type RequestFn, type RequestOptions, type RequestStream, type RequestWork, type RequeueCause, type Result, type RoutePattern, type RouteRequest, type Rule, type RunOptions, SLIDING_WINDOW_MACHINE, STAGES, type Scenario, type ServiceBuilder, type ServiceDescription, type ShutdownOptions, type Side, type SlidingWindow, type Span, type Stage, type StartOptions, type Step, type StoreFailure, TOKEN_BUCKET_MACHINE, type TakeRequest, type TakeStep, type Term, type Timing, type TokenBucket, type Transport, type TransportImpl, type UnitRequest, type Verdict, type Watchdog, type WatchdogSpec, type WorkerKnobs, type WorkerLoop, type WorkerLoopOptions, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, cooperativeAbortAt, daemon, decide, detectionTime, embeddedProcess, ephemeral, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redact, redeliverOnce, replay, retryAfterSeconds, service, stageRank, watchdog };
435
+ export { type Allowed, type Backoff, type Binding, type Bound, type Cancel, type Change, type Check, type CheckFn, type CheckMetrics, type CheckVerdict, type Claim, type Clock, type Compressed, type Compression, type Controller, type ControllerBuilder, type CooperativeFloor, type Coupling, DURATION_FRACTIONS, type Decision, type DelayBand, type Dependency, type DependencyHandle, type DurationHistogram, EPISODES_KEPT, EXIT_SIGNALS, type EmbeddedProcess, type Episode, type Epochs, type Evaluated, type ExitCodes, type ExitSignal, type ExitTrigger, FORMAT, type FailsBy, type Finding, type Gate, type Health, type HealthMetrics, type HealthServer, type HealthVerdict, type Heartbeat, type HeartbeatOptions, type HostStream, type Idempotency, type Inequality, type IterationContext, type Jitter, type JobAction, type JobContext, type JobFailure, type JobSettle, type JobSource, type KeyState, type KeyedLimiter, type Lifecycle, type LifecycleEvent, type LifecycleState, type LimitPolicy, type LimitRule, type Limiter, type LimiterEviction, type LimiterSpec, type LimiterState, type LimiterStore, type LimiterStoreSpec, type MachineName, type MemoryStore, type MessageShape, type MessageTrigger, type Operation, type OperationOf, type Outcome, PROCESS_TRIGGERS, type Phase, type PhaseContext, type PhaseWork, type PlatformClockOptions, type PodTiming, type Poll, type ProbeAnswer, type ProbeKind, type ProcessHost, type ProcessPort, type ProcessTrigger, type Profile, type ProgressMarks, type QueueDrain, type QueueDrainOptions, type QueueKnobs, type Recorder, type RedeliveryPolicy, type RefusalCause, type Refused, type RepeatSignal, type RequestEnd, type RequestEvent, type RequestFn, type RequestOptions, type RequestStream, type RequestWork, type RequeueCause, type Result, type RoutePattern, type RouteRequest, type Rule, type RunOptions, SLIDING_WINDOW_MACHINE, STAGES, type Scenario, type ScenarioValue, type ScopedChange, type ServiceBuilder, type ServiceDescription, type ShutdownOptions, type Side, type SlidingWindow, type Span, type Stage, type StartOptions, type Statistic, type Step, type Stop, type StopWay, type StoreFailure, TOKEN_BUCKET_MACHINE, type TakeRequest, type TakeStep, type Term, type TimerMark, type TimerMarks, type Timing, type TokenBucket, type Transport, type TransportImpl, type UnitRequest, type Verdict, type Watchdog, type WatchdogSpec, type WorkerKnobs, type WorkerLoop, type WorkerLoopOptions, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, cooperativeAbortAt, daemon, decide, detectionTime, embeddedProcess, ephemeral, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redact, redeliverOnce, replay, retryAfterSeconds, service, stageRank, watchdog };
296
436
  //# sourceMappingURL=index.d.ts.map
package/build/index.js CHANGED
@@ -2318,8 +2318,74 @@ const verdictOf = (rule, bind, evaluated) => {
2318
2318
  const all = inequalities(rule);
2319
2319
  return applicable.some((index) => readsAbsent(all[index], bind)) ? "unbounded" : "holds";
2320
2320
  };
2321
+ /** This function gives the mark of a symbol: its mark at its hop, else the mark of the bare symbol. */
2322
+ const markOf = (marks, key) => marks[key] ?? marks[key.split("@")[0] ?? key];
2323
+ const STEP_EPSILON = 1e-9;
2324
+ const scaled = (seconds, k, mark) => {
2325
+ switch (mark.kind) {
2326
+ case "scalable": return seconds * k;
2327
+ case "fixed": return seconds;
2328
+ case "stepped": return Math.max(Math.ceil(seconds * k / mark.step - STEP_EPSILON), 0) * mark.step;
2329
+ case "floored": return Math.max(seconds * k, mark.floor);
2330
+ }
2331
+ };
2332
+ /**
2333
+ * This function scales each timer of a binding by k, by its mark (E10). An absent timer ("inf" or
2334
+ * "off") stays absent. A chain scales each value. The margins of the binding and the constants of
2335
+ * the rules do not scale: they are tolerances, not timers. A symbol with no mark keeps its value.
2336
+ */
2337
+ const compress = (bind, k, marks) => {
2338
+ if (!Number.isFinite(k) || k <= 0) throw new RangeError(`a compression scales time by a finite factor above 0, not ${k}`);
2339
+ const out = {};
2340
+ const unmarked = [];
2341
+ for (const [key, bound] of Object.entries(bind)) {
2342
+ if (key === "margin" || key === "margins") continue;
2343
+ const mark = markOf(marks, key);
2344
+ if (mark === void 0) {
2345
+ if (bound !== void 0) unmarked.push(key);
2346
+ out[key] = bound;
2347
+ } else if (typeof bound === "number") out[key] = scaled(bound, k, mark);
2348
+ else if (Array.isArray(bound)) out[key] = bound.map((seconds) => scaled(seconds, k, mark));
2349
+ else out[key] = bound;
2350
+ }
2351
+ const margins = {
2352
+ ...bind.margin !== void 0 ? { margin: bind.margin } : {},
2353
+ ...bind.margins !== void 0 ? { margins: bind.margins } : {}
2354
+ };
2355
+ return {
2356
+ bind: {
2357
+ ...out,
2358
+ ...margins
2359
+ },
2360
+ unmarked
2361
+ };
2362
+ };
2363
+ /**
2364
+ * This function tells which rules a compression by k changes. A compression keeps each ordering
2365
+ * when no verdict changes. A change in either direction counts: a violation that the compression
2366
+ * hides is as wrong as one that it makes. The unmarked symbols are the symbols that the rules read.
2367
+ */
2368
+ const compression = (rules, bind, k, marks) => {
2369
+ const compressed = compress(bind, k, marks);
2370
+ const changed = rules.flatMap((rule) => {
2371
+ const before = verdictOf(rule, bind, evaluate(rule, bind));
2372
+ const after = verdictOf(rule, compressed.bind, evaluate(rule, compressed.bind));
2373
+ return before === after ? [] : [{
2374
+ rule: rule.id,
2375
+ before,
2376
+ after
2377
+ }];
2378
+ });
2379
+ const read = new Set(rules.flatMap(symbolsOf));
2380
+ return {
2381
+ changed,
2382
+ unmarked: compressed.unmarked.filter((symbol) => read.has(symbol))
2383
+ };
2384
+ };
2321
2385
  //#endregion
2322
2386
  //#region src/lint.ts
2387
+ /** The law binds this statistic of a distribution. */
2388
+ const LAW_STATISTIC = "p99";
2323
2389
  /** These are the identifiers of the rules that each declaration gets. */
2324
2390
  const SERVICE_RULES = [
2325
2391
  "O04",
@@ -2410,6 +2476,35 @@ const LIMITER_RULES = [{
2410
2476
  const secondsOf = (amount) => rationals.toNumber(convertExact(rationals.fromNumber(amount.value), amount.unit, "s"));
2411
2477
  /** This function gives an amount, or no bound, as the law binds it. */
2412
2478
  const boundOf = (amount) => amount.value === "infinite" ? "inf" : secondsOf(amount);
2479
+ /**
2480
+ * This function gives a value of the scenario as the law binds it, with its epoch. A distribution
2481
+ * binds its p99. A distribution without a p99 binds nothing, thus each rule that reads it depends.
2482
+ */
2483
+ const claimOf = (value) => {
2484
+ if (!("epoch" in value)) return {
2485
+ bound: boundOf(value),
2486
+ epoch: null
2487
+ };
2488
+ if ("value" in value) return {
2489
+ bound: boundOf(value.value),
2490
+ epoch: value.epoch
2491
+ };
2492
+ const statistic = value.distribution["p99"];
2493
+ return {
2494
+ bound: statistic === void 0 ? void 0 : secondsOf(statistic),
2495
+ epoch: value.epoch
2496
+ };
2497
+ };
2498
+ /** This function binds each value of a part of the scenario, at the symbol that `at` gives, and records its epoch. */
2499
+ const bindScenario = (values, at, bind, epochs) => {
2500
+ for (const [symbol, value] of Object.entries(values)) {
2501
+ const claim = claimOf(value);
2502
+ const key = at(symbol);
2503
+ epochs[key] = claim.epoch;
2504
+ if (claim.bound !== void 0) bind[key] = claim.bound;
2505
+ else delete bind[key];
2506
+ }
2507
+ };
2413
2508
  /** This function tells if a unit is a time. A unit that the package does not know is not a time. */
2414
2509
  const isTime = (unit) => {
2415
2510
  try {
@@ -2424,6 +2519,7 @@ const most = (values) => values.length === 0 ? void 0 : Math.max(...values);
2424
2519
  const serviceBindings = (svc, scenario) => {
2425
2520
  const bind = {};
2426
2521
  const from = {};
2522
+ const epochs = {};
2427
2523
  const set = (symbol, bound, said) => {
2428
2524
  if (bound === void 0) return;
2429
2525
  bind[symbol] = bound;
@@ -2446,18 +2542,25 @@ const serviceBindings = (svc, scenario) => {
2446
2542
  set("cooperative_window@process", boundOf(svc.shutdown.cooperativeWindow), "the declared cooperative window");
2447
2543
  const hold = svc.shutdown.phases.find((phase) => phase.name === "fail readiness");
2448
2544
  set("readiness_hold@process", hold === void 0 ? 0 : secondsOf(hold.cap), hold === void 0 ? "no fail readiness phase: the intake stops at once after the signal" : "the cap of the fail readiness phase, which holds while the endpoints update");
2449
- for (const [symbol, amount] of Object.entries(scenario.platform)) set(symbol, boundOf(amount), "the scenario");
2545
+ bindScenario(scenario.platform, (symbol) => symbol, bind, epochs);
2546
+ for (const [symbol, epoch] of Object.entries(epochs)) if (bind[symbol] === void 0) delete from[symbol];
2547
+ else from[symbol] = epoch === null ? "the scenario" : `the scenario, for ${epoch}`;
2450
2548
  return {
2451
2549
  bind,
2452
- from
2550
+ from,
2551
+ epochs
2453
2552
  };
2454
2553
  };
2455
2554
  /** This function gives the client knobs of a dependency at @client, and the values of the scenario at its own hops. */
2456
2555
  const dependencyBindings = (dependency, scenario) => {
2457
2556
  const bind = {};
2557
+ const epochs = {};
2458
2558
  for (const [knob, amount] of Object.entries(dependency.knobs)) bind[`${knob}@client`] = boundOf(amount);
2459
- for (const [symbol, amount] of Object.entries(scenario.dependencies?.[dependency.name] ?? {})) bind[symbol.includes("@") ? symbol : `${symbol}@dependency`] = boundOf(amount);
2460
- return bind;
2559
+ bindScenario(scenario.dependencies?.[dependency.name] ?? {}, (symbol) => symbol.includes("@") ? symbol : `${symbol}@dependency`, bind, epochs);
2560
+ return {
2561
+ bind,
2562
+ epochs
2563
+ };
2461
2564
  };
2462
2565
  /**
2463
2566
  * These are the hops of the law at which the knobs of a transport bind, by its kind. A worker does
@@ -2474,12 +2577,16 @@ const TRANSPORT_HOPS = {
2474
2577
  */
2475
2578
  const transportBindings = (transport, hop, scenario) => {
2476
2579
  const bind = {};
2580
+ const epochs = {};
2477
2581
  for (const [knob, amount] of Object.entries(transport.knobs)) {
2478
2582
  if (!isTime(amount.unit)) continue;
2479
2583
  bind[knob.includes("@") ? knob : `${knob}@${hop}`] = boundOf(amount);
2480
2584
  }
2481
- for (const [symbol, amount] of Object.entries(scenario.transports?.[transport.name] ?? {})) bind[symbol] = boundOf(amount);
2482
- return bind;
2585
+ bindScenario(scenario.transports?.[transport.name] ?? {}, (symbol) => symbol, bind, epochs);
2586
+ return {
2587
+ bind,
2588
+ epochs
2589
+ };
2483
2590
  };
2484
2591
  /**
2485
2592
  * This function gives the symbols of a limiter, and the values of the scenario for its callers.
@@ -2493,8 +2600,12 @@ const limiterBindings = (limiter, scenario) => {
2493
2600
  "retry_after@limiter": retryAfterSeconds(longestRetryAfter(limiter.policy)),
2494
2601
  "retry_after_at_cap@limiter": retryAfterSeconds(idleSpan(limiter.policy))
2495
2602
  };
2496
- for (const [symbol, amount] of Object.entries(scenario.limiters?.[limiter.name] ?? {})) bind[symbol.includes("@") ? symbol : `${symbol}@caller`] = boundOf(amount);
2497
- return bind;
2603
+ const epochs = {};
2604
+ bindScenario(scenario.limiters?.[limiter.name] ?? {}, (symbol) => symbol.includes("@") ? symbol : `${symbol}@caller`, bind, epochs);
2605
+ return {
2606
+ bind,
2607
+ epochs
2608
+ };
2498
2609
  };
2499
2610
  const SCOPED = /@(client|dependency|server)$/;
2500
2611
  const TRANSPORT_SCOPED = /@consumer$/;
@@ -2644,6 +2755,79 @@ const progressFindings = (svc) => {
2644
2755
  }
2645
2756
  return findings;
2646
2757
  };
2758
+ /** This function gives the scopes of a rule that reads these symbols: the service, each client, each consumer and worker, or each limiter. */
2759
+ const scopesOf = (svc, scenario, service, symbols) => {
2760
+ const within = (name, scoped) => {
2761
+ const bind = {
2762
+ ...service.bind,
2763
+ ...scoped.bind
2764
+ };
2765
+ for (const symbol of Object.keys(scoped.epochs)) if (!(symbol in scoped.bind)) delete bind[symbol];
2766
+ return [
2767
+ name,
2768
+ bind,
2769
+ {
2770
+ ...service.epochs,
2771
+ ...scoped.epochs
2772
+ }
2773
+ ];
2774
+ };
2775
+ return symbols.some((symbol) => LIMITER_SCOPED.test(symbol)) ? (svc.limiters ?? []).map((limiter) => within(limiter.name, limiterBindings(limiter, scenario))) : symbols.some((symbol) => TRANSPORT_SCOPED.test(symbol)) ? svc.transports.flatMap((transport) => {
2776
+ const hop = TRANSPORT_HOPS[transport.kind];
2777
+ return hop === null ? [] : [within(transport.name, transportBindings(transport, hop, scenario))];
2778
+ }) : symbols.some((symbol) => SCOPED.test(symbol)) ? svc.dependencies.map((dependency) => within(dependency.name, dependencyBindings(dependency, scenario))) : [[
2779
+ svc.name,
2780
+ service.bind,
2781
+ service.epochs
2782
+ ]];
2783
+ };
2784
+ /** This function gives the epochs of the symbols of a rule that the scenario binds. */
2785
+ const epochsOf = (symbols, epochs) => Object.fromEntries(symbols.flatMap((symbol) => symbol in epochs ? [[symbol, epochs[symbol] ?? null]] : []));
2786
+ /**
2787
+ * These are the marks of the timers of the platform (E10). Kubernetes takes the probe period, the
2788
+ * preStop sleep and the termination grace in whole seconds, thus each is stepped by 1 s. The
2789
+ * cluster sets the propagation of the endpoints, thus a compression of the service cannot scale it.
2790
+ */
2791
+ const PLATFORM_MARKS = {
2792
+ "probe_period@pod": {
2793
+ kind: "stepped",
2794
+ step: 1
2795
+ },
2796
+ "prestop_sleep@pod": {
2797
+ kind: "stepped",
2798
+ step: 1
2799
+ },
2800
+ "termination_grace@pod": {
2801
+ kind: "stepped",
2802
+ step: 1
2803
+ },
2804
+ "propagation_bound@cluster": { kind: "fixed" }
2805
+ };
2806
+ /**
2807
+ * This function tells which findings of `lint` a compression by k changes (E10). It scales the
2808
+ * bindings of each scope by the marks. Thus a harness that runs the service k times faster learns
2809
+ * the orderings that its run breaks. The unmarked symbols are the symbols of the rules with no mark.
2810
+ */
2811
+ const compressionOf = (svc, scenario, rules, k, marks) => {
2812
+ const service = serviceBindings(svc, scenario);
2813
+ const changed = [];
2814
+ const unmarked = /* @__PURE__ */ new Set();
2815
+ for (const rule of rules) {
2816
+ if (!SERVICE_RULES.includes(rule.id)) continue;
2817
+ for (const [scope, bind] of scopesOf(svc, scenario, service, symbolsOf(rule))) {
2818
+ const result = compression([rule], bind, k, marks);
2819
+ for (const change of result.changed) changed.push({
2820
+ ...change,
2821
+ scope
2822
+ });
2823
+ for (const symbol of result.unmarked) unmarked.add(symbol);
2824
+ }
2825
+ }
2826
+ return {
2827
+ changed,
2828
+ unmarked: [...unmarked]
2829
+ };
2830
+ };
2647
2831
  /**
2648
2832
  * This function applies each service rule to a declaration and a scenario. A rule applies one time
2649
2833
  * for the service, for each client, for each consumer and worker, or for each limiter. The function
@@ -2651,26 +2835,14 @@ const progressFindings = (svc) => {
2651
2835
  * Each progress check that names its loops also gets its findings (`PROGRESS_RULE`).
2652
2836
  */
2653
2837
  const lint = (svc, scenario, rules) => {
2654
- const service = serviceBindings(svc, scenario).bind;
2838
+ const service = serviceBindings(svc, scenario);
2655
2839
  const findings = [...declarationFindings(svc), ...progressFindings(svc)];
2656
2840
  for (const rule of rules) {
2657
2841
  if (!SERVICE_RULES.includes(rule.id)) continue;
2658
2842
  const symbols = symbolsOf(rule);
2659
- const scopes = symbols.some((symbol) => LIMITER_SCOPED.test(symbol)) ? (svc.limiters ?? []).map((limiter) => [limiter.name, {
2660
- ...service,
2661
- ...limiterBindings(limiter, scenario)
2662
- }]) : symbols.some((symbol) => TRANSPORT_SCOPED.test(symbol)) ? svc.transports.flatMap((transport) => {
2663
- const hop = TRANSPORT_HOPS[transport.kind];
2664
- return hop === null ? [] : [[transport.name, {
2665
- ...service,
2666
- ...transportBindings(transport, hop, scenario)
2667
- }]];
2668
- }) : symbols.some((symbol) => SCOPED.test(symbol)) ? svc.dependencies.map((dependency) => [dependency.name, {
2669
- ...service,
2670
- ...dependencyBindings(dependency, scenario)
2671
- }]) : [[svc.name, service]];
2672
- for (const [scope, bind] of scopes) {
2843
+ for (const [scope, bind, all] of scopesOf(svc, scenario, service, symbols)) {
2673
2844
  const evaluated = evaluate(rule, bind);
2845
+ const epochs = epochsOf(symbols, all);
2674
2846
  findings.push({
2675
2847
  rule: rule.id,
2676
2848
  scope,
@@ -2678,13 +2850,97 @@ const lint = (svc, scenario, rules) => {
2678
2850
  statement: rule.rule,
2679
2851
  ...rule.window !== void 0 ? { window: rule.window } : {},
2680
2852
  evaluated,
2681
- unbound: symbols.filter((symbol) => bind[symbol] === void 0)
2853
+ unbound: symbols.filter((symbol) => bind[symbol] === void 0),
2854
+ ...Object.values(epochs).some((epoch) => epoch !== null) ? { epochs } : {}
2682
2855
  });
2683
2856
  }
2684
2857
  }
2685
2858
  return findings;
2686
2859
  };
2687
2860
  //#endregion
2861
+ //#region src/stops.ts
2862
+ /** These are the rules of the drain. Each way of stopping applies only its own rules of this set. */
2863
+ const DRAIN_RULE_IDS = [
2864
+ "O04",
2865
+ "O09",
2866
+ "O10",
2867
+ "O16"
2868
+ ];
2869
+ /**
2870
+ * These are the four ways that a pod stops.
2871
+ * - A deletion runs the preStop hook, then SIGTERM. The endpoints drop the pod at its deletion,
2872
+ * thus the preStop sleep and the hold must cover the propagation (O04).
2873
+ * - A liveness restart also runs the preStop hook and SIGTERM, with the grace. The pod stays in its
2874
+ * endpoints until its readiness fails, thus readiness must fail first (O09).
2875
+ * - A crash and an OOM stop run no hook and no drain. The kubelet finds the exit at its relist, and
2876
+ * no rule bounds that time. A client that can reissue a request is the only guard.
2877
+ */
2878
+ const STOPS = {
2879
+ deletion: {
2880
+ way: "deletion",
2881
+ signal: "SIGTERM",
2882
+ preStop: true,
2883
+ drain: true,
2884
+ leaves: "deletion",
2885
+ rules: [
2886
+ "O04",
2887
+ "O10",
2888
+ "O16"
2889
+ ]
2890
+ },
2891
+ "liveness-restart": {
2892
+ way: "liveness-restart",
2893
+ signal: "SIGTERM",
2894
+ preStop: true,
2895
+ drain: true,
2896
+ leaves: "readiness",
2897
+ rules: [
2898
+ "O09",
2899
+ "O10",
2900
+ "O16"
2901
+ ]
2902
+ },
2903
+ crash: {
2904
+ way: "crash",
2905
+ signal: null,
2906
+ preStop: false,
2907
+ drain: false,
2908
+ leaves: "exit",
2909
+ rules: []
2910
+ },
2911
+ oom: {
2912
+ way: "oom",
2913
+ signal: "SIGKILL",
2914
+ preStop: false,
2915
+ drain: false,
2916
+ leaves: "exit",
2917
+ rules: []
2918
+ }
2919
+ };
2920
+ /** The identifier of the finding of a way of stopping that runs no drain. */
2921
+ const STOP_RULE = "stop";
2922
+ /**
2923
+ * This function applies the rules to a declaration for one way of stopping. A rule of the drain
2924
+ * applies only when it guards that way. The other rules apply as `lint()` applies them. A way with
2925
+ * no drain also gets a finding of `STOP_RULE` that depends. No rule bounds the time until the
2926
+ * kubelet finds the exit, and the work in flight is lost.
2927
+ */
2928
+ const lintStop = (svc, scenario, rules, way) => {
2929
+ const stop = STOPS[way];
2930
+ const applies = rules.filter((rule) => !DRAIN_RULE_IDS.includes(rule.id) || stop.rules.includes(rule.id));
2931
+ const findings = lint(svc, scenario, applies);
2932
+ if (stop.drain) return findings;
2933
+ return [...findings, {
2934
+ rule: STOP_RULE,
2935
+ scope: svc.name,
2936
+ verdict: "depends",
2937
+ statement: `a ${way} runs no preStop hook and no drain, and no rule bounds the time until the kubelet finds the exit`,
2938
+ window: "the work in flight is lost, and the endpoints keep the pod until the kubelet finds the exit",
2939
+ evaluated: [],
2940
+ unbound: ["exit_detection@pod"]
2941
+ }];
2942
+ };
2943
+ //#endregion
2688
2944
  //#region src/profiles.ts
2689
2945
  const s = (value) => ({
2690
2946
  value,
@@ -3671,6 +3927,6 @@ const MACHINES = {
3671
3927
  watchdog: WATCHDOG_MACHINE
3672
3928
  };
3673
3929
  //#endregion
3674
- export { DECLARATION_RULE, DURATION_FRACTIONS, EPISODES_KEPT, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LIMITER_RULES, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, PROGRESS_RULE, SERVICE_RULES, SLIDING_WINDOW_MACHINE, STAGES, TOKEN_BUCKET_MACHINE, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, decide, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, inequalities, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, lint, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redact, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3930
+ export { DECLARATION_RULE, DRAIN_RULE_IDS, DURATION_FRACTIONS, EPISODES_KEPT, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LAW_STATISTIC, LIMITER_RULES, MACHINES, PLATFORM_MARKS, POLLER_MACHINE, PROCESS_TRIGGERS, PROGRESS_RULE, SERVICE_RULES, SLIDING_WINDOW_MACHINE, STAGES, STOPS, STOP_RULE, TOKEN_BUCKET_MACHINE, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, compress, compression, compressionOf, cooperativeAbortAt, daemon, decide, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, inequalities, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, lint, lintStop, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redact, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3675
3931
 
3676
3932
  //# sourceMappingURL=index.js.map