@mark1russell7/service 0.5.11 → 0.5.12

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,6 +15,9 @@ This release has the declaration, its export, the law, the health runtime, the l
15
15
  - **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).
16
16
  - **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.
17
17
  - **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.
18
+ - **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.
19
+ - **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.
20
+ - **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.
18
21
  - **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)).
19
22
  - **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.
20
23
 
@@ -510,13 +513,27 @@ const life = await app.run(); // the process exits after SIGTERM, with 0 or 1
510
513
  - **Redis Cluster.** The names of the keys put the limiter in braces, a hash tag. Thus the state and the index of a limiter are in one slot. The prefix has no braces.
511
514
  - **The test clock.** With `{ clock }`, the script uses the time of that clock in place of `TIME`. The tests use it to compare each decision of Redis with `decide`.
512
515
 
516
+ ## The PostgreSQL client
517
+
518
+ `@mark1russell7/service/pg` is the PostgreSQL client of the service, over a pool of node-postgres 8. This entry is a prototype of S6. pg is an optional peer dependency. The client loads it only when it connects.
519
+
520
+ - **The declaration.** `pgClient(name, spec)` declares `connect_bound`, `request_timeout`, `statement_timeout`, `pool_idle_expiry`, `keepalive_idle`, `max_connection_age`, `close_bound` and `retrans_giveup_time`. Each knob is necessary, and a bound that nobody gives is "infinite". `specProblems` refuses a knob that pg cannot use, for example a zero, which pg reads as no timer.
521
+ - **The starting knobs.** `PG_STARTING_KNOBS` and `PG_STARTING_POLL` are a declaration from the cells of S1. Each value has its measured reason in its doc comment.
522
+ - **Three outcomes.** `query(operation, text, values)` gives `ok`, `error` or `indeterminate`. An ERROR of the server is certain. A FATAL, a reset or a query timeout is not certain. The client then gives the connection back with an error, thus the pool ends the orphaned query.
523
+ - **Idempotency for each operation.** SQL text does not tell if a statement changes a state. Thus the client sends again only an idempotent operation that its table names, one time at most, in the bound of the request.
524
+ - **The join.** At each connect, the client records the process id of the backend and the socket (`joins`).
525
+ - **No unheard error.** The client listens to the errors of the pool and of each client that it holds. In Node, an error with no listener ends the process.
526
+ - **The checks.** `checks({ poll, steps })` gives `local-state`, `checkout`, `round-trip` (a `SELECT 1` through the pool) and `dial` (an SSLRequest on a new connection).
527
+ - **The close.** At the shutdown, the client ends the pool, and it destroys each socket that stays at its `close_bound`.
528
+ - **The manifestations and the machine.** `meets` lists the manifestations and their detectors. `PG_CLIENT_MACHINE` is the client as a `mark1russell7.machine/1` document, and the tests replay the client against it.
529
+
513
530
  ## Installation
514
531
 
515
532
  ```sh
516
533
  pnpm add @mark1russell7/service
517
534
  ```
518
535
 
519
- For the Redis client, the application also installs `ioredis@6`.
536
+ For the Redis client, the application also installs `ioredis@6`. For the PostgreSQL client, it installs `pg@8`.
520
537
 
