@mark1russell7/service 0.5.12 → 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 +93 -1
- package/build/{core-C2wnF4mg.d.ts → core-BPLRDcUJ.d.ts} +45 -2
- 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/index.d.ts +3 -147
- package/build/{machine-Czfb6fV4.js → machine-BhTRu3u9.js} +182 -2
- 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 +1 -1
- package/build/pg.js +30 -33
- package/build/pg.js.map +1 -1
- 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 +3 -9
- package/build/redis.js +6 -38
- package/build/redis.js.map +1 -1
- package/build/websocket.d.ts +275 -0
- package/build/websocket.js +780 -0
- package/build/websocket.js.map +1 -0
- package/package.json +38 -1
- package/build/machine-Czfb6fV4.js.map +0 -1
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { realClock } from "./clock.js";
|
|
2
|
+
import { B as zeroProblems, C as notSent, E as poolProblems, F as timeProblems, G as immediate, I as trackWithTimer, J as keepaliveSilent, K as keepaliveAnswered, L as wholeProblems, M as sendWithReissue, N as socketEnds, P as stepChecks, R as withinBound, S as ms, U as WHAT, V as MANIFESTATIONS, W as firstDetector, X as unwatched, Y as kernelGiveUp, _ as inFlight, a as helloDial, b as message, c as ageProblems, d as clientProblems, f as clientRecorder, g as idleCloseProblem, i as authority, j as roundTripCheck, l as attachClient, m as dialCheck, n as closeTimer, o as statusLineJudge, q as keepaliveReset, r as transition, s as STARTING_POLL, t as clientKnob, u as clientBounds, v as keepaliveOnProblems, w as operationTable, y as localStateCheck, z as wrapperTimerProblems } from "./machine-BhTRu3u9.js";
|
|
3
|
+
//#region src/http-client/connection.ts
|
|
4
|
+
/**
|
|
5
|
+
* This factory makes an `http.Agent` of Node. It loads node:http only when a client connects.
|
|
6
|
+
* Each request goes through `http.request` with the agent. The response settles at the end of its body.
|
|
7
|
+
* A body that ends before it is complete rejects the response.
|
|
8
|
+
*/
|
|
9
|
+
const nodeAgent = async (options) => {
|
|
10
|
+
const http = await import("node:http");
|
|
11
|
+
const agent = new http.Agent(options);
|
|
12
|
+
const all = (lists) => Object.values(lists).flatMap((list) => list ?? []);
|
|
13
|
+
return {
|
|
14
|
+
request(request, events) {
|
|
15
|
+
const req = http.request({
|
|
16
|
+
host: request.host,
|
|
17
|
+
port: request.port,
|
|
18
|
+
method: request.method,
|
|
19
|
+
path: request.path,
|
|
20
|
+
headers: request.headers,
|
|
21
|
+
agent
|
|
22
|
+
});
|
|
23
|
+
const response = new Promise((resolve, reject) => {
|
|
24
|
+
req.on("socket", (socket) => {
|
|
25
|
+
events.socket(socket, req.reusedSocket);
|
|
26
|
+
if (socket.connecting) socket.once("connect", () => events.connect());
|
|
27
|
+
else if (!req.reusedSocket) events.connect();
|
|
28
|
+
});
|
|
29
|
+
req.on("response", (incoming) => {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
incoming.on("data", (chunk) => chunks.push(chunk));
|
|
32
|
+
incoming.on("error", reject);
|
|
33
|
+
incoming.on("end", () => resolve({
|
|
34
|
+
status: incoming.statusCode ?? 0,
|
|
35
|
+
headers: incoming.headers,
|
|
36
|
+
body: Buffer.concat(chunks)
|
|
37
|
+
}));
|
|
38
|
+
incoming.on("close", () => {
|
|
39
|
+
if (!incoming.complete) reject(/* @__PURE__ */ new Error("the connection closed before the end of the body"));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
req.on("error", reject);
|
|
43
|
+
});
|
|
44
|
+
req.end(request.body);
|
|
45
|
+
return {
|
|
46
|
+
response,
|
|
47
|
+
abort: (error) => req.destroy(error)
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
sockets: () => [...all(agent.sockets), ...all(agent.freeSockets)],
|
|
51
|
+
destroy: () => agent.destroy()
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/http-client/meets.ts
|
|
56
|
+
/**
|
|
57
|
+
* This function gives the manifestations that an HTTP client can meet, and the detectors of each,
|
|
58
|
+
* from its knobs. The HTTP cells measured them on the agent of Node 24.21.0:
|
|
59
|
+
* - The agent has no timer of a call, thus the timer of the wrapper finds each silent call in flight.
|
|
60
|
+
* - The free socket timer closes an idle socket at `pool_idle_expiry` at most. Thus no request goes
|
|
61
|
+
* later on a socket that went silent while it was free.
|
|
62
|
+
* - A FIN on a busy socket fails the call with "socket hang up". A FIN on a free socket destroys it.
|
|
63
|
+
*/
|
|
64
|
+
const httpClientMeetings = (name, knobs) => {
|
|
65
|
+
const call = {
|
|
66
|
+
by: "request_timeout",
|
|
67
|
+
within: knobs.request_timeout,
|
|
68
|
+
situation: "in flight",
|
|
69
|
+
effect: `the timer of the wrapper ends the call as indeterminate, and ${name} destroys its socket`
|
|
70
|
+
};
|
|
71
|
+
const idleExpiry = (situation) => ({
|
|
72
|
+
by: "pool_idle_expiry",
|
|
73
|
+
within: knobs.pool_idle_expiry,
|
|
74
|
+
situation,
|
|
75
|
+
effect: "the free socket timer of the agent closes the socket, and the next request opens a new socket"
|
|
76
|
+
});
|
|
77
|
+
const giveUp = kernelGiveUp(knobs.retrans_giveup_time, "the call fails as indeterminate");
|
|
78
|
+
const silent = (manifestation) => ({
|
|
79
|
+
manifestation,
|
|
80
|
+
what: WHAT[manifestation],
|
|
81
|
+
detectors: [
|
|
82
|
+
call,
|
|
83
|
+
giveUp,
|
|
84
|
+
idleExpiry("idle"),
|
|
85
|
+
keepaliveSilent(knobs.keepalive_idle)
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
return [
|
|
89
|
+
{
|
|
90
|
+
manifestation: "rst",
|
|
91
|
+
what: WHAT.rst,
|
|
92
|
+
detectors: [
|
|
93
|
+
immediate("in flight", "the call fails with ECONNRESET as indeterminate, and the next request opens a new socket"),
|
|
94
|
+
idleExpiry("idle"),
|
|
95
|
+
keepaliveReset(knobs.keepalive_idle)
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
manifestation: "fin",
|
|
100
|
+
what: WHAT.fin,
|
|
101
|
+
detectors: [immediate("in flight", "the call fails with \"socket hang up\" as indeterminate"), immediate("idle", "the agent destroys the free socket, and the next request opens a new socket")]
|
|
102
|
+
},
|
|
103
|
+
silent("blackhole"),
|
|
104
|
+
silent("freeze-host"),
|
|
105
|
+
{
|
|
106
|
+
manifestation: "freeze-process",
|
|
107
|
+
what: WHAT["freeze-process"],
|
|
108
|
+
detectors: [
|
|
109
|
+
call,
|
|
110
|
+
idleExpiry("idle"),
|
|
111
|
+
keepaliveAnswered()
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
manifestation: "app-stall",
|
|
116
|
+
what: "the server holds its reply, for example in a slow handler, and its kernel stays up",
|
|
117
|
+
detectors: [
|
|
118
|
+
call,
|
|
119
|
+
idleExpiry("idle"),
|
|
120
|
+
keepaliveAnswered()
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
];
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/http-client/client.ts
|
|
127
|
+
/**
|
|
128
|
+
* This request asks about the server, not about a resource (RFC 9110 §9.3.7). Thus it changes no
|
|
129
|
+
* state. A server that does not route it still answers with a status, and the status tells that it is alive.
|
|
130
|
+
*/
|
|
131
|
+
const OPTIONS_STAR = {
|
|
132
|
+
method: "OPTIONS",
|
|
133
|
+
path: "*"
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* These operations are the methods of HTTP, by their idempotency in RFC 9110 §9.2.2. A second
|
|
137
|
+
* request of GET, HEAD, OPTIONS, TRACE, PUT or DELETE has the same effect as one. POST and PATCH do not.
|
|
138
|
+
*/
|
|
139
|
+
const HTTP_OPERATIONS = {
|
|
140
|
+
GET: { idempotent: true },
|
|
141
|
+
HEAD: { idempotent: true },
|
|
142
|
+
OPTIONS: { idempotent: true },
|
|
143
|
+
TRACE: { idempotent: true },
|
|
144
|
+
PUT: { idempotent: true },
|
|
145
|
+
DELETE: { idempotent: true },
|
|
146
|
+
POST: { idempotent: false },
|
|
147
|
+
PATCH: { idempotent: false }
|
|
148
|
+
};
|
|
149
|
+
/** This function gives the problems of a spec: the knobs that the agent cannot use. */
|
|
150
|
+
const specProblems = (name, spec) => {
|
|
151
|
+
const { knobs } = spec;
|
|
152
|
+
const problems = [
|
|
153
|
+
...timeProblems(name, knobs),
|
|
154
|
+
...wrapperTimerProblems(name, knobs, {
|
|
155
|
+
connect_bound: "a connect",
|
|
156
|
+
request_timeout: "a call"
|
|
157
|
+
}),
|
|
158
|
+
...zeroProblems(name, "the agent", knobs, { bounded: ["pool_idle_expiry"] }),
|
|
159
|
+
...keepaliveOnProblems(name, knobs, "the agent"),
|
|
160
|
+
...wholeProblems(name, knobs, {
|
|
161
|
+
millis: [
|
|
162
|
+
"connect_bound",
|
|
163
|
+
"request_timeout",
|
|
164
|
+
"pool_idle_expiry",
|
|
165
|
+
"hint_buffer"
|
|
166
|
+
],
|
|
167
|
+
seconds: ["keepalive_idle"]
|
|
168
|
+
}),
|
|
169
|
+
...poolProblems(name, {
|
|
170
|
+
max: spec.pool.max,
|
|
171
|
+
min: 0
|
|
172
|
+
}),
|
|
173
|
+
...ageProblems(name, knobs, "the agent does not replace a connection at an age")
|
|
174
|
+
];
|
|
175
|
+
if (!Number.isFinite(ms(knobs.hint_buffer))) problems.push(`${name}'s hint_buffer is not a finite time: the agent subtracts it from the hint of the server`);
|
|
176
|
+
return problems;
|
|
177
|
+
};
|
|
178
|
+
/** This function gives the options of `new http.Agent` from a spec. Each timer of the agent comes from the declaration. */
|
|
179
|
+
const agentOptions = (spec) => {
|
|
180
|
+
const { knobs } = spec;
|
|
181
|
+
return {
|
|
182
|
+
keepAlive: true,
|
|
183
|
+
timeout: ms(knobs.pool_idle_expiry),
|
|
184
|
+
agentKeepAliveTimeoutBuffer: ms(knobs.hint_buffer),
|
|
185
|
+
keepAliveMsecs: ms(knobs.keepalive_idle),
|
|
186
|
+
maxSockets: spec.pool.max,
|
|
187
|
+
maxFreeSockets: spec.pool.max,
|
|
188
|
+
scheduling: "lifo"
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
/** This function gives the bytes of the dial: `OPTIONS *` with the Host header, and a close after the answer. */
|
|
192
|
+
const optionsRequest = (host, port) => `OPTIONS * HTTP/1.1\r\nHost: ${authority(host, port)}\r\nConnection: close\r\n\r\n`;
|
|
193
|
+
/** This judge reads the status line of the answer. Any status passes, because it tells that an HTTP server answered. */
|
|
194
|
+
const statusJudge = statusLineJudge(() => null);
|
|
195
|
+
/** This dial function opens a TCP connection, sends `OPTIONS *`, and waits for the status line. It changes no state on the server. */
|
|
196
|
+
const httpDial = (host, port, signal) => helloDial(optionsRequest(host, port), statusJudge)(host, port, signal);
|
|
197
|
+
/**
|
|
198
|
+
* This function makes an HTTP client, a dependency of a service over an `http.Agent` of Node.
|
|
199
|
+
* - It sets each timer of the agent from the declaration. The agent sends no request again by itself,
|
|
200
|
+
* and the client sends again only an idempotent operation of its table.
|
|
201
|
+
* - Its call is a request until the end of the body of its response. The wrapper has a timer for the
|
|
202
|
+
* connect and one for the call, because the agent has none.
|
|
203
|
+
* - It tells the outcome of a call in three values. A request that did not get a connected socket
|
|
204
|
+
* did not leave the client. A failure after that is not certain.
|
|
205
|
+
* - It destroys the socket of each call that is not `ok`, thus no later request gets that socket.
|
|
206
|
+
* - It records each new socket of the pool (D1). HTTP gives no id of a connection.
|
|
207
|
+
*
|
|
208
|
+
* The core of the clients gives the outcomes, the second try, the calls in flight and the timer of a
|
|
209
|
+
* call. It also gives the bounded close and the checks. This entry gives what the agent does in its own way.
|
|
210
|
+
*/
|
|
211
|
+
const httpClient = (name, spec, options = {}) => {
|
|
212
|
+
const problems = specProblems(name, spec);
|
|
213
|
+
if (problems.length > 0) throw new Error(problems.join("; "));
|
|
214
|
+
const { knobs } = spec;
|
|
215
|
+
const clock = options.clock ?? realClock;
|
|
216
|
+
const create = options.create ?? nodeAgent;
|
|
217
|
+
const dial = options.dial ?? httpDial;
|
|
218
|
+
const operations = operationTable(HTTP_OPERATIONS, spec.operations);
|
|
219
|
+
const requestTimeout = ms(knobs.request_timeout);
|
|
220
|
+
const connectBound = ms(knobs.connect_bound);
|
|
221
|
+
const check = spec.check ?? OPTIONS_STAR;
|
|
222
|
+
let agent = null;
|
|
223
|
+
let status = "none";
|
|
224
|
+
const joins = [];
|
|
225
|
+
const flight = inFlight();
|
|
226
|
+
const waiting = /* @__PURE__ */ new Set();
|
|
227
|
+
const emit = clientRecorder(options.record, clock, name);
|
|
228
|
+
const record = (on, to = status, detail) => {
|
|
229
|
+
const from = status;
|
|
230
|
+
status = to;
|
|
231
|
+
emit(from, to, on, detail);
|
|
232
|
+
};
|
|
233
|
+
const closing = () => status === "closing" || status === "end";
|
|
234
|
+
/**
|
|
235
|
+
* This function makes one try of a call. The try waits for a socket of the agent: a free one, or a
|
|
236
|
+
* new one in `connect_bound`. A failure before the socket is ready is certain: the request did not
|
|
237
|
+
* leave the client. After that, the try waits for the whole response in the rest of `request_timeout`.
|
|
238
|
+
*/
|
|
239
|
+
const attempt = async (call, signal) => {
|
|
240
|
+
const a = agent;
|
|
241
|
+
if (a === null) return notSent(`${name} has not connected`);
|
|
242
|
+
if (closing()) return notSent(`${name} is ${status}: the request did not leave the client`);
|
|
243
|
+
if (signal?.aborted) return notSent("the caller aborted the request before it left the client");
|
|
244
|
+
const started = clock.now();
|
|
245
|
+
const body = typeof call.body === "string" ? Buffer.from(call.body, "utf8") : call.body;
|
|
246
|
+
const headers = {
|
|
247
|
+
...call.headers,
|
|
248
|
+
...body === void 0 ? {} : { "content-length": String(body.byteLength) }
|
|
249
|
+
};
|
|
250
|
+
let socket;
|
|
251
|
+
let reused = false;
|
|
252
|
+
let left = false;
|
|
253
|
+
let ending = false;
|
|
254
|
+
let connectTimer = () => {};
|
|
255
|
+
let decide = () => {};
|
|
256
|
+
const leaving = new Promise((resolve) => {
|
|
257
|
+
let decided = false;
|
|
258
|
+
decide = (value) => {
|
|
259
|
+
if (decided) return;
|
|
260
|
+
decided = true;
|
|
261
|
+
resolve(value);
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
let deliver = null;
|
|
265
|
+
let early = null;
|
|
266
|
+
let settled = false;
|
|
267
|
+
const settleWith = (value, cause) => {
|
|
268
|
+
if (deliver === null) early ??= {
|
|
269
|
+
value,
|
|
270
|
+
cause
|
|
271
|
+
};
|
|
272
|
+
else deliver(value, cause);
|
|
273
|
+
};
|
|
274
|
+
let exchange = null;
|
|
275
|
+
const stop = (text) => {
|
|
276
|
+
if (left) return;
|
|
277
|
+
ending = true;
|
|
278
|
+
connectTimer();
|
|
279
|
+
exchange?.abort(new Error(text));
|
|
280
|
+
decide(notSent(text));
|
|
281
|
+
};
|
|
282
|
+
const ready = () => {
|
|
283
|
+
left = true;
|
|
284
|
+
connectTimer();
|
|
285
|
+
decide(true);
|
|
286
|
+
};
|
|
287
|
+
exchange = a.request({
|
|
288
|
+
host: spec.host,
|
|
289
|
+
port: spec.port,
|
|
290
|
+
method: call.method,
|
|
291
|
+
path: call.path,
|
|
292
|
+
headers,
|
|
293
|
+
body
|
|
294
|
+
}, {
|
|
295
|
+
socket(given, isReused) {
|
|
296
|
+
socket = given;
|
|
297
|
+
reused = isReused;
|
|
298
|
+
if (isReused) {
|
|
299
|
+
ready();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
connectTimer = clock.after(connectBound, () => {
|
|
303
|
+
if (left) return;
|
|
304
|
+
record("connect.failed", closing() ? status : "failing", "no connection in connect_bound");
|
|
305
|
+
stop("no connection in connect_bound: the request did not leave the client");
|
|
306
|
+
});
|
|
307
|
+
},
|
|
308
|
+
connect() {
|
|
309
|
+
const generation = joins.length + 1;
|
|
310
|
+
joins.push({
|
|
311
|
+
generation,
|
|
312
|
+
at: clock.now(),
|
|
313
|
+
...socketEnds(socket)
|
|
314
|
+
});
|
|
315
|
+
record("join", closing() ? status : "ready", `socket ${generation}`);
|
|
316
|
+
ready();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
exchange.response.then((response) => settleWith({
|
|
320
|
+
outcome: "ok",
|
|
321
|
+
value: response
|
|
322
|
+
}), (error) => {
|
|
323
|
+
const cause = error instanceof Error ? error : new Error(message(error));
|
|
324
|
+
if (!left) {
|
|
325
|
+
connectTimer();
|
|
326
|
+
if (!ending) record("connect.failed", closing() ? status : "failing", message(error));
|
|
327
|
+
decide(notSent(message(error)));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (settled) return;
|
|
331
|
+
if (reused) record("reuse.failed", status, message(error));
|
|
332
|
+
settleWith({
|
|
333
|
+
outcome: "indeterminate",
|
|
334
|
+
error: message(error)
|
|
335
|
+
}, cause);
|
|
336
|
+
});
|
|
337
|
+
const abandoned = () => stop("the caller abandoned the request before it left the client");
|
|
338
|
+
signal?.addEventListener("abort", abandoned, { once: true });
|
|
339
|
+
const queued = clock.after(requestTimeout, () => stop("no socket in request_timeout: the request did not leave the client"));
|
|
340
|
+
waiting.add(stop);
|
|
341
|
+
const got = await leaving;
|
|
342
|
+
signal?.removeEventListener("abort", abandoned);
|
|
343
|
+
queued();
|
|
344
|
+
waiting.delete(stop);
|
|
345
|
+
if (got !== true) return got;
|
|
346
|
+
const sent = exchange;
|
|
347
|
+
return trackWithTimer(flight, clock, Math.max(0, requestTimeout - (clock.now() - started)), signal, "the caller abandoned the request after it left the client", "no whole response in request_timeout: the server can have the request", (settle) => {
|
|
348
|
+
deliver = settle;
|
|
349
|
+
if (early !== null) settle(early.value, early.cause);
|
|
350
|
+
}, (value, cause) => {
|
|
351
|
+
settled = true;
|
|
352
|
+
if (value.outcome !== "ok") sent.abort(cause ?? new Error(value.error));
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
const request = (call, sendOptions = {}) => {
|
|
356
|
+
const method = call.method.toUpperCase();
|
|
357
|
+
const named = call.operation ?? method;
|
|
358
|
+
return sendWithReissue({
|
|
359
|
+
clock,
|
|
360
|
+
bound: requestTimeout,
|
|
361
|
+
operation: operations[named],
|
|
362
|
+
options: sendOptions,
|
|
363
|
+
attempt: (signal) => attempt({
|
|
364
|
+
...call,
|
|
365
|
+
method
|
|
366
|
+
}, signal),
|
|
367
|
+
onReissue: () => record("command.reissue", status, named)
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
const handle = {
|
|
371
|
+
async connect(signal) {
|
|
372
|
+
record("status.connecting", "connecting");
|
|
373
|
+
agent = await create(agentOptions(spec));
|
|
374
|
+
if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);
|
|
375
|
+
await request(check, {
|
|
376
|
+
signal,
|
|
377
|
+
reissue: false
|
|
378
|
+
});
|
|
379
|
+
},
|
|
380
|
+
async close() {
|
|
381
|
+
const a = agent;
|
|
382
|
+
if (a === null || closing()) return;
|
|
383
|
+
record("close.request", "closing");
|
|
384
|
+
for (const stop of [...waiting]) stop("the client closed before the request left");
|
|
385
|
+
if (!await withinBound(clock, ms(knobs.close_bound), flight.idle())) {
|
|
386
|
+
record("close.bound");
|
|
387
|
+
const open = a.sockets().length;
|
|
388
|
+
flight.settleAll("the client closed with the request in flight");
|
|
389
|
+
a.destroy();
|
|
390
|
+
record("close.destroy", status, `open sockets: ${open}`);
|
|
391
|
+
} else a.destroy();
|
|
392
|
+
record("status.end", "end");
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
const declaration = {
|
|
396
|
+
system: spec.system,
|
|
397
|
+
knobs: {
|
|
398
|
+
connect_bound: knobs.connect_bound,
|
|
399
|
+
request_timeout: knobs.request_timeout,
|
|
400
|
+
pool_idle_expiry: knobs.pool_idle_expiry,
|
|
401
|
+
hint_buffer: knobs.hint_buffer,
|
|
402
|
+
keepalive_idle: knobs.keepalive_idle,
|
|
403
|
+
idle_refresh_interval: knobs.keepalive_idle,
|
|
404
|
+
max_connection_age: knobs.max_connection_age,
|
|
405
|
+
close_bound: knobs.close_bound,
|
|
406
|
+
retrans_giveup_time: knobs.retrans_giveup_time
|
|
407
|
+
},
|
|
408
|
+
operations
|
|
409
|
+
};
|
|
410
|
+
const checks = (checkOptions) => stepChecks(name, checkOptions, {
|
|
411
|
+
"local-state": localStateCheck(() => status === "ready"),
|
|
412
|
+
"round-trip": roundTripCheck(async (sendOptions) => {
|
|
413
|
+
const sent = await request(check, sendOptions);
|
|
414
|
+
return sent.outcome === "ok" && sent.value.status >= 500 ? {
|
|
415
|
+
outcome: "error",
|
|
416
|
+
error: `the server answered ${sent.value.status}`,
|
|
417
|
+
reply: true,
|
|
418
|
+
attempts: sent.attempts
|
|
419
|
+
} : sent;
|
|
420
|
+
}),
|
|
421
|
+
dial: dialCheck(dial, spec.host, spec.port)
|
|
422
|
+
});
|
|
423
|
+
return {
|
|
424
|
+
name,
|
|
425
|
+
declaration,
|
|
426
|
+
handle,
|
|
427
|
+
meets: httpClientMeetings(name, knobs),
|
|
428
|
+
get status() {
|
|
429
|
+
return status;
|
|
430
|
+
},
|
|
431
|
+
joins,
|
|
432
|
+
request,
|
|
433
|
+
checks,
|
|
434
|
+
attach: (svc, checkOptions) => attachClient(svc, name, declaration, handle, checks(checkOptions)),
|
|
435
|
+
problems(description) {
|
|
436
|
+
const idle = idleCloseProblem(name, knobs.pool_idle_expiry, knobs.server_idle_close);
|
|
437
|
+
return [...clientProblems(name, description, clientBounds(knobs)), ...idle === null ? [] : [idle]];
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region src/http-client/machine.ts
|
|
443
|
+
/** The states where the agent exists and serves. */
|
|
444
|
+
const SERVING = [
|
|
445
|
+
"connecting",
|
|
446
|
+
"ready",
|
|
447
|
+
"failing"
|
|
448
|
+
];
|
|
449
|
+
const OPEN = [...SERVING, "closing"];
|
|
450
|
+
/**
|
|
451
|
+
* The HTTP client of the service as a `mark1russell7.machine/1` document. It is the agent as the
|
|
452
|
+
* wrapper sees it, not one socket: the document `node-http-agent` of `@mark1russell7/machines`
|
|
453
|
+
* gives each socket of the agent. The state is the outcome of the last connect of a socket:
|
|
454
|
+
* - `ready`: the last connect succeeded.
|
|
455
|
+
* - `failing`: the last connect failed, or did not end in connect_bound. The agent does not connect
|
|
456
|
+
* again by itself. The next request tries again.
|
|
457
|
+
*
|
|
458
|
+
* A free socket that the agent closes does not change the state. A request on a reused socket that
|
|
459
|
+
* fails records `reuse.failed`, and the state stays. A test records the transitions of the client
|
|
460
|
+
* and replays them against this document.
|
|
461
|
+
*/
|
|
462
|
+
const HTTP_CLIENT_MACHINE = {
|
|
463
|
+
format: "mark1russell7.machine/1",
|
|
464
|
+
id: "service.machine.http-client",
|
|
465
|
+
title: "The HTTP client of the service, over an http.Agent of Node",
|
|
466
|
+
layer: "driver",
|
|
467
|
+
version: "0.1.0",
|
|
468
|
+
sources: [{
|
|
469
|
+
ref: "packages/service/src/http-client (the client implements this document)",
|
|
470
|
+
rung: "implemented"
|
|
471
|
+
}],
|
|
472
|
+
states: {
|
|
473
|
+
none: {
|
|
474
|
+
initial: true,
|
|
475
|
+
note: "the client exists, and its agent does not"
|
|
476
|
+
},
|
|
477
|
+
connecting: { note: "the check request of the startup opens the first socket" },
|
|
478
|
+
ready: { note: "the last connect succeeded. A dead free socket stays hidden until the free socket timer or a request meets it" },
|
|
479
|
+
failing: { note: "the last connect failed. A request that waits for a socket of a full pool does not change the state" },
|
|
480
|
+
closing: { note: "the client waits for its calls in flight in close_bound, then destroys each socket of the agent" },
|
|
481
|
+
end: {
|
|
482
|
+
final: true,
|
|
483
|
+
note: "the service closed the client"
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
events: {
|
|
487
|
+
"status.connecting": { kind: "local" },
|
|
488
|
+
join: {
|
|
489
|
+
kind: "rx",
|
|
490
|
+
note: "a new socket of the agent connected: the request on it leaves the client (D1)"
|
|
491
|
+
},
|
|
492
|
+
"connect.failed": {
|
|
493
|
+
kind: "local",
|
|
494
|
+
note: "a connect failed: its error, or connect_bound"
|
|
495
|
+
},
|
|
496
|
+
"reuse.failed": {
|
|
497
|
+
kind: "rx",
|
|
498
|
+
note: "a request on a free socket that the agent reused failed: the server closed that socket"
|
|
499
|
+
},
|
|
500
|
+
"command.reissue": { kind: "local" },
|
|
501
|
+
"close.request": { kind: "local" },
|
|
502
|
+
"close.bound": {
|
|
503
|
+
kind: "timer",
|
|
504
|
+
note: "a call in flight did not end in close_bound"
|
|
505
|
+
},
|
|
506
|
+
"close.destroy": {
|
|
507
|
+
kind: "local",
|
|
508
|
+
note: "the bound destroys each socket of the agent, and each call in flight is indeterminate"
|
|
509
|
+
},
|
|
510
|
+
"status.end": { kind: "local" }
|
|
511
|
+
},
|
|
512
|
+
knobs: {
|
|
513
|
+
connect_bound: clientKnob("connect.bound"),
|
|
514
|
+
request_timeout: clientKnob("request.bound"),
|
|
515
|
+
pool_idle_expiry: clientKnob("timeout"),
|
|
516
|
+
hint_buffer: clientKnob("agentKeepAliveTimeoutBuffer"),
|
|
517
|
+
keepalive_idle: clientKnob("keepAliveMsecs"),
|
|
518
|
+
close_bound: clientKnob("close.bound")
|
|
519
|
+
},
|
|
520
|
+
timers: {
|
|
521
|
+
connect_timeout: {
|
|
522
|
+
duration: {
|
|
523
|
+
knob: "connect_bound",
|
|
524
|
+
unit: "s"
|
|
525
|
+
},
|
|
526
|
+
arms: ["enter:connecting"],
|
|
527
|
+
cancels: ["join", "connect.failed"],
|
|
528
|
+
fires: "connect.failed",
|
|
529
|
+
note: "the timer of the wrapper on the connect of a new socket. The agent has no connect timer"
|
|
530
|
+
},
|
|
531
|
+
close: closeTimer()
|
|
532
|
+
},
|
|
533
|
+
transitions: [
|
|
534
|
+
transition("connect", "none", "status.connecting", "connecting"),
|
|
535
|
+
transition("join", [...SERVING], "join", "ready"),
|
|
536
|
+
transition("connect-failed", [...SERVING], "connect.failed", "failing", { timer: "connect_timeout" }),
|
|
537
|
+
transition("join-closing", "closing", "join", "closing"),
|
|
538
|
+
transition("connect-failed-closing", "closing", "connect.failed", "closing"),
|
|
539
|
+
...OPEN.map((state) => transition(`reuse-failed-${state}`, state, "reuse.failed", state)),
|
|
540
|
+
...OPEN.map((state) => transition(`reissue-${state}`, state, "command.reissue", state)),
|
|
541
|
+
transition("close", [...SERVING], "close.request", "closing"),
|
|
542
|
+
transition("close-bound", "closing", "close.bound", "closing", { timer: "close" }),
|
|
543
|
+
transition("close-destroy", "closing", "close.destroy", "closing"),
|
|
544
|
+
transition("end", "closing", "status.end", "end")
|
|
545
|
+
]
|
|
546
|
+
};
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/http-client/starting.ts
|
|
549
|
+
/**
|
|
550
|
+
* This poll is the poll of the checks that `HTTP_CLIENT_STARTING_KNOBS` fits. It is `STARTING_POLL` of
|
|
551
|
+
* the core: each 5 s with a jitter of 20 %, and a timeout of 4 s (B6, O12). The poll does not depend on the driver.
|
|
552
|
+
*/
|
|
553
|
+
const HTTP_CLIENT_STARTING_POLL = STARTING_POLL;
|
|
554
|
+
/**
|
|
555
|
+
* These knobs are a declaration to start from. They come from what the HTTP cells measured on the
|
|
556
|
+
* agent of Node 24.21.0 against a Node server at its defaults. The runtime has no defaults: a service gives
|
|
557
|
+
* these knobs, or its own. Each value holds the borders that the cells measured:
|
|
558
|
+
* - `connect_bound` 5 s: the connect timeout of DP44. The agent has no connect timer.
|
|
559
|
+
* - `request_timeout` 9.5 s: in the slowest poll of 10 s (O13). The agent has no timer of a call.
|
|
560
|
+
* A frozen server held the call of undici until its headersTimeout, at 10.187 s.
|
|
561
|
+
* - `pool_idle_expiry` 4 s: the free socket timer. The agent then closes a free socket at 4 s, the
|
|
562
|
+
* hint of 5 s less `hint_buffer`. That is below the idle close of the server.
|
|
563
|
+
* - `hint_buffer` 1 s: the default of the agent.
|
|
564
|
+
* - `server_idle_close` 6 s: a Node server closes an idle connection at its `keepAliveTimeout` of 5 s
|
|
565
|
+
* plus its buffer of 1 s. The cells measured 6.0003 s. O01 then has a margin of 2 s.
|
|
566
|
+
* - `keepalive_idle` 1 s: the default of the agent. Node truncates it to whole seconds.
|
|
567
|
+
* - `max_connection_age` "infinite": the agent does not replace a connection at an age.
|
|
568
|
+
* - `close_bound` 2 s: a call in flight behind a black hole holds its socket with no bound.
|
|
569
|
+
* - `retrans_giveup_time` 924.6 s: the give-up of Linux at `tcp_retries2 = 15`.
|
|
570
|
+
*/
|
|
571
|
+
const HTTP_CLIENT_STARTING_KNOBS = {
|
|
572
|
+
connect_bound: {
|
|
573
|
+
value: 5,
|
|
574
|
+
unit: "s"
|
|
575
|
+
},
|
|
576
|
+
request_timeout: {
|
|
577
|
+
value: 9.5,
|
|
578
|
+
unit: "s"
|
|
579
|
+
},
|
|
580
|
+
pool_idle_expiry: {
|
|
581
|
+
value: 4,
|
|
582
|
+
unit: "s"
|
|
583
|
+
},
|
|
584
|
+
hint_buffer: {
|
|
585
|
+
value: 1,
|
|
586
|
+
unit: "s"
|
|
587
|
+
},
|
|
588
|
+
server_idle_close: {
|
|
589
|
+
value: 6,
|
|
590
|
+
unit: "s"
|
|
591
|
+
},
|
|
592
|
+
keepalive_idle: {
|
|
593
|
+
value: 1,
|
|
594
|
+
unit: "s"
|
|
595
|
+
},
|
|
596
|
+
max_connection_age: {
|
|
597
|
+
value: "infinite",
|
|
598
|
+
unit: "s"
|
|
599
|
+
},
|
|
600
|
+
close_bound: {
|
|
601
|
+
value: 2,
|
|
602
|
+
unit: "s"
|
|
603
|
+
},
|
|
604
|
+
retrans_giveup_time: {
|
|
605
|
+
value: 924.6,
|
|
606
|
+
unit: "s"
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
//#endregion
|
|
610
|
+
export { HTTP_CLIENT_MACHINE, HTTP_CLIENT_STARTING_KNOBS, HTTP_CLIENT_STARTING_POLL, HTTP_OPERATIONS, MANIFESTATIONS, OPTIONS_STAR, STARTING_POLL, agentOptions, firstDetector, httpClient, httpClientMeetings, httpDial, nodeAgent, optionsRequest, specProblems, statusJudge, unwatched };
|
|
611
|
+
|
|
612
|
+
//# sourceMappingURL=http-client.js.map
|