@mark1russell7/service 0.5.11 → 0.5.13
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 +110 -1
- package/build/{builder-Dn0cby6Q.d.ts → builder-DawJhq4p.d.ts} +2 -2
- package/build/core-BPLRDcUJ.d.ts +226 -0
- package/build/dial-DA8vJfh2.d.ts +16 -0
- package/build/grpc.d.ts +307 -0
- package/build/grpc.js +859 -0
- package/build/grpc.js.map +1 -0
- package/build/http-client.d.ts +248 -0
- package/build/http-client.js +612 -0
- package/build/http-client.js.map +1 -0
- package/build/http.d.ts +1 -1
- package/build/index.d.ts +8 -149
- package/build/index.js +155 -11
- package/build/index.js.map +1 -1
- package/build/{lifecycle-CGEegMI0.d.ts → lifecycle-fgYfxrHK.d.ts} +17 -2
- package/build/machine-BhTRu3u9.js +552 -0
- package/build/machine-BhTRu3u9.js.map +1 -0
- package/build/mongodb.d.ts +307 -0
- package/build/mongodb.js +688 -0
- package/build/mongodb.js.map +1 -0
- package/build/pg.d.ts +230 -0
- package/build/pg.js +531 -0
- package/build/pg.js.map +1 -0
- package/build/queue-snRHK3Jm.d.ts +150 -0
- package/build/rabbitmq.d.ts +294 -0
- package/build/rabbitmq.js +748 -0
- package/build/rabbitmq.js.map +1 -0
- package/build/redis.d.ts +24 -143
- package/build/redis.js +76 -315
- package/build/redis.js.map +1 -1
- package/build/testing.d.ts +2 -2
- package/build/websocket.d.ts +275 -0
- package/build/websocket.js +780 -0
- package/build/websocket.js.map +1 -0
- package/package.json +46 -1
|
@@ -255,6 +255,21 @@ interface Check {
|
|
|
255
255
|
/** This gate is the policy that it controls, or null for a check that only reports. It has no default. */
|
|
256
256
|
readonly gates: Gate | null;
|
|
257
257
|
readonly poll: Poll;
|
|
258
|
+
/**
|
|
259
|
+
* These are the loops that mark the progress of a check of the step `progress`, and its stall
|
|
260
|
+
* time. Without a declaration, the check names no loops, and `lint()` does not compare its stall time.
|
|
261
|
+
*/
|
|
262
|
+
readonly marks?: ProgressMarks | undefined;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* A progress mark declaration tells which loops mark the progress of a check, and how long the
|
|
266
|
+
* check waits for a mark. The check function fails when no loop marked in the stall time.
|
|
267
|
+
*/
|
|
268
|
+
interface ProgressMarks {
|
|
269
|
+
/** These are the names of the loops: a check (its poller) or a worker of the service. */
|
|
270
|
+
readonly loops: readonly string[];
|
|
271
|
+
/** This time is the most time from one mark to the next before the check fails. */
|
|
272
|
+
readonly stall: Quantity;
|
|
258
273
|
}
|
|
259
274
|
/**
|
|
260
275
|
* A dependency is something that the service uses, with its client. Its knobs have the names that
|
|
@@ -701,5 +716,5 @@ interface Lifecycle {
|
|
|
701
716
|
keepAlive(): boolean;
|
|
702
717
|
}
|
|
703
718
|
//#endregion
|
|
704
|
-
export {
|
|
705
|
-
//# sourceMappingURL=lifecycle-
|
|
719
|
+
export { asWindow as $, LifecycleEvent as A, Gate as B, ProbeAnswer as C, Idempotency as Ct, Outcome as D, CheckFn as E, CooperativeFloor as F, ServiceDescription as G, Phase as H, Coupling as I, FailsBy as J, Step as K, Dependency as L, Recorder as M, Check as N, Result as O, Controller as P, asDebounce as Q, ExitCodes as R, HealthVerdict as S, processPortOf as St, StartOptions as T, mayReissue as Tt, ProgressMarks as U, MessageTrigger as V, RepeatSignal as W, Span as X, Poll as Y, Timing as Z, RequestOptions as _, isExitCode as _t, PhaseContext as a, RouteRequest as at, CheckVerdict as b, matchesMessage as bt, STAGES as c, ExitSignal as ct, TransportImpl as d, MessageShape as dt, failSpan as et, cooperativeAbortAt as f, PROCESS_TRIGGERS as ft, RequestFn as g, embeddedProcess as gt, RequestEvent as h, ProcessTrigger as ht, LifecycleState as i, RoutePattern as it, MachineName as j, Trace as k, ShutdownOptions as l, ExitTrigger as lt, RequestEnd as m, ProcessPort as mt, HealthServer as n, recoverSpan as nt, PhaseWork as o, EXIT_SIGNALS as ot, stageRank as p, ProcessHost as pt, Transport as q, Lifecycle as r, OperationOf as rt, RunOptions as s, EmbeddedProcess as st, DependencyHandle as t, heartbeatDetection as tt, Stage as u, HostStream as ut, RequestStream as v, isExitSignal as vt, ProbeKind as w, Operation as wt, Health as x, nodeProcess as xt, RequestWork as y, isMessageShape as yt, FORMAT as z };
|
|
720
|
+
//# sourceMappingURL=lifecycle-fgYfxrHK.d.ts.map
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { t as mayReissue } from "./operation-DL5RxFds.js";
|
|
2
|
+
import { backoffDelay } from "./backoff.js";
|
|
3
|
+
import { quantity, valueIn } from "@mark1russell7/quantity";
|
|
4
|
+
import { connect } from "node:net";
|
|
5
|
+
//#region src/client/meets.ts
|
|
6
|
+
/** These are the manifestations that each client gives a meeting for, in this sequence. */
|
|
7
|
+
const MANIFESTATIONS = [
|
|
8
|
+
"rst",
|
|
9
|
+
"fin",
|
|
10
|
+
"blackhole",
|
|
11
|
+
"freeze-host",
|
|
12
|
+
"freeze-process",
|
|
13
|
+
"app-stall"
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* These words tell each manifestation that does not depend on the driver. The words of `app-stall`
|
|
17
|
+
* name an example, and the example is a fact of each server, thus each client gives them.
|
|
18
|
+
*/
|
|
19
|
+
const WHAT = {
|
|
20
|
+
rst: "the server answers each segment with a reset, and refuses each new connection",
|
|
21
|
+
fin: "the process of the server ends, and its kernel closes the connection",
|
|
22
|
+
blackhole: "the network drops each segment in the two directions",
|
|
23
|
+
"freeze-host": "the host of the server stops, without a close: the death without a close",
|
|
24
|
+
"freeze-process": "the process of the server stops, and its kernel stays up"
|
|
25
|
+
};
|
|
26
|
+
/** This amount is a detector that does not find the failure. */
|
|
27
|
+
const NEVER = {
|
|
28
|
+
value: "infinite",
|
|
29
|
+
unit: "s"
|
|
30
|
+
};
|
|
31
|
+
const s = (value) => quantity(Math.round(value * 1e6) / 1e6, "s");
|
|
32
|
+
const seconds = (amount) => amount.value === "infinite" ? Number.POSITIVE_INFINITY : valueIn(amount, "s");
|
|
33
|
+
/** This function gives the time at which Node's TCP keepalive gives up a silent peer: the idle time, then 10 probes 1 s apart. */
|
|
34
|
+
const keepaliveGiveUp = (idle) => idle.value === "infinite" ? NEVER : s(seconds(idle) + 10);
|
|
35
|
+
/** This detector is a segment that arrives at once: a reset or a FIN. */
|
|
36
|
+
const immediate = (situation, effect) => ({
|
|
37
|
+
by: "the next segment",
|
|
38
|
+
within: s(0),
|
|
39
|
+
situation,
|
|
40
|
+
effect
|
|
41
|
+
});
|
|
42
|
+
/** This detector is TCP keepalive on a silent peer. It gives up after its probes, and the socket closes. */
|
|
43
|
+
const keepaliveSilent = (idle) => ({
|
|
44
|
+
by: "keepalive_idle",
|
|
45
|
+
within: keepaliveGiveUp(idle),
|
|
46
|
+
situation: "idle",
|
|
47
|
+
effect: "TCP keepalive gives up, and the socket closes"
|
|
48
|
+
});
|
|
49
|
+
/** This detector is TCP keepalive on a peer whose kernel stays up. That kernel answers each probe, thus keepalive does not find the failure. */
|
|
50
|
+
const keepaliveAnswered = () => ({
|
|
51
|
+
by: "keepalive_idle",
|
|
52
|
+
within: NEVER,
|
|
53
|
+
situation: "idle",
|
|
54
|
+
effect: "the kernel of the server answers each probe"
|
|
55
|
+
});
|
|
56
|
+
/** This detector is TCP keepalive on a peer that resets: the first probe gets the reset. */
|
|
57
|
+
const keepaliveReset = (idle) => ({
|
|
58
|
+
by: "keepalive_idle",
|
|
59
|
+
within: idle.value === "infinite" ? NEVER : idle,
|
|
60
|
+
situation: "idle",
|
|
61
|
+
effect: "the first probe gets a reset"
|
|
62
|
+
});
|
|
63
|
+
/** This detector is the kernel, which gives up the retransmissions of a segment in flight. A driver can add what follows. */
|
|
64
|
+
const kernelGiveUp = (within, after) => ({
|
|
65
|
+
by: "retrans_giveup_time",
|
|
66
|
+
within,
|
|
67
|
+
situation: "in flight",
|
|
68
|
+
effect: `the kernel gives up the retransmissions and closes the socket${after === void 0 ? "" : `, and ${after}`}`
|
|
69
|
+
});
|
|
70
|
+
/** This function gives the first detector of a manifestation in a situation, or null when no detector has an end (C3). */
|
|
71
|
+
const firstDetector = (meeting, situation) => {
|
|
72
|
+
return meeting.detectors.filter((detector) => detector.situation === situation && detector.within.value !== "infinite").sort((a, b) => seconds(a.within) - seconds(b.within))[0] ?? null;
|
|
73
|
+
};
|
|
74
|
+
/** This function gives each manifestation and situation that no detector of the client finds: the standing rule, as a query (C3, E9). */
|
|
75
|
+
const unwatched = (meets) => meets.flatMap((meeting) => ["in flight", "idle"].flatMap((situation) => firstDetector(meeting, situation) === null ? [{
|
|
76
|
+
manifestation: meeting.manifestation,
|
|
77
|
+
situation
|
|
78
|
+
}] : []));
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/client/core.ts
|
|
81
|
+
/** This function gives an amount in milliseconds. "infinite" is `Infinity`. */
|
|
82
|
+
const ms = (amount) => amount.value === "infinite" ? Number.POSITIVE_INFINITY : valueIn(amount, "ms");
|
|
83
|
+
/** This function gives an amount in milliseconds, or undefined for "infinite". Each driver then writes its own form of "no timer". */
|
|
84
|
+
const finiteMs = (amount) => {
|
|
85
|
+
const value = ms(amount);
|
|
86
|
+
return Number.isFinite(value) ? value : void 0;
|
|
87
|
+
};
|
|
88
|
+
/** This function gives the text of an error. */
|
|
89
|
+
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
90
|
+
/** This function gives the addresses and the ports of a socket for a join. Each one is null when the socket does not give it. */
|
|
91
|
+
const socketEnds = (socket) => ({
|
|
92
|
+
localAddress: socket?.localAddress ?? null,
|
|
93
|
+
localPort: socket?.localPort ?? null,
|
|
94
|
+
remoteAddress: socket?.remoteAddress ?? null,
|
|
95
|
+
remotePort: socket?.remotePort ?? null
|
|
96
|
+
});
|
|
97
|
+
/** This function declares a client in a service: its dependency, its handle, and each of its checks. */
|
|
98
|
+
const attachClient = (svc, name, declaration, handle, checks) => {
|
|
99
|
+
svc.dependency(name, declaration, handle);
|
|
100
|
+
for (const [check, run] of checks) svc.check(check, run);
|
|
101
|
+
return svc;
|
|
102
|
+
};
|
|
103
|
+
/** This function gives the table of operations of a client: the defaults of its driver, then the operations of its spec. */
|
|
104
|
+
const operationTable = (defaults, more) => ({
|
|
105
|
+
...defaults,
|
|
106
|
+
...more
|
|
107
|
+
});
|
|
108
|
+
/** This function makes the recorder of a client. Each event has the machine "client", and the name of the client as its instance. */
|
|
109
|
+
const clientRecorder = (record, clock, name) => (from, to, on, detail) => record?.({
|
|
110
|
+
at: clock.now(),
|
|
111
|
+
machine: "client",
|
|
112
|
+
instance: name,
|
|
113
|
+
from,
|
|
114
|
+
to,
|
|
115
|
+
on,
|
|
116
|
+
...detail === void 0 ? {} : { detail }
|
|
117
|
+
});
|
|
118
|
+
/** This function gives the outcome of a call that did not leave the client. It is certain: the call did not apply. */
|
|
119
|
+
const notSent = (error) => ({
|
|
120
|
+
outcome: "error",
|
|
121
|
+
error,
|
|
122
|
+
reply: false
|
|
123
|
+
});
|
|
124
|
+
/** This function makes the record of the calls in flight of a client. */
|
|
125
|
+
const inFlight = () => {
|
|
126
|
+
const closes = /* @__PURE__ */ new Set();
|
|
127
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
128
|
+
const wake = () => {
|
|
129
|
+
if (closes.size > 0) return;
|
|
130
|
+
for (const waiter of [...waiters]) waiter();
|
|
131
|
+
waiters.clear();
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
get size() {
|
|
135
|
+
return closes.size;
|
|
136
|
+
},
|
|
137
|
+
idle: () => new Promise((resolve) => {
|
|
138
|
+
if (closes.size === 0) resolve();
|
|
139
|
+
else waiters.add(resolve);
|
|
140
|
+
}),
|
|
141
|
+
track(signal, abandoned, start, after) {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
let settled = false;
|
|
144
|
+
const settle = (value, cause) => {
|
|
145
|
+
if (settled) return;
|
|
146
|
+
settled = true;
|
|
147
|
+
closes.delete(close);
|
|
148
|
+
signal?.removeEventListener("abort", aborted);
|
|
149
|
+
after?.(value, cause);
|
|
150
|
+
resolve(value);
|
|
151
|
+
wake();
|
|
152
|
+
};
|
|
153
|
+
const close = (error) => settle({
|
|
154
|
+
outcome: "indeterminate",
|
|
155
|
+
error
|
|
156
|
+
}, new Error(error));
|
|
157
|
+
const aborted = () => close(abandoned);
|
|
158
|
+
closes.add(close);
|
|
159
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
160
|
+
start(settle);
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
settleAll(error) {
|
|
164
|
+
for (const close of [...closes]) close(error);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* This function sends one call through the record of the calls in flight, with a timer of the
|
|
170
|
+
* wrapper. A driver with no timer for its call uses it: amqplib, and the agent of Node. At `bound`
|
|
171
|
+
* milliseconds the call is indeterminate with the error `timedOut`, because it left the client.
|
|
172
|
+
* The timer stops when the call settles, before `after` operates.
|
|
173
|
+
*/
|
|
174
|
+
const trackWithTimer = (flight, clock, bound, signal, abandoned, timedOut, start, after) => {
|
|
175
|
+
let stop = () => {};
|
|
176
|
+
return flight.track(signal, abandoned, (settle) => {
|
|
177
|
+
stop = clock.after(bound, () => settle({
|
|
178
|
+
outcome: "indeterminate",
|
|
179
|
+
error: timedOut
|
|
180
|
+
}));
|
|
181
|
+
start(settle);
|
|
182
|
+
}, (value, cause) => {
|
|
183
|
+
stop();
|
|
184
|
+
after?.(value, cause);
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* This function waits for the work of a close, for `bound` milliseconds at most. It gives true when
|
|
189
|
+
* the work ended in its bound, with a success or a failure. It gives false at the bound. The driver
|
|
190
|
+
* then destroys each socket that stays, because a silent server does not close its half.
|
|
191
|
+
*/
|
|
192
|
+
const withinBound = (clock, bound, work) => {
|
|
193
|
+
const done = work.then(() => true, () => true);
|
|
194
|
+
let cancel = () => {};
|
|
195
|
+
const timer = new Promise((resolve) => {
|
|
196
|
+
cancel = clock.after(bound, () => resolve(false));
|
|
197
|
+
});
|
|
198
|
+
return Promise.race([done, timer]).then((ended) => {
|
|
199
|
+
cancel();
|
|
200
|
+
return ended;
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
/** This function makes the ready wait of a client. The function `ready` tells if the client is ready. */
|
|
204
|
+
const readyWait = (clock, ready) => {
|
|
205
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
206
|
+
return {
|
|
207
|
+
wait: (within, signal) => new Promise((resolve) => {
|
|
208
|
+
if (ready()) {
|
|
209
|
+
resolve(true);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
let cancel = () => {};
|
|
213
|
+
const done = (value) => {
|
|
214
|
+
waiters.delete(wake);
|
|
215
|
+
cancel();
|
|
216
|
+
signal?.removeEventListener("abort", aborted);
|
|
217
|
+
resolve(value);
|
|
218
|
+
};
|
|
219
|
+
const wake = () => done(true);
|
|
220
|
+
const aborted = () => done(false);
|
|
221
|
+
waiters.add(wake);
|
|
222
|
+
cancel = clock.after(within, () => done(ready()));
|
|
223
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
224
|
+
}),
|
|
225
|
+
wake() {
|
|
226
|
+
for (const wake of [...waiters]) wake();
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* This function gives the backoff of the tries to connect: from `retry_base`, doubling to
|
|
232
|
+
* `retry_cap`, each with a jitter up to the base. The client rounds each delay to a whole millisecond.
|
|
233
|
+
*/
|
|
234
|
+
const retryBackoff = (knobs) => ({
|
|
235
|
+
base: ms(knobs.retry_base),
|
|
236
|
+
factor: 2,
|
|
237
|
+
cap: ms(knobs.retry_cap),
|
|
238
|
+
jitter: {
|
|
239
|
+
kind: "add",
|
|
240
|
+
amount: ms(knobs.retry_base)
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
/** This function makes the retry schedule of a client, from its backoff and its random numbers. RabbitMQ and WebSocket use it. */
|
|
244
|
+
const retrySchedule = (clock, backoff, random) => {
|
|
245
|
+
let tries = 0;
|
|
246
|
+
let timer = null;
|
|
247
|
+
return {
|
|
248
|
+
get tries() {
|
|
249
|
+
return tries;
|
|
250
|
+
},
|
|
251
|
+
schedule(due) {
|
|
252
|
+
tries += 1;
|
|
253
|
+
const delay = Math.round(backoffDelay(backoff, tries, random));
|
|
254
|
+
timer?.();
|
|
255
|
+
timer = clock.after(delay, () => {
|
|
256
|
+
timer = null;
|
|
257
|
+
due();
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
cancel() {
|
|
261
|
+
timer?.();
|
|
262
|
+
timer = null;
|
|
263
|
+
},
|
|
264
|
+
reset() {
|
|
265
|
+
tries = 0;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
/** This function gives the problems of the retry knobs: the cap must not be below the base. */
|
|
270
|
+
const retryProblems = (name, knobs) => ms(knobs.retry_cap) < ms(knobs.retry_base) ? [`${name}'s retry_cap is below its retry_base`] : [];
|
|
271
|
+
/**
|
|
272
|
+
* This function gives the problem of a count of misses that is not a whole number above zero. A
|
|
273
|
+
* client that closes its connection after a count of heartbeat misses uses it. `mechanism` names the heartbeat, for example "watchdog".
|
|
274
|
+
*/
|
|
275
|
+
const missesProblems = (name, misses, mechanism) => Number.isInteger(misses) && misses >= 1 ? [] : [`${name}'s ${mechanism} closes the connection after ${misses} misses, not a whole number above zero`];
|
|
276
|
+
/** This function gives the problems of the knobs of a spec that each client has: each knob must be a time of zero or more. */
|
|
277
|
+
const timeProblems = (name, knobs) => {
|
|
278
|
+
const problems = [];
|
|
279
|
+
for (const [knob, amount] of Object.entries(knobs)) if (amount.value !== "infinite" && !(ms(amount) >= 0)) problems.push(`${name}'s ${knob} is not a time of zero or more`);
|
|
280
|
+
return problems;
|
|
281
|
+
};
|
|
282
|
+
/** This function writes a bound for a driver that reads 0 as no timer: "infinite" is 0. */
|
|
283
|
+
const orZero = (amount) => finiteMs(amount) ?? 0;
|
|
284
|
+
/** This function tells if an amount is a whole number of milliseconds. "infinite" is whole. */
|
|
285
|
+
const wholeMs = (amount) => amount.value === "infinite" || Number.isInteger(ms(amount));
|
|
286
|
+
/** This function tells if an amount is a whole number of seconds. "infinite" is whole. */
|
|
287
|
+
const wholeSeconds = (amount) => amount.value === "infinite" || Number.isInteger(valueIn(amount, "s"));
|
|
288
|
+
const knobOf = (knobs, knob) => knobs[knob];
|
|
289
|
+
/**
|
|
290
|
+
* This function gives the problems of the knobs of a driver that reads 0 as no timer. pg and the
|
|
291
|
+
* MongoDB driver read 0 so. A bounded knob must be a finite time above zero. An open knob can be
|
|
292
|
+
* "infinite", and 0 is then a mistake.
|
|
293
|
+
*/
|
|
294
|
+
const zeroProblems = (name, driver, knobs, kinds) => {
|
|
295
|
+
const problems = [];
|
|
296
|
+
for (const knob of kinds.bounded ?? []) {
|
|
297
|
+
const value = ms(knobOf(knobs, knob));
|
|
298
|
+
if (!(value > 0) || !Number.isFinite(value)) problems.push(`${name}'s ${knob} is not a finite time above zero: ${driver} reads 0 as no bound`);
|
|
299
|
+
}
|
|
300
|
+
for (const knob of kinds.open ?? []) {
|
|
301
|
+
const amount = knobOf(knobs, knob);
|
|
302
|
+
if (amount.value !== "infinite" && !(ms(amount) > 0)) problems.push(`${name}'s ${knob} is zero: give "infinite" for no bound`);
|
|
303
|
+
}
|
|
304
|
+
return problems;
|
|
305
|
+
};
|
|
306
|
+
/** This function gives the problems of the knobs that a driver takes in whole milliseconds or in whole seconds. */
|
|
307
|
+
const wholeProblems = (name, knobs, kinds) => [...(kinds.millis ?? []).filter((knob) => !wholeMs(knobOf(knobs, knob))).map((knob) => `${name}'s ${knob} is not a whole number of milliseconds`), ...(kinds.seconds ?? []).filter((knob) => !wholeSeconds(knobOf(knobs, knob))).map((knob) => `${name}'s ${knob} is not a whole number of seconds`)];
|
|
308
|
+
/** This function gives the problems of the sizes of a pool: at most `max` connections, and `min` from 0 to `max`. */
|
|
309
|
+
const poolProblems = (name, pool) => {
|
|
310
|
+
const problems = [];
|
|
311
|
+
if (!Number.isInteger(pool.max) || pool.max < 1) problems.push(`${name}'s pool has a max of ${pool.max}, not a whole number above zero`);
|
|
312
|
+
if (!Number.isInteger(pool.min) || pool.min < 0 || pool.min > pool.max) problems.push(`${name}'s pool has a min of ${pool.min}, not a whole number from 0 to its max`);
|
|
313
|
+
return problems;
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* This function gives the problem O07 of a client whose server has a deadline, or null. The client
|
|
317
|
+
* must wait longer than the deadline of the server. Then the server ends a slow call with an error
|
|
318
|
+
* first. Otherwise the client gives up first, and the server goes on with the call. `call` names
|
|
319
|
+
* the call of the server, for example a statement.
|
|
320
|
+
*/
|
|
321
|
+
const deadlineProblem = (name, request, deadline, call) => deadline.value !== "infinite" && ms(request) <= ms(deadline) ? `${name}'s request_timeout is not above its statement_timeout: the server goes on with a ${call} that the client gave up (O07)` : null;
|
|
322
|
+
/**
|
|
323
|
+
* This function gives the problem O01 of a client pool, or null. The pool must close an idle
|
|
324
|
+
* connection before its server does. Otherwise a request can go on a connection that the server
|
|
325
|
+
* closed. A server that keeps an idle connection with no bound gives no problem. `knob` names the
|
|
326
|
+
* idle time of the client: `pool_idle_expiry` of a pool, or `client_idle_timeout` of a gRPC channel.
|
|
327
|
+
*/
|
|
328
|
+
const idleCloseProblem = (name, expiry, close, knob = "pool_idle_expiry") => close.value !== "infinite" && !(ms(expiry) < ms(close)) ? `${name}'s ${knob} is not below the idle close of its server: a request can go on a connection that the server closed (O01)` : null;
|
|
329
|
+
/**
|
|
330
|
+
* This function gives the problems of the timers that the wrapper has, because its driver has none.
|
|
331
|
+
* Each one must be a finite time above zero. `bounds` gives what each knob bounds, for example "a call".
|
|
332
|
+
*/
|
|
333
|
+
const wrapperTimerProblems = (name, knobs, bounds) => Object.entries(bounds).filter(([knob]) => {
|
|
334
|
+
const value = ms(knobOf(knobs, knob));
|
|
335
|
+
return !(value > 0) || !Number.isFinite(value);
|
|
336
|
+
}).map(([knob, what]) => `${name}'s ${knob} is not a finite time above zero: the timer of the wrapper is the only bound of ${what}`);
|
|
337
|
+
/** This function gives the problem of a finite connection age, for a client that does not replace a connection at an age. `reason` tells why. */
|
|
338
|
+
const ageProblems = (name, knobs, reason) => knobs.max_connection_age.value === "infinite" ? [] : [`${name}'s max_connection_age is not "infinite": ${reason}`];
|
|
339
|
+
/** This function gives the problem of a keepalive that is not a finite time above zero, for a driver that always turns keepalive on. */
|
|
340
|
+
const keepaliveOnProblems = (name, knobs, driver) => {
|
|
341
|
+
const value = ms(knobs.keepalive_idle);
|
|
342
|
+
return value > 0 && Number.isFinite(value) ? [] : [`${name}'s keepalive_idle is not a finite time above zero: ${driver} always turns keepalive on`];
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* This function sends a call, and gives its outcome in three values. It sends again only an
|
|
346
|
+
* idempotent operation, one time at most, after an outcome that is not certain. A reply of the
|
|
347
|
+
* server is certain. The second try ends at the bound of the call, not one bound after its own start.
|
|
348
|
+
*/
|
|
349
|
+
const sendWithReissue = async (call) => {
|
|
350
|
+
const { clock, bound } = call;
|
|
351
|
+
const { signal } = call.options;
|
|
352
|
+
const operation = call.options.reissue === false ? void 0 : call.operation;
|
|
353
|
+
const started = clock.now();
|
|
354
|
+
const first = await call.attempt(signal);
|
|
355
|
+
if (first.outcome === "ok") return {
|
|
356
|
+
outcome: "ok",
|
|
357
|
+
value: first.value,
|
|
358
|
+
attempts: 1
|
|
359
|
+
};
|
|
360
|
+
const again = !(first.outcome === "error" && first.reply) && operation !== void 0 && signal?.aborted !== true && mayReissue(operation, quantity((clock.now() - started) / 1e3, "s"));
|
|
361
|
+
const left = bound - (clock.now() - started);
|
|
362
|
+
if (!again || left <= 0) return {
|
|
363
|
+
...first,
|
|
364
|
+
attempts: 1
|
|
365
|
+
};
|
|
366
|
+
if (call.beforeAgain !== void 0 && !await call.beforeAgain(left, signal)) return {
|
|
367
|
+
...first,
|
|
368
|
+
attempts: 1
|
|
369
|
+
};
|
|
370
|
+
call.onReissue();
|
|
371
|
+
const end = new AbortController();
|
|
372
|
+
const stop = clock.after(Math.max(0, bound - (clock.now() - started)), () => end.abort());
|
|
373
|
+
const forward = () => end.abort();
|
|
374
|
+
signal?.addEventListener("abort", forward, { once: true });
|
|
375
|
+
const second = await call.attempt(end.signal);
|
|
376
|
+
stop();
|
|
377
|
+
signal?.removeEventListener("abort", forward);
|
|
378
|
+
return second.outcome === "ok" ? {
|
|
379
|
+
outcome: "ok",
|
|
380
|
+
value: second.value,
|
|
381
|
+
attempts: 2
|
|
382
|
+
} : {
|
|
383
|
+
...second,
|
|
384
|
+
attempts: 2
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
/**
|
|
388
|
+
* This function gives the checks of a client, for `svc.check(check, run)`. It gives a check only for
|
|
389
|
+
* each step that the options name, and only when the client has a function for that step.
|
|
390
|
+
*/
|
|
391
|
+
const stepChecks = (name, options, runs) => {
|
|
392
|
+
const made = [];
|
|
393
|
+
for (const [step, run] of Object.entries(runs)) {
|
|
394
|
+
if (run === void 0 || !(step in options.steps)) continue;
|
|
395
|
+
made.push([{
|
|
396
|
+
name: `${name}.${step}`,
|
|
397
|
+
dependency: name,
|
|
398
|
+
step,
|
|
399
|
+
ownConnection: run.ownConnection,
|
|
400
|
+
gates: options.steps[step] ?? null,
|
|
401
|
+
poll: options.poll
|
|
402
|
+
}, run.run]);
|
|
403
|
+
}
|
|
404
|
+
return made;
|
|
405
|
+
};
|
|
406
|
+
/** This function makes a check outcome from a sent result: only `ok` passes. */
|
|
407
|
+
const passes = (sent) => sent.outcome === "ok" ? "ok" : "error";
|
|
408
|
+
/** This check gives the state of the client, without a round trip. */
|
|
409
|
+
const localStateCheck = (ready) => ({
|
|
410
|
+
ownConnection: true,
|
|
411
|
+
run: () => ready() ? "ok" : "error"
|
|
412
|
+
});
|
|
413
|
+
/**
|
|
414
|
+
* This check sends one call on the connection of the application. A check measures the step at this time.
|
|
415
|
+
* Thus it does not send the call again, and only `ok` passes.
|
|
416
|
+
*/
|
|
417
|
+
const roundTripCheck = (send) => ({
|
|
418
|
+
ownConnection: true,
|
|
419
|
+
run: async (signal) => passes(await send({
|
|
420
|
+
signal,
|
|
421
|
+
reissue: false
|
|
422
|
+
}))
|
|
423
|
+
});
|
|
424
|
+
/**
|
|
425
|
+
* This check opens a new connection each time. It finds a server that refuses connections, but not
|
|
426
|
+
* a stalled connection of the application. A failed dial throws, and the poller counts the throw as an error.
|
|
427
|
+
*/
|
|
428
|
+
const dialCheck = (dial, host, port) => ({
|
|
429
|
+
ownConnection: false,
|
|
430
|
+
run: async (signal) => {
|
|
431
|
+
await dial(host, port, signal);
|
|
432
|
+
return "ok";
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
/** This function gives the bounds of a client that `clientProblems` examines, from its knobs. */
|
|
436
|
+
const clientBounds = (knobs) => ({
|
|
437
|
+
close: ms(knobs.close_bound),
|
|
438
|
+
request: ms(knobs.request_timeout)
|
|
439
|
+
});
|
|
440
|
+
/**
|
|
441
|
+
* This function gives the problems of a client in a description:
|
|
442
|
+
* - The client needs the phase "tear down dependencies", and its close must fit in the cap of that phase.
|
|
443
|
+
* - A round-trip check must end in one jittered poll (B7, O13).
|
|
444
|
+
*/
|
|
445
|
+
const clientProblems = (name, description, bounds) => {
|
|
446
|
+
const found = [];
|
|
447
|
+
const teardown = description.shutdown.phases.find((phase) => phase.name === "tear down dependencies");
|
|
448
|
+
if (teardown === void 0) found.push(`${name} needs the phase "tear down dependencies" to close`);
|
|
449
|
+
else if (bounds.close >= valueIn(teardown.cap, "ms")) found.push(`${name}'s close_bound of ${bounds.close} ms does not fit in the cap of "tear down dependencies"`);
|
|
450
|
+
for (const check of description.checks) {
|
|
451
|
+
if (check.dependency !== name || check.step !== "round-trip") continue;
|
|
452
|
+
const slowest = valueIn(check.poll.timeout, "ms") + valueIn(check.poll.interval, "ms") * (1 + check.poll.jitter);
|
|
453
|
+
if (bounds.request > slowest) found.push(`${name}'s request_timeout outlives one jittered poll of ${check.name} (B7, O13)`);
|
|
454
|
+
}
|
|
455
|
+
return found;
|
|
456
|
+
};
|
|
457
|
+
/**
|
|
458
|
+
* This poll is a poll of the checks to start from: each 5 s with a jitter of 20 %, and a timeout of
|
|
459
|
+
* 4 s (B6, O12). Its slowest poll is 4 s + 5 s × 1.2 = 10 s, the slowest poll of the cells of O13.
|
|
460
|
+
* It does not depend on a driver. The starting knobs of each driver fit it.
|
|
461
|
+
*/
|
|
462
|
+
const STARTING_POLL = {
|
|
463
|
+
interval: {
|
|
464
|
+
value: 5,
|
|
465
|
+
unit: "s"
|
|
466
|
+
},
|
|
467
|
+
jitter: .2,
|
|
468
|
+
timeout: {
|
|
469
|
+
value: 4,
|
|
470
|
+
unit: "s"
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/client/dial.ts
|
|
475
|
+
/**
|
|
476
|
+
* This function makes a dial function. The dial opens a TCP connection, sends `hello`, and reads the
|
|
477
|
+
* answer until `judge` decides. An abort, an error of the socket, and a close before the decision fail the dial.
|
|
478
|
+
* The dial destroys its socket at its end.
|
|
479
|
+
*/
|
|
480
|
+
const helloDial = (hello, judge) => (host, port, signal) => new Promise((resolve, reject) => {
|
|
481
|
+
if (signal.aborted) {
|
|
482
|
+
reject(/* @__PURE__ */ new Error("the dial was aborted"));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const socket = connect({
|
|
486
|
+
host,
|
|
487
|
+
port
|
|
488
|
+
});
|
|
489
|
+
let received = Buffer.alloc(0);
|
|
490
|
+
const end = (error) => {
|
|
491
|
+
signal.removeEventListener("abort", onAbort);
|
|
492
|
+
socket.destroy();
|
|
493
|
+
if (error === void 0) resolve();
|
|
494
|
+
else reject(error);
|
|
495
|
+
};
|
|
496
|
+
const onAbort = () => end(/* @__PURE__ */ new Error("the dial was aborted"));
|
|
497
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
498
|
+
socket.once("connect", () => socket.write(hello));
|
|
499
|
+
socket.on("data", (chunk) => {
|
|
500
|
+
received = Buffer.concat([received, chunk]);
|
|
501
|
+
const verdict = judge(received);
|
|
502
|
+
if (verdict === "more") return;
|
|
503
|
+
end(verdict ?? void 0);
|
|
504
|
+
});
|
|
505
|
+
socket.once("error", (error) => end(error));
|
|
506
|
+
socket.once("close", () => end(/* @__PURE__ */ new Error("the server closed the dial")));
|
|
507
|
+
});
|
|
508
|
+
/** This function gives the authority of a host and a port for a Host header. An IPv6 address goes in brackets. */
|
|
509
|
+
const authority = (host, port) => `${host.includes(":") ? `[${host}]` : host}:${port}`;
|
|
510
|
+
/**
|
|
511
|
+
* This function makes a judge of the status line of an HTTP/1.1 answer. The judge waits for the end
|
|
512
|
+
* of the line, in the first 8 KiB. Then `accept` reads the status code, and gives null for a good status or an error.
|
|
513
|
+
* The HTTP client accepts each status. The WebSocket client accepts only 101.
|
|
514
|
+
*/
|
|
515
|
+
const statusLineJudge = (accept) => (received) => {
|
|
516
|
+
const end = received.indexOf("\r\n");
|
|
517
|
+
if (end < 0) return received.length > 8192 ? /* @__PURE__ */ new Error("the answer has no status line in its first 8 KiB") : "more";
|
|
518
|
+
const line = received.toString("latin1", 0, end);
|
|
519
|
+
const status = /^HTTP\/1\.[01] (\d{3})/.exec(line);
|
|
520
|
+
return status === null ? /* @__PURE__ */ new Error(`the answer ${JSON.stringify(line)} is not an HTTP/1.1 status line`) : accept(Number(status[1]), line);
|
|
521
|
+
};
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/client/machine.ts
|
|
524
|
+
/** This function gives one transition of a machine document. */
|
|
525
|
+
const transition = (id, from, on, to, extra = {}) => ({
|
|
526
|
+
id,
|
|
527
|
+
from,
|
|
528
|
+
on,
|
|
529
|
+
to,
|
|
530
|
+
...extra
|
|
531
|
+
});
|
|
532
|
+
/** This function gives a knob of a client in a machine document, by the option of the driver that it sets. */
|
|
533
|
+
const clientKnob = (key) => ({
|
|
534
|
+
key,
|
|
535
|
+
scope: "client",
|
|
536
|
+
unit: "s",
|
|
537
|
+
default: null
|
|
538
|
+
});
|
|
539
|
+
/** This timer is the bound of the close. The close request arms it, and the end cancels it. At its end, the driver destroys each socket. */
|
|
540
|
+
const closeTimer = () => ({
|
|
541
|
+
duration: {
|
|
542
|
+
knob: "close_bound",
|
|
543
|
+
unit: "s"
|
|
544
|
+
},
|
|
545
|
+
arms: ["close.request"],
|
|
546
|
+
cancels: ["status.end"],
|
|
547
|
+
fires: "close.bound"
|
|
548
|
+
});
|
|
549
|
+
//#endregion
|
|
550
|
+
export { retrySchedule as A, zeroProblems as B, notSent as C, readyWait as D, poolProblems as E, timeProblems as F, immediate as G, NEVER as H, trackWithTimer as I, keepaliveSilent as J, keepaliveAnswered as K, wholeProblems as L, sendWithReissue as M, socketEnds as N, retryBackoff as O, stepChecks as P, withinBound as R, ms as S, orZero as T, WHAT as U, MANIFESTATIONS as V, firstDetector as W, unwatched as X, kernelGiveUp as Y, inFlight as _, helloDial as a, message as b, ageProblems as c, clientProblems as d, clientRecorder as f, idleCloseProblem as g, finiteMs as h, authority as i, roundTripCheck as j, retryProblems as k, attachClient as l, dialCheck as m, closeTimer as n, statusLineJudge as o, deadlineProblem as p, keepaliveReset as q, transition as r, STARTING_POLL as s, clientKnob as t, clientBounds as u, keepaliveOnProblems as v, operationTable as w, missesProblems as x, localStateCheck as y, wrapperTimerProblems as z };
|
|
551
|
+
|
|
552
|
+
//# sourceMappingURL=machine-BhTRu3u9.js.map
|