521
538
  The package uses [`@mark1russell7/quantity`](https://www.npmjs.com/package/@mark1russell7/quantity) for each amount. No entry of the service loads `zod` at run time, thus `zod` is an optional peer dependency. An application that validates amounts with the schemas of `quantity` installs `zod`.
522
539
 
@@ -1,6 +1,6 @@
1
1
  import { n as Clock } from "./clock-mVqhXD0b.js";
2
2
  import { c as LimiterSpec } from "./policy-G4QJ1ZZq.js";
3
- import { B as Gate, Ct as Operation, D as Outcome, E as CheckFn, F as CooperativeFloor, H as Phase, I as Coupling, J as Poll, L as Dependency, N as Check, R as ExitCodes, T as StartOptions, U as RepeatSignal, V as MessageTrigger, W as ServiceDescription, X as Timing, ct as ExitTrigger, d as TransportImpl, k as Trace, mt as ProcessTrigger, n as HealthServer, nt as OperationOf, o as PhaseWork, r as Lifecycle, s as RunOptions, t as DependencyHandle, x as Health } from "./lifecycle-CGEegMI0.js";
3
+ import { B as Gate, D as Outcome, E as CheckFn, F as CooperativeFloor, G as ServiceDescription, H as Phase, I as Coupling, L as Dependency, N as Check, R as ExitCodes, T as StartOptions, V as MessageTrigger, W as RepeatSignal, Y as Poll, Z as Timing, d as TransportImpl, ht as ProcessTrigger, k as Trace, lt as ExitTrigger, n as HealthServer, o as PhaseWork, r as Lifecycle, rt as OperationOf, s as RunOptions, t as DependencyHandle, wt as Operation, x as Health } from "./lifecycle-fgYfxrHK.js";
4
4
  import { Extent, Quantity } from "@mark1russell7/quantity";
5
5
  //#region src/profiles.d.ts
6
6
  /**
@@ -208,4 +208,4 @@ interface ServiceBuilder {
208
208
  declare const service: (name: string) => ServiceBuilder;
209
209
  //#endregion
210
210
  export { WatchdogSpec as a, Profile as c, kubernetes as d, localDev as f, Watchdog as i, daemon as l, ServiceBuilder as n, watchdog as o, service as r, PodTiming as s, ControllerBuilder as t, ephemeral as u };
211
- //# sourceMappingURL=builder-Dn0cby6Q.d.ts.map
211
+ //# sourceMappingURL=builder-DawJhq4p.d.ts.map
@@ -0,0 +1,183 @@
1
+ import { n as Clock } from "./clock-mVqhXD0b.js";
2
+ import { B as Gate, E as CheckFn, G as ServiceDescription, K as Step, L as Dependency, M as Recorder, N as Check, Y as Poll, t as DependencyHandle, wt as Operation } from "./lifecycle-fgYfxrHK.js";
3
+ import { n as ServiceBuilder } from "./builder-DawJhq4p.js";
4
+ import { Extent, Quantity } from "@mark1russell7/quantity";
5
+ //#region src/client/meets.d.ts
6
+ /** A situation is what the connection did when the failure started: a command in flight, or idle. */
7
+ type Situation = "in flight" | "idle";
8
+ /** A detector is a mechanism that finds a failure, and the most time that it takes. */
9
+ interface Detector {
10
+ /** This name identifies the mechanism: a knob of the declaration, or the kernel. */
11
+ readonly by: string;
12
+ /** This time is the most that the detector takes, after the failure starts. "infinite" tells that it does not find the failure. */
13
+ readonly within: Extent;
14
+ readonly situation: Situation;
15
+ /** This text tells what the detector does, for example that it fails a command without a change of state. */
16
+ readonly effect: string;
17
+ }
18
+ /**
19
+ * A meeting is a manifestation that the client can meet, and its detectors (E9). The names of the
20
+ * manifestations are the names of the vocabulary of the harness and the corpus. `app-stall` is a
21
+ * server whose process is alive and does not answer, a proposal of the vocabulary.
22
+ */
23
+ interface Meeting {
24
+ readonly manifestation: string;
25
+ /** This text tells the failure in words. */
26
+ readonly what: string;
27
+ readonly detectors: readonly Detector[];
28
+ }
29
+ /** These are the manifestations that each client gives a meeting for, in this sequence. */
30
+ declare const MANIFESTATIONS: readonly ["rst", "fin", "blackhole", "freeze-host", "freeze-process", "app-stall"];
31
+ /** A manifestation is one name of `MANIFESTATIONS`. */
32
+ type Manifestation = (typeof MANIFESTATIONS)[number];
33
+ /** This function gives the first detector of a manifestation in a situation, or null when no detector has an end (C3). */
34
+ declare const firstDetector: (meeting: Meeting, situation: Situation) => Detector | null;
35
+ /** This function gives each manifestation and situation that no detector of the client finds: the standing rule, as a query (C3, E9). */
36
+ declare const unwatched: (meets: readonly Meeting[]) => Array<{
37
+ readonly manifestation: string;
38
+ readonly situation: Situation;
39
+ }>;
40
+ //#endregion
41
+ //#region src/client/core.d.ts
42
+ /**
43
+ * A sent result is the outcome of a call, in three values (C8):
44
+ * - `ok`: the server answered.
45
+ * - `error`: the call did not apply. The server answered with an error (`reply`), or the call did
46
+ * not leave the client.
47
+ * - `indeterminate`: the call can have reached the server, and nothing tells if it applied.
48
+ */
49
+ type Sent<T> = {
50
+ readonly outcome: "ok";
51
+ readonly value: T;
52
+ readonly attempts: number;
53
+ } | {
54
+ readonly outcome: "error";
55
+ readonly error: string;
56
+ readonly reply: boolean;
57
+ readonly attempts: number;
58
+ } | {
59
+ readonly outcome: "indeterminate";
60
+ readonly error: string;
61
+ readonly attempts: number;
62
+ };
63
+ /** This type is the outcome of one try of a call, before the core counts the tries. */
64
+ type Attempt<T = unknown> = {
65
+ readonly outcome: "ok";
66
+ readonly value: T;
67
+ } | {
68
+ readonly outcome: "error";
69
+ readonly error: string;
70
+ readonly reply: boolean;
71
+ } | {
72
+ readonly outcome: "indeterminate";
73
+ readonly error: string;
74
+ };
75
+ /** Send options tell how the client sends one call. */
76
+ interface SendOptions {
77
+ /** This signal abandons the call. A call that left the client is then indeterminate. */
78
+ readonly signal?: AbortSignal | undefined;
79
+ /** False stops the client: it does not send the call again. A check that must fail at once uses it. */
80
+ readonly reissue?: boolean | undefined;
81
+ }
82
+ /** A dial function opens a new connection to the server, and reads one reply of the protocol. The `dial` check uses it. */
83
+ type DialFn = (host: string, port: number, signal: AbortSignal) => Promise<void>;
84
+ /** The steps of a connection that each client gives a check for (B14). A client can add more steps. */
85
+ type ClientStep = Extract<Step, "dial" | "round-trip" | "local-state">;
86
+ /** Check options tell which checks the client gives the health runtime, and which policy each one controls. */
87
+ interface CheckOptions<S extends Step = ClientStep> {
88
+ readonly poll: Poll;
89
+ /** For each step, the policy that its check controls, or null for a check that only reports. A step that is absent has no check. */
90
+ readonly steps: Partial<Record<S, Gate | null>>;
91
+ }
92
+ /**
93
+ * These are the knobs that each client has, by the names of the ordering law. Each driver adds its
94
+ * own knobs. The comments of each driver tell which option of the driver a knob sets.
95
+ */
96
+ interface ClientKnobs {
97
+ /** This time bounds a new connection. */
98
+ readonly connect_bound: Quantity;
99
+ /** This time bounds each call on the connection of the application. */
100
+ readonly request_timeout: Quantity;
101
+ /** This time is the idle time of TCP keepalive. Node then sends 10 probes 1 s apart. */
102
+ readonly keepalive_idle: Extent;
103
+ /** This time is the age at which the client replaces a connection. */
104
+ readonly max_connection_age: Extent;
105
+ /** This time bounds the close of the client at the shutdown. */
106
+ readonly close_bound: Quantity;
107
+ /**
108
+ * This time is when the kernel gives up a segment that it retransmits. Node cannot set it, thus
109
+ * the declaration states it for the ordering law (O17), from the platform.
110
+ */
111
+ readonly retrans_giveup_time: Extent;
112
+ }
113
+ /** A client spec is the declaration of one client: where it connects, and its knobs. Each driver adds its own fields. */
114
+ interface ClientSpec<K extends ClientKnobs = ClientKnobs> {
115
+ readonly host: string;
116
+ readonly port: number;
117
+ readonly password?: string | undefined;
118
+ /** This identifier is the component in the corpus, for example `systems.component.redis`, or null. */
119
+ readonly system: string | null;
120
+ readonly knobs: K;
121
+ /** These operations add to the table of the client, or change it. */
122
+ readonly operations?: Readonly<Record<string, Operation>> | undefined;
123
+ }
124
+ /** A factory makes the connection or the pool of a client from the options of its driver. */
125
+ type Factory<O, C> = (options: O) => C | Promise<C>;
126
+ /** These options make a client. Without them, a client uses its driver, the real clock, and the dial of its protocol. */
127
+ interface ClientOptions<F> {
128
+ readonly create?: F | undefined;
129
+ readonly clock?: Clock | undefined;
130
+ /** This function receives each transition of the client, for the prober (D3). */
131
+ readonly record?: Recorder | undefined;
132
+ readonly dial?: DialFn | undefined;
133
+ }
134
+ /** A client socket is the part of the socket of a connection that the join of the prober and the bounded close read. */
135
+ interface ClientSocket {
136
+ readonly localAddress?: string | undefined;
137
+ readonly localPort?: number | undefined;
138
+ readonly remoteAddress?: string | undefined;
139
+ readonly remotePort?: number | undefined;
140
+ /** This method closes the socket at once. A close of the driver only half-closes it, and a silent peer does not answer that. */
141
+ destroy?(): void;
142
+ }
143
+ /**
144
+ * A client join is one connection, at the time when it can serve (D1, D2). The prober joins its
145
+ * kernel records to the server's view of the connection through it. Each driver adds the server's id of the connection.
146
+ */
147
+ interface ClientJoin {
148
+ readonly generation: number;
149
+ /** This time is when the connection became ready, in milliseconds on the monotonic clock. */
150
+ readonly at: number;
151
+ readonly localAddress: string | null;
152
+ readonly localPort: number | null;
153
+ readonly remoteAddress: string | null;
154
+ readonly remotePort: number | null;
155
+ }
156
+ /** A client is a dependency of a service: its declaration, its handle and its checks. Each driver adds the method that sends a call. */
157
+ interface Client<S extends Step = ClientStep, J extends ClientJoin = ClientJoin> {
158
+ readonly name: string;
159
+ /** This declaration goes to `svc.dependency(name, declaration, handle)`. */
160
+ readonly declaration: Omit<Dependency, "name">;
161
+ readonly handle: DependencyHandle;
162
+ /** These are the manifestations that the client can meet, and the detectors of each (E9). */
163
+ readonly meets: readonly Meeting[];
164
+ /** This text is the state of the client, as the machine document of the client names it. */
165
+ readonly status: string;
166
+ /** These are the connections of the client, the latest last. */
167
+ readonly joins: readonly J[];
168
+ /** This method gives the checks of the client and their functions, for `svc.check(check, run)`. */
169
+ checks(options: CheckOptions<S>): ReadonlyArray<readonly [Check, CheckFn]>;
170
+ /** This method declares the client in a service: its dependency, its handle and its checks. */
171
+ attach(svc: ServiceBuilder, options: CheckOptions<S>): ServiceBuilder;
172
+ /** This method gives the problems of the client in a description: a close that does not fit in its phase, for example. */
173
+ problems(description: ServiceDescription): string[];
174
+ }
175
+ /**
176
+ * This poll is a poll of the checks to start from: each 5 s with a jitter of 20 %, and a timeout of
177
+ * 4 s (B6, O12). Its slowest poll is 4 s + 5 s × 1.2 = 10 s, the slowest poll of the cells of O13.
178
+ * It does not depend on a driver. The starting knobs of each driver fit it.
179
+ */
180
+ declare const STARTING_POLL: Poll;
181
+ //#endregion
182
+ export { Manifestation as _, ClientKnobs as a, firstDetector as b, ClientSpec as c, Factory as d, STARTING_POLL as f, MANIFESTATIONS as g, Detector as h, ClientJoin as i, ClientStep as l, Sent as m, CheckOptions as n, ClientOptions as o, SendOptions as p, Client as r, ClientSocket as s, Attempt as t, DialFn as u, Meeting as v, unwatched as x, Situation as y };
183
+ //# sourceMappingURL=core-C2wnF4mg.d.ts.map
package/build/http.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { n as Clock } from "./clock-mVqhXD0b.js";
2
2
  import { d as Refused } from "./policy-G4QJ1ZZq.js";
3
- import { d as TransportImpl, m as RequestEnd, n as HealthServer, r as Lifecycle, v as RequestStream, w as ProbeKind, x as Health, y as RequestWork } from "./lifecycle-CGEegMI0.js";
3
+ import { d as TransportImpl, m as RequestEnd, n as HealthServer, r as Lifecycle, v as RequestStream, w as ProbeKind, x as Health, y as RequestWork } from "./lifecycle-fgYfxrHK.js";
4
4
  import { Extent, Quantity } from "@mark1russell7/quantity";
5
5
  import { ListenOptions } from "node:net";
6
6
  import { IncomingMessage, Server, ServerEventMap, ServerResponse } from "node:http";
package/build/index.d.ts CHANGED
@@ -2,8 +2,8 @@ import { a as backoffDelay, i as backoffBand, n as DelayBand, o as backoffProble
2
2
  import { a as realClock, i as platformClock, n as Clock, r as PlatformClockOptions, t as Cancel } from "./clock-mVqhXD0b.js";
3
3
  import { Heartbeat, HeartbeatOptions, detectionTime, heartbeat } from "./heartbeat.js";
4
4
  import { S as retryAfterSeconds, _ as freeAt, a as LimitRule, b as limiterProblems, c as LimiterSpec, d as Refused, f as SlidingWindow, g as decide, h as TokenBucket, i as LimitPolicy, l as LimiterStoreSpec, m as TakeStep, n as Decision, o as Limiter, p as StoreFailure, r as KeyState, s as LimiterEviction, t as Allowed, u as RefusalCause, v as idleSpan, x as longestRetryAfter, y as limitRule } from "./policy-G4QJ1ZZq.js";
5
- import { $ as failSpan, A as LifecycleEvent, B as Gate, C as ProbeAnswer, Ct as Operation, D as Outcome, E as CheckFn, F as CooperativeFloor, G as Step, H as Phase, I as Coupling, J as Poll, K as Transport, L as Dependency, M as Recorder, N as Check, O as Result, P as Controller, Q as asWindow, R as ExitCodes, S as HealthVerdict, St as Idempotency, T as StartOptions, U as RepeatSignal, V as MessageTrigger, W as ServiceDescription, X as Timing, Y as Span, Z as asDebounce, _ as RequestOptions, _t as isExitSignal, a as PhaseContext, at as EXIT_SIGNALS, b as CheckVerdict, bt as nodeProcess, c as STAGES, ct as ExitTrigger, d as TransportImpl, dt as PROCESS_TRIGGERS, et as heartbeatDetection, f as cooperativeAbortAt, ft as ProcessHost, g as RequestFn, gt as isExitCode, h as RequestEvent, ht as embeddedProcess, i as LifecycleState, it as RouteRequest, j as MachineName, l as ShutdownOptions, lt as HostStream, m as RequestEnd, mt as ProcessTrigger, n as HealthServer, nt as OperationOf, o as PhaseWork, ot as EmbeddedProcess, p as stageRank, pt as ProcessPort, q as FailsBy, r as Lifecycle, rt as RoutePattern, s as RunOptions, st as ExitSignal, t as DependencyHandle, tt as recoverSpan, u as Stage, ut as MessageShape, v as RequestStream, vt as isMessageShape, w as ProbeKind, wt as mayReissue, x as Health, xt as processPortOf, y as RequestWork, yt as matchesMessage, z as FORMAT } from "./lifecycle-CGEegMI0.js";
6
- import { a as WatchdogSpec, c as Profile, d as kubernetes, f as localDev, i as Watchdog, l as daemon, n as ServiceBuilder, o as watchdog, r as service, s as PodTiming, t as ControllerBuilder, u as ephemeral } from "./builder-Dn0cby6Q.js";
5
+ import { $ as asWindow, A as LifecycleEvent, B as Gate, C as ProbeAnswer, Ct as Idempotency, D as Outcome, E as CheckFn, F as CooperativeFloor, G as ServiceDescription, H as Phase, I as Coupling, J as FailsBy, K as Step, L as Dependency, M as Recorder, N as Check, O as Result, P as Controller, Q as asDebounce, R as ExitCodes, S as HealthVerdict, St as processPortOf, T as StartOptions, Tt as mayReissue, U as ProgressMarks, V as MessageTrigger, W as RepeatSignal, X as Span, Y as Poll, Z as Timing, _ as RequestOptions, _t as isExitCode, a as PhaseContext, at as RouteRequest, b as CheckVerdict, bt as matchesMessage, c as STAGES, ct as ExitSignal, d as TransportImpl, dt as MessageShape, et as failSpan, f as cooperativeAbortAt, ft as PROCESS_TRIGGERS, g as RequestFn, gt as embeddedProcess, h as RequestEvent, ht as ProcessTrigger, i as LifecycleState, it as RoutePattern, j as MachineName, l as ShutdownOptions, lt as ExitTrigger, m as RequestEnd, mt as ProcessPort, n as HealthServer, nt as recoverSpan, o as PhaseWork, ot as EXIT_SIGNALS, p as stageRank, pt as ProcessHost, q as Transport, r as Lifecycle, rt as OperationOf, s as RunOptions, st as EmbeddedProcess, t as DependencyHandle, tt as heartbeatDetection, u as Stage, ut as HostStream, v as RequestStream, vt as isExitSignal, w as ProbeKind, wt as Operation, x as Health, xt as nodeProcess, y as RequestWork, yt as isMessageShape, z as FORMAT } from "./lifecycle-fgYfxrHK.js";
6
+ import { a as WatchdogSpec, c as Profile, d as kubernetes, f as localDev, i as Watchdog, l as daemon, n as ServiceBuilder, o as watchdog, r as service, s as PodTiming, t as ControllerBuilder, u as ephemeral } from "./builder-DawJhq4p.js";
7
7
  import { a as keyedLimiter, i as TakeRequest, n as LimiterStore, o as memoryStore, r as MemoryStore, t as KeyedLimiter } from "./store-D5HLgBnW.js";
8
8
  import { a as limiterStateOf, i as limiterMachine, n as SLIDING_WINDOW_MACHINE, r as TOKEN_BUCKET_MACHINE, t as LimiterState } from "./limiter-B0849Vhp.js";
9
9
  import { Extent, Quantity } from "@mark1russell7/quantity";
@@ -152,10 +152,13 @@ export declare const serviceBindings: (svc: ServiceDescription, scenario: Scenar
152
152
  };
153
153
  /** This identifier is the rule of the findings about a field that the declaration does not give. */
154
154
  export declare const DECLARATION_RULE = "declaration";
155
+ /** This identifier is the rule of the findings about the stall time of a progress check (`Check.marks`). */
156
+ export declare const PROGRESS_RULE = "progress";
155
157
  /**
156
158
  * This function applies each service rule to a declaration and a scenario. A rule applies one time
157
159
  * for the service, for each client, for each consumer and worker, or for each limiter. The function
158
160
  * also gives a finding for each necessary field that the description does not give (`DECLARATION_RULE`).
161
+ * Each progress check that names its loops also gets its findings (`PROGRESS_RULE`).
159
162
  */
160
163
  export declare const lint: (svc: ServiceDescription, scenario: Scenario, rules: readonly Rule[]) => Finding[];
161
164
  //#endregion
@@ -433,5 +436,5 @@ export declare const drainMachine: (description: ServiceDescription) => Machine;
433
436
  /** These are the static machines of the runtime, by the name that their events carry. */
434
437
  export declare const MACHINES: Readonly<Record<Exclude<MachineName, "drain" | "client">, Machine>>;
435
438
  //#endregion
436
- export { type Allowed, type Backoff, type Binding, type Bound, type Cancel, type Check, type CheckFn, type CheckVerdict, type Clock, type Controller, type ControllerBuilder, type CooperativeFloor, type Coupling, type Decision, type DelayBand, type Dependency, type DependencyHandle, EXIT_SIGNALS, type EmbeddedProcess, type Evaluated, type ExitCodes, type ExitSignal, type ExitTrigger, FORMAT, type FailsBy, type Finding, type Gate, type Health, 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 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, realClock, recoverSpan, replay, retryAfterSeconds, service, stageRank, watchdog };
439
+ export { type Allowed, type Backoff, type Binding, type Bound, type Cancel, type Check, type CheckFn, type CheckVerdict, type Clock, type Controller, type ControllerBuilder, type CooperativeFloor, type Coupling, type Decision, type DelayBand, type Dependency, type DependencyHandle, EXIT_SIGNALS, type EmbeddedProcess, type Evaluated, type ExitCodes, type ExitSignal, type ExitTrigger, FORMAT, type FailsBy, type Finding, type Gate, type Health, 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, realClock, recoverSpan, replay, retryAfterSeconds, service, stageRank, watchdog };
437
440
  //# sourceMappingURL=index.d.ts.map
package/build/index.js CHANGED
@@ -1656,6 +1656,25 @@ const watchdog = (clock, spec, trace) => {
1656
1656
  const isTime$1 = (amount) => convertible(amount.unit, "s");
1657
1657
  const above = (amount) => amount.value === "infinite" || isTime$1(amount) && valueIn(amount, "s") > 0;
1658
1658
  const whole = (count) => Number.isInteger(count) && count >= 1;
1659
+ /** This function copies a check, with its poll and the loops that mark it. */
1660
+ const copyCheck = (check) => ({
1661
+ ...check,
1662
+ poll: { ...check.poll },
1663
+ ...check.marks === void 0 ? {} : { marks: {
1664
+ loops: [...check.marks.loops],
1665
+ stall: { ...check.marks.stall }
1666
+ } }
1667
+ });
1668
+ /** This function gives the problems of the loops that mark a check. `lint()` compares the stall time with the loops. */
1669
+ const marksProblems = (check) => {
1670
+ const marks = check.marks;
1671
+ if (marks === void 0) return [];
1672
+ const problems = [];
1673
+ if (check.step !== "progress") problems.push(`check ${check.name} names the loops that mark it, but its step is ${check.step}, not progress`);
1674
+ if (marks.loops.length === 0) problems.push(`check ${check.name} names no loop that marks it`);
1675
+ if (typeof marks.stall.value !== "number" || !above(marks.stall)) problems.push(`check ${check.name} has a stall time that is not a time above zero`);
1676
+ return problems;
1677
+ };
1659
1678
  /** This function gives the problems of a timing: a zero interval or count fails decoding. */
1660
1679
  const timingProblems = (policy, timing) => {
1661
1680
  if (timing.type === "debounce") {
@@ -1844,14 +1863,11 @@ const service = (name) => {
1844
1863
  check(check, run) {
1845
1864
  once(checks, check.name, "check");
1846
1865
  if (started && run === void 0) throw new Error(`check ${check.name} joins after the start, thus it needs its function`);
1847
- checks.push({ ...check });
1866
+ checks.push(copyCheck(check));
1848
1867
  if (run !== void 0) runs.set(check.name, run);
1849
1868
  if (started) {
1850
1869
  partial = true;
1851
- live?.addCheck({
1852
- ...check,
1853
- poll: { ...check.poll }
1854
- }, run);
1870
+ live?.addCheck(copyCheck(check), run);
1855
1871
  }
1856
1872
  return builder;
1857
1873
  },
@@ -1989,6 +2005,7 @@ const service = (name) => {
1989
2005
  if (!isTime$1(check.poll.interval) || !isTime$1(check.poll.timeout)) problems.push(`check ${check.name} polls in a unit that is not a time`);
1990
2006
  else if (!above(check.poll.interval) || !above(check.poll.timeout)) problems.push(`check ${check.name} polls with an interval or a timeout that is not above zero`);
1991
2007
  if (!(check.poll.jitter >= 0 && check.poll.jitter < 1)) problems.push(`check ${check.name} has a jitter of ${check.poll.jitter}, not a fraction below 1`);
2008
+ problems.push(...marksProblems(check));
1992
2009
  const timing = check.gates === null ? null : policies[check.gates];
1993
2010
  if (timing?.type === "debounce" && isTime$1(check.poll.interval) && above(timing.interval) && valueIn(check.poll.interval, "s") !== valueIn(timing.interval, "s")) problems.push(`check ${check.name} polls every ${valueIn(check.poll.interval, "s")} s, but the ${check.gates} debounce counts polls every ${valueIn(timing.interval, "s")} s`);
1994
2011
  }
@@ -2031,10 +2048,7 @@ const service = (name) => {
2031
2048
  operations: { ...entry.operations }
2032
2049
  })),
2033
2050
  controllers: controllersOut,
2034
- checks: checks.map((entry) => ({
2035
- ...entry,
2036
- poll: { ...entry.poll }
2037
- })),
2051
+ checks: checks.map(copyCheck),
2038
2052
  ...limiters.length === 0 ? {} : { limiters: limiters.map((entry) => ({
2039
2053
  ...entry,
2040
2054
  policy: { ...entry.policy },
@@ -2385,14 +2399,144 @@ const declarationFindings = (svc) => svc.shutdown.onRepeatSignal === void 0 ? [{
2385
2399
  evaluated: [],
2386
2400
  unbound: ["shutdown.onRepeatSignal"]
2387
2401
  }] : [];
2402
+ /** This identifier is the rule of the findings about the stall time of a progress check (`Check.marks`). */
2403
+ const PROGRESS_RULE = "progress";
2404
+ /**
2405
+ * This rule compares the stall time of a progress check with the longest gap between two marks of
2406
+ * the slowest loop that it names. A shorter stall time trips the check while each loop turns on time.
2407
+ */
2408
+ const PROGRESS_LAW = {
2409
+ id: PROGRESS_RULE,
2410
+ rule: "the stall time of a progress check is at least the longest gap between two marks of the slowest loop that it names",
2411
+ window: "a loop that turns on time can trip the check",
2412
+ expr: {
2413
+ lhs: { terms: [{
2414
+ knob: "stall",
2415
+ coef: 1,
2416
+ at: "check"
2417
+ }] },
2418
+ op: ">=",
2419
+ rhs: { terms: [{
2420
+ knob: "mark_gap",
2421
+ coef: 1,
2422
+ at: "loop"
2423
+ }] },
2424
+ margin: 0
2425
+ }
2426
+ };
2427
+ /** These are the knobs of a worker that bound the gap between two of its marks. */
2428
+ const WORKER_GAP_KNOBS = [
2429
+ "worker_sleep",
2430
+ "unit_processing_time",
2431
+ "retry_base",
2432
+ "retry_cap"
2433
+ ];
2434
+ /**
2435
+ * This function gives the longest gap between two marks of each loop with a name. The rule counts
2436
+ * a mark at the end of each turn of the loop, also a turn that fails.
2437
+ * - A check polls on a fixed delay from the start of each poll, and each poll ends at its timeout
2438
+ * at the latest. Thus the gap is `interval × (1 + jitter) + timeout`.
2439
+ * - After an iteration, a lane of a worker waits its sleep, or its backoff after a failure. The
2440
+ * longest backoff is `retry_cap` plus its jitter of up to `retry_base`. The next iteration then
2441
+ * takes up to `unit_processing_time`. Thus the gap is the longer wait plus that bound.
2442
+ *
2443
+ * A sleep or a bound without a limit gives a gap without a limit. An iteration that does not end
2444
+ * at its abort can take more time, and the rule does not bound it.
2445
+ */
2446
+ const gapsOf = (svc, loop) => {
2447
+ const gaps = svc.checks.filter((check) => check.name === loop).map((check) => ({
2448
+ loop,
2449
+ seconds: secondsOf(check.poll.interval) * (1 + check.poll.jitter) + secondsOf(check.poll.timeout),
2450
+ unbound: []
2451
+ }));
2452
+ for (const worker of svc.transports) {
2453
+ if (worker.kind !== "worker" || worker.name !== loop) continue;
2454
+ const knob = (name) => {
2455
+ const amount = worker.knobs[name];
2456
+ if (amount === void 0 || !isTime(amount.unit)) return void 0;
2457
+ return amount.value === "infinite" ? Infinity : secondsOf(amount);
2458
+ };
2459
+ const unbound = WORKER_GAP_KNOBS.filter((name) => knob(name) === void 0).map((name) => `${loop}.${name}`);
2460
+ if (unbound.length > 0) {
2461
+ gaps.push({
2462
+ loop,
2463
+ seconds: void 0,
2464
+ unbound
2465
+ });
2466
+ continue;
2467
+ }
2468
+ const [sleep, unit, base, cap] = WORKER_GAP_KNOBS.map((name) => knob(name));
2469
+ const backoff = Number.isFinite(base) && Number.isFinite(cap) ? backoffBand({
2470
+ base,
2471
+ factor: 2,
2472
+ cap,
2473
+ jitter: {
2474
+ kind: "add",
2475
+ amount: base
2476
+ }
2477
+ }).high : Infinity;
2478
+ gaps.push({
2479
+ loop,
2480
+ seconds: Math.max(sleep, backoff) + unit,
2481
+ unbound: []
2482
+ });
2483
+ }
2484
+ return gaps;
2485
+ };
2486
+ /**
2487
+ * This function gives the findings of each check that names the loops that mark it. A name that is
2488
+ * not a check or a worker of the service gets a finding of its own, with the verdict `depends`.
2489
+ * Without `marks`, a check gets no finding.
2490
+ */
2491
+ const progressFindings = (svc) => {
2492
+ const findings = [];
2493
+ const finding = (check, fields) => ({
2494
+ rule: PROGRESS_RULE,
2495
+ scope: check.name,
2496
+ statement: PROGRESS_LAW.rule,
2497
+ window: PROGRESS_LAW.window,
2498
+ ...fields
2499
+ });
2500
+ for (const check of svc.checks) {
2501
+ const marks = check.marks;
2502
+ if (marks === void 0) continue;
2503
+ const gaps = [];
2504
+ for (const loop of marks.loops) {
2505
+ const found = gapsOf(svc, loop);
2506
+ if (found.length > 0) gaps.push(...found);
2507
+ else findings.push(finding(check, {
2508
+ verdict: "depends",
2509
+ statement: "each loop that a progress check names is a check or a worker of the service",
2510
+ window: `the check waits for the marks of ${loop}, and the service has no loop with that name`,
2511
+ evaluated: [],
2512
+ unbound: [loop]
2513
+ }));
2514
+ }
2515
+ if (gaps.length === 0) continue;
2516
+ const unbound = [...isTime(marks.stall.unit) ? [] : [`${check.name}.marks.stall`], ...gaps.flatMap((gap) => gap.unbound)];
2517
+ const slowest = gaps.reduce((worst, gap) => (gap.seconds ?? -Infinity) > (worst.seconds ?? -Infinity) ? gap : worst);
2518
+ const bind = {};
2519
+ if (isTime(marks.stall.unit)) bind["stall@check"] = boundOf(marks.stall);
2520
+ if (unbound.length === 0 && slowest.seconds !== void 0) bind["mark_gap@loop"] = Number.isFinite(slowest.seconds) ? slowest.seconds : "inf";
2521
+ const evaluated = evaluate(PROGRESS_LAW, bind);
2522
+ findings.push(finding(check, {
2523
+ verdict: verdictOf(PROGRESS_LAW, bind, evaluated),
2524
+ ...unbound.length === 0 ? { statement: `${PROGRESS_LAW.rule} (${slowest.loop})` } : {},
2525
+ evaluated,
2526
+ unbound
2527
+ }));
2528
+ }
2529
+ return findings;
2530
+ };
2388
2531
  /**
2389
2532
  * This function applies each service rule to a declaration and a scenario. A rule applies one time
2390
2533
  * for the service, for each client, for each consumer and worker, or for each limiter. The function
2391
2534
  * also gives a finding for each necessary field that the description does not give (`DECLARATION_RULE`).
2535
+ * Each progress check that names its loops also gets its findings (`PROGRESS_RULE`).
2392
2536
  */
2393
2537
  const lint = (svc, scenario, rules) => {
2394
2538
  const service = serviceBindings(svc, scenario).bind;
2395
- const findings = [...declarationFindings(svc)];
2539
+ const findings = [...declarationFindings(svc), ...progressFindings(svc)];
2396
2540
  for (const rule of rules) {
2397
2541
  if (!SERVICE_RULES.includes(rule.id)) continue;
2398
2542
  const symbols = symbolsOf(rule);
@@ -3411,6 +3555,6 @@ const MACHINES = {
3411
3555
  watchdog: WATCHDOG_MACHINE
3412
3556
  };
3413
3557
  //#endregion
3414
- export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LIMITER_RULES, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, 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, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3558
+ export { DECLARATION_RULE, 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, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3415
3559
 
3416
3560
  //# sourceMappingURL=index.js.map