@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.
@@ -0,0 +1,748 @@
1
+ import { realClock } from "./clock.js";
2
+ import { A as retrySchedule, B as zeroProblems, C as notSent, D as readyWait, F as timeProblems, G as immediate, H as NEVER, I as trackWithTimer, J as keepaliveSilent, K as keepaliveAnswered, L as wholeProblems, M as sendWithReissue, N as socketEnds, O as retryBackoff, 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, h as finiteMs, j as roundTripCheck, k as retryProblems, l as attachClient, m as dialCheck, n as closeTimer, q as keepaliveReset, r as transition, s as STARTING_POLL, t as clientKnob, u as clientBounds, w as operationTable, y as localStateCheck, z as wrapperTimerProblems } from "./machine-BhTRu3u9.js";
3
+ import { quantity } from "@mark1russell7/quantity";
4
+ //#region src/rabbitmq/connection.ts
5
+ /**
6
+ * This factory opens a connection of amqplib. It loads amqplib only when a client connects, thus a
7
+ * service that does not use RabbitMQ does not need it. The specifier is a value, thus the package
8
+ * needs no types of amqplib. The built-in recovery of amqplib stays off, because the wrapper connects again by itself.
9
+ */
10
+ const nodeAmqp = async ({ url, socketOptions }) => {
11
+ const loaded = await import("amqplib");
12
+ const connect = loaded.connect ?? loaded.default?.connect;
13
+ if (connect === void 0) throw new Error("amqplib gives no connect function");
14
+ return connect(url, socketOptions);
15
+ };
16
+ //#endregion
17
+ //#region src/rabbitmq/meets.ts
18
+ /**
19
+ * This function gives the manifestations that a RabbitMQ client can meet, and the detectors of each,
20
+ * from its knobs. The cells of R3 measured them on amqplib 2.2.0 against RabbitMQ 4.3.6:
21
+ * - The heartbeat ends a silent connection 2 to 3 heartbeats after the last data. The cells measured
22
+ * 180.009 s at 60 s, and 45.009 s at 15 s (O17). It finds a silent broker in flight and idle.
23
+ * - A stopped broker resets each client at once, because each socket of RabbitMQ has linger 0.
24
+ * - At `tcp_retries2 = 7`, the kernel gave up at 53.016 s.
25
+ * - A broker with a resource alarm blocks each connection that publishes, and it still sends its heartbeats.
26
+ */
27
+ const rabbitMeetings = (name, knobs) => {
28
+ const beat = finiteMs(knobs.heartbeat_interval);
29
+ const detection = beat === void 0 ? NEVER : quantity(3 * beat / 1e3, "s");
30
+ const heartbeat = (situation) => ({
31
+ by: "heartbeat_detection_time",
32
+ within: detection,
33
+ situation,
34
+ effect: `the heartbeat of amqplib ends the connection, each call in flight fails as indeterminate, and ${name} connects again`
35
+ });
36
+ const call = {
37
+ by: "request_timeout",
38
+ within: knobs.request_timeout,
39
+ situation: "in flight",
40
+ effect: "the timer of the wrapper ends the call as indeterminate, and the connection stays"
41
+ };
42
+ const giveUp = kernelGiveUp(knobs.retrans_giveup_time, "each call in flight fails as indeterminate");
43
+ const silent = (manifestation) => ({
44
+ manifestation,
45
+ what: WHAT[manifestation],
46
+ detectors: [
47
+ call,
48
+ heartbeat("in flight"),
49
+ giveUp,
50
+ heartbeat("idle"),
51
+ keepaliveSilent(knobs.keepalive_idle)
52
+ ]
53
+ });
54
+ return [
55
+ {
56
+ manifestation: "rst",
57
+ what: WHAT.rst,
58
+ detectors: [
59
+ immediate("in flight", "the call fails with ECONNRESET as indeterminate, and the client connects again"),
60
+ {
61
+ by: "heartbeat_interval",
62
+ within: beat === void 0 ? NEVER : knobs.heartbeat_interval,
63
+ situation: "idle",
64
+ effect: "the next heartbeat frame of the client gets a reset, and the client connects again"
65
+ },
66
+ keepaliveReset(knobs.keepalive_idle)
67
+ ]
68
+ },
69
+ {
70
+ manifestation: "fin",
71
+ what: WHAT.fin,
72
+ detectors: [immediate("in flight", "the broker closes its socket with linger 0, thus a reset arrives, and the call fails as indeterminate"), immediate("idle", "the reset of the broker arrives, and the client connects again")]
73
+ },
74
+ silent("blackhole"),
75
+ silent("freeze-host"),
76
+ {
77
+ manifestation: "freeze-process",
78
+ what: WHAT["freeze-process"],
79
+ detectors: [
80
+ call,
81
+ heartbeat("in flight"),
82
+ heartbeat("idle"),
83
+ keepaliveAnswered()
84
+ ]
85
+ },
86
+ {
87
+ manifestation: "app-stall",
88
+ what: "the broker stops its confirms and its replies for a time, and still sends its heartbeats: a resource alarm, or a quorum queue with no leader",
89
+ detectors: [
90
+ call,
91
+ {
92
+ by: "connection.blocked",
93
+ within: quantity(0, "s"),
94
+ situation: "idle",
95
+ effect: "a resource alarm blocks a connection that publishes at once. The client then refuses each call as not sent, until the unblock"
96
+ },
97
+ {
98
+ ...heartbeat("idle"),
99
+ within: NEVER,
100
+ effect: "the broker still sends its heartbeats"
101
+ },
102
+ keepaliveAnswered()
103
+ ]
104
+ }
105
+ ];
106
+ };
107
+ //#endregion
108
+ //#region src/rabbitmq/client.ts
109
+ /** This function gives the problems of a spec: the knobs that the client cannot use. */
110
+ const specProblems = (name, spec) => {
111
+ const { knobs } = spec;
112
+ return [
113
+ ...timeProblems(name, knobs),
114
+ ...zeroProblems(name, "amqplib", knobs, {
115
+ bounded: ["connect_bound"],
116
+ open: ["heartbeat_interval", "keepalive_idle"]
117
+ }),
118
+ ...wholeProblems(name, knobs, {
119
+ millis: ["connect_bound"],
120
+ seconds: ["heartbeat_interval", "keepalive_idle"]
121
+ }),
122
+ ...retryProblems(name, knobs),
123
+ ...wrapperTimerProblems(name, knobs, { request_timeout: "a call" }),
124
+ ...ageProblems(name, knobs, "the client does not replace its connection at an age")
125
+ ];
126
+ };
127
+ /** This function gives the node of a try: `host`, then each seed, in sequence. */
128
+ const nodeOf = (spec, index) => {
129
+ const nodes = [{
130
+ host: spec.host,
131
+ port: spec.port
132
+ }, ...spec.seeds ?? []];
133
+ return nodes[index % nodes.length];
134
+ };
135
+ /**
136
+ * This function gives the arguments of `connect` for one node of a spec. Each timer of amqplib
137
+ * comes from the declaration. The heartbeat goes in the URL, and "infinite" is 0, no heartbeat.
138
+ */
139
+ const rabbitOptions = (name, spec, index = 0) => {
140
+ const { knobs } = spec;
141
+ const node = nodeOf(spec, index);
142
+ const user = spec.username === void 0 ? "" : `${encodeURIComponent(spec.username)}:${encodeURIComponent(spec.password ?? "")}@`;
143
+ const heartbeat = finiteMs(knobs.heartbeat_interval);
144
+ const keepalive = finiteMs(knobs.keepalive_idle);
145
+ return {
146
+ url: `amqp://${user}${node.host}:${node.port}/${encodeURIComponent(spec.vhost ?? "/")}?heartbeat=${heartbeat === void 0 ? 0 : heartbeat / 1e3}`,
147
+ socketOptions: {
148
+ timeout: ms(knobs.connect_bound),
149
+ noDelay: true,
150
+ keepAlive: keepalive !== void 0,
151
+ ...keepalive === void 0 ? {} : { keepAliveDelay: keepalive },
152
+ clientProperties: { connection_name: name }
153
+ }
154
+ };
155
+ };
156
+ /** The bytes of the protocol header of AMQP 0-9-1. A broker answers it with `connection.start`, before the authentication. */
157
+ const AMQP_HEADER = Uint8Array.of(65, 77, 81, 80, 0, 0, 9, 1);
158
+ /**
159
+ * This judge reads the answer to the protocol header. A broker that takes the version sends a method
160
+ * frame on channel 0: `connection.start`, class 10 and method 10. A broker that refuses the version
161
+ * sends its own protocol header, and closes the socket.
162
+ */
163
+ const startJudge = (received) => {
164
+ if (received.length < 4) return "more";
165
+ if (received.toString("latin1", 0, 4) === "AMQP") return /* @__PURE__ */ new Error(`the broker refused the version: it answered ${received.toString("hex")}`);
166
+ if (received.length < 11) return "more";
167
+ const type = received[0];
168
+ const channel = received.readUInt16BE(1);
169
+ const classId = received.readUInt16BE(7);
170
+ const methodId = received.readUInt16BE(9);
171
+ if (type !== 1 || channel !== 0) return /* @__PURE__ */ new Error(`the answer to the header is a frame of type ${type} on channel ${channel}, not a method on channel 0`);
172
+ return classId === 10 && methodId === 10 ? null : /* @__PURE__ */ new Error(`the answer to the header is the method ${classId}.${methodId}, not connection.start (10.10)`);
173
+ };
174
+ /** This dial function opens a TCP connection, sends the protocol header, and waits for `connection.start`. It needs no password. */
175
+ const amqpDial = helloDial(AMQP_HEADER, startJudge);
176
+ /**
177
+ * This function gives the outcome of a failure of amqplib. The library names few classes of error.
178
+ * Thus the wrapper reads the fields:
179
+ * - A throw before the write (`IllegalOperationError`: the channel is closed or closing) is certain: the call did not leave the client.
180
+ * - A reply code of the broker, from the close of the channel, is certain. The broker refused the call, or a call before it.
181
+ * - A nack, and an end of the channel before the reply or the confirm, are not certain.
182
+ */
183
+ const failureOutcome = (error) => {
184
+ const text = message(error);
185
+ const { name, code } = typeof error === "object" && error !== null ? error : {};
186
+ if (name === "IllegalOperationError") return notSent(text);
187
+ if (typeof code === "number") return {
188
+ outcome: "error",
189
+ error: `${code} ${text}`,
190
+ reply: true
191
+ };
192
+ if (text === "message nacked") return {
193
+ outcome: "indeterminate",
194
+ error: "the broker nacked the message: a queue of the route can hold it, or not"
195
+ };
196
+ return {
197
+ outcome: "indeterminate",
198
+ error: text
199
+ };
200
+ };
201
+ /** This function gives the count of deliveries of a message, 1 at the first. A quorum queue counts in `x-delivery-count`. */
202
+ const deliveryCount = (message) => {
203
+ const count = message.properties.headers?.["x-delivery-count"];
204
+ if (typeof count === "number" && Number.isInteger(count) && count >= 0) return count + 1;
205
+ return message.fields.redelivered ? 2 : 1;
206
+ };
207
+ /** The exchange of each broker: the round-trip check declares it in passive mode, which changes nothing. */
208
+ const CHECK_EXCHANGE = "amq.direct";
209
+ /**
210
+ * This function makes a RabbitMQ client, a dependency of a service over amqplib 2.
211
+ * - It has one connection and one confirm channel, as one generation. The end of either one ends
212
+ * the generation. The client then connects again by itself, to the next node, after a backoff.
213
+ * - It sets each timer of amqplib from the declaration. The built-in recovery of amqplib stays off.
214
+ * - Its call is a confirmed publish, or an RPC with a reply. Each call has a timer of the wrapper, because amqplib has none.
215
+ * - It tells the outcome of a call in three values, and it settles each call in flight at the end of its generation (C8, C10).
216
+ * - It records the node and the name of each connection at the open of its channel (D1).
217
+ *
218
+ * These parts come from the core of the clients: the outcomes, the second try, the wait for a ready client and the backoff.
219
+ * The calls in flight, the bounded close and the checks also come from the core. This entry gives what amqplib does in its own way.
220
+ */
221
+ const rabbitClient = (name, spec, options = {}) => {
222
+ const problems = specProblems(name, spec);
223
+ if (problems.length > 0) throw new Error(problems.join("; "));
224
+ const { knobs } = spec;
225
+ const clock = options.clock ?? realClock;
226
+ const random = options.random ?? Math.random;
227
+ const create = options.create ?? nodeAmqp;
228
+ const dial = options.dial ?? amqpDial;
229
+ const operations = operationTable({}, spec.operations);
230
+ const requestTimeout = ms(knobs.request_timeout);
231
+ const backoff = retryBackoff(knobs);
232
+ let status = "none";
233
+ let connection = null;
234
+ let channel = null;
235
+ let generation = 0;
236
+ let token = 0;
237
+ let next = 0;
238
+ const retries = retrySchedule(clock, backoff, random);
239
+ const joins = [];
240
+ const flight = inFlight();
241
+ const readied = readyWait(clock, () => status === "ready");
242
+ const emit = clientRecorder(options.record, clock, name);
243
+ const record = (on, to = status, detail) => {
244
+ const from = status;
245
+ status = to;
246
+ emit(from, to, on, detail);
247
+ };
248
+ const closing = () => status === "closing" || status === "end";
249
+ /** This function closes a connection that the client does not use, in `close_bound`, then destroys its socket. */
250
+ const retire = (c) => {
251
+ withinBound(clock, ms(knobs.close_bound), Promise.resolve().then(() => c.close())).then((ended) => {
252
+ if (!ended) c.connection.stream?.destroy?.();
253
+ });
254
+ };
255
+ /** This function waits for the backoff of the next try, then opens a new generation on the next node. */
256
+ const scheduleRetry = () => retries.schedule(() => {
257
+ if (closing()) return;
258
+ record("retry.connect", "connecting", `try ${retries.tries}, ${nodeOf(spec, next).host}:${nodeOf(spec, next).port}`);
259
+ open();
260
+ });
261
+ /** This function ends the generation that is open. The calls in flight learn it after the rejections of amqplib, which can hold a reply code. */
262
+ const lose = (on, detail) => {
263
+ const c = connection;
264
+ connection = null;
265
+ channel = null;
266
+ if (closing() || c === null) return;
267
+ record(on, "failing", detail);
268
+ clock.after(0, () => flight.settleAll(`the generation ended with the call in flight (${detail})`));
269
+ if (on === "channel.end") retire(c);
270
+ scheduleRetry();
271
+ };
272
+ /** This function listens to a connection from its creation. The error listener stays: in Node, an error with no listener throws. */
273
+ const watch = (c) => {
274
+ c.on("error", (error) => {
275
+ if (c === connection) record("connection.error", status, message(error));
276
+ });
277
+ c.on("close", (error) => {
278
+ if (c === connection) lose("connection.end", error === void 0 ? "the connection closed" : message(error));
279
+ });
280
+ c.on("blocked", (reason) => {
281
+ if (c === connection && status === "ready") record("connection.blocked", "blocked", String(reason));
282
+ });
283
+ c.on("unblocked", () => {
284
+ if (c !== connection || status !== "blocked") return;
285
+ record("connection.unblocked", "ready");
286
+ readied.wake();
287
+ });
288
+ };
289
+ const join = (c, ch, address) => {
290
+ connection = c;
291
+ channel = ch;
292
+ retries.reset();
293
+ const ends = socketEnds(c.connection.stream);
294
+ generation = joins.length + 1;
295
+ const connectionName = ends.localAddress === null ? null : `${ends.localAddress}:${ends.localPort ?? "?"} -> ${ends.remoteAddress ?? "?"}:${ends.remotePort ?? "?"}`;
296
+ joins.push({
297
+ generation,
298
+ at: clock.now(),
299
+ address,
300
+ connectionName,
301
+ ...ends
302
+ });
303
+ ch.on("error", (error) => {
304
+ if (ch === channel) record("channel.error", status, message(error));
305
+ });
306
+ ch.on("close", () => {
307
+ queueMicrotask(() => {
308
+ if (ch === channel) lose("channel.end", "the channel closed");
309
+ });
310
+ });
311
+ record("join", "ready", `generation ${generation}, ${address}`);
312
+ readied.wake();
313
+ };
314
+ /**
315
+ * This function opens one generation: a connection, then its confirm channel, in `connect_bound`.
316
+ * A failure goes to `failing`, and the backoff starts. A connection that arrives after its bound
317
+ * or after the close is closed at once. The promise resolves when this try ends.
318
+ */
319
+ const open = () => {
320
+ const mine = token += 1;
321
+ const index = next;
322
+ next += 1;
323
+ const node = nodeOf(spec, index);
324
+ const address = `${node.host}:${node.port}`;
325
+ let opened = null;
326
+ let finish = () => {};
327
+ const ended = new Promise((resolve) => finish = resolve);
328
+ const stale = () => mine !== token || closing();
329
+ const fail = (detail) => {
330
+ bound();
331
+ finish();
332
+ if (stale()) return;
333
+ token += 1;
334
+ if (opened !== null) retire(opened);
335
+ record("connect.failed", "failing", `${address}: ${detail}`);
336
+ scheduleRetry();
337
+ };
338
+ const bound = clock.after(ms(knobs.connect_bound), () => fail("no open connection and channel in connect_bound"));
339
+ (async () => {
340
+ const c = await create(rabbitOptions(name, spec, index));
341
+ opened = c;
342
+ watch(c);
343
+ if (stale()) {
344
+ retire(c);
345
+ return;
346
+ }
347
+ const ch = await c.createConfirmChannel();
348
+ if (stale()) {
349
+ retire(c);
350
+ return;
351
+ }
352
+ bound();
353
+ join(c, ch, address);
354
+ finish();
355
+ })().catch((error) => fail(message(error)));
356
+ return ended;
357
+ };
358
+ /** This function gives the channel for a call, or the reason that the call does not leave the client. */
359
+ const usable = (signal) => {
360
+ if (status === "none") return notSent(`${name} has not connected`);
361
+ if (status !== "ready" || channel === null) return notSent(`${name} is ${status}, not ready: the call did not leave the client`);
362
+ if (signal?.aborted) return notSent("the caller aborted the call before it left the client");
363
+ return channel;
364
+ };
365
+ /** This function makes one try of a publish: the write, then the confirm, in `request_timeout`. */
366
+ const publishAttempt = (sent, signal) => {
367
+ const ch = usable(signal);
368
+ if ("outcome" in ch) return Promise.resolve(ch);
369
+ const content = typeof sent.content === "string" ? Buffer.from(sent.content, "utf8") : sent.content;
370
+ return trackWithTimer(flight, clock, requestTimeout, signal, "the caller abandoned the publish after it was written", "no confirm in request_timeout: the broker can have the message", (settle) => {
371
+ try {
372
+ ch.publish(sent.exchange, sent.routingKey, content, sent.properties ?? {}, (error) => settle(error === null ? {
373
+ outcome: "ok",
374
+ value: void 0
375
+ } : failureOutcome(error)));
376
+ } catch (error) {
377
+ settle(failureOutcome(error));
378
+ }
379
+ });
380
+ };
381
+ /**
382
+ * This function makes one try of an RPC on the channel, in `request_timeout`. A reply that comes
383
+ * after the call settled goes to `late`, thus a message that nobody receives goes back to its queue.
384
+ */
385
+ const rpcAttempt = (signal, what, run, value, late) => {
386
+ const ch = usable(signal);
387
+ if ("outcome" in ch) return Promise.resolve(ch);
388
+ let finished = false;
389
+ return trackWithTimer(flight, clock, requestTimeout, signal, `the caller abandoned ${what} after it was sent`, `no reply to ${what} in request_timeout`, (settle) => {
390
+ Promise.resolve().then(() => run(ch)).then((reply) => {
391
+ if (finished) late?.(reply, ch);
392
+ else settle({
393
+ outcome: "ok",
394
+ value: value(reply)
395
+ });
396
+ }, (error) => settle(failureOutcome(error)));
397
+ }, () => {
398
+ finished = true;
399
+ });
400
+ };
401
+ const publish = (operation, sent, sendOptions = {}) => sendWithReissue({
402
+ clock,
403
+ bound: requestTimeout,
404
+ operation: operations[operation],
405
+ options: sendOptions,
406
+ attempt: (signal) => publishAttempt(sent, signal),
407
+ beforeAgain: (left, signal) => readied.wait(left, signal),
408
+ onReissue: () => record("command.reissue", status, operation)
409
+ });
410
+ const get = (operation, queue, sendOptions = {}) => {
411
+ const at = () => generation;
412
+ return sendWithReissue({
413
+ clock,
414
+ bound: requestTimeout,
415
+ operation: operations[operation],
416
+ options: sendOptions,
417
+ attempt: (signal) => {
418
+ const held = at();
419
+ return rpcAttempt(signal, "basic.get", (ch) => ch.get(queue, { noAck: false }), (reply) => reply === false ? null : {
420
+ message: reply,
421
+ generation: held,
422
+ delivery: deliveryCount(reply)
423
+ }, (reply, ch) => {
424
+ if (reply === false) return;
425
+ try {
426
+ ch.nack(reply, false, true);
427
+ } catch {}
428
+ });
429
+ },
430
+ beforeAgain: (left, signal) => readied.wait(left, signal),
431
+ onReissue: () => record("command.reissue", status, operation)
432
+ });
433
+ };
434
+ /** This function gives the channel that holds a delivery. A delivery of an ended generation is back on its queue already (at least once). */
435
+ const holder = (job) => {
436
+ const ch = channel;
437
+ if (ch === null || job.generation !== generation) throw new Error(`${name}: the channel of generation ${job.generation} ended, thus the broker put the message back on its queue`);
438
+ return ch;
439
+ };
440
+ const jobs = (operation, queue) => ({
441
+ source: async (signal) => {
442
+ const taken = await get(operation, queue, { signal });
443
+ if (taken.outcome === "ok") return taken.value;
444
+ throw new Error(`${name} could not take a message from ${queue}: ${taken.error}`);
445
+ },
446
+ settle: {
447
+ ack: (job) => holder(job).ack(job.message),
448
+ requeue: (job) => holder(job).nack(job.message, false, true),
449
+ reject: (job) => holder(job).nack(job.message, false, false),
450
+ delivery: (job) => job.delivery
451
+ }
452
+ });
453
+ const handle = {
454
+ async connect(signal) {
455
+ if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);
456
+ record("status.connecting", "connecting");
457
+ await open();
458
+ },
459
+ async close() {
460
+ if (status === "none" || closing()) return;
461
+ record("close.request", "closing");
462
+ retries.cancel();
463
+ token += 1;
464
+ const c = connection;
465
+ if (c !== null) {
466
+ if (!await withinBound(clock, ms(knobs.close_bound), Promise.resolve().then(() => c.close()))) {
467
+ record("close.bound", "closing");
468
+ record("close.destroy", "closing");
469
+ c.connection.stream?.destroy?.();
470
+ }
471
+ }
472
+ connection = null;
473
+ channel = null;
474
+ flight.settleAll("the client closed with the call in flight");
475
+ record("status.end", "end");
476
+ }
477
+ };
478
+ const heartbeat = finiteMs(knobs.heartbeat_interval);
479
+ const declaration = {
480
+ system: spec.system,
481
+ knobs: {
482
+ connect_bound: knobs.connect_bound,
483
+ request_timeout: knobs.request_timeout,
484
+ heartbeat_interval: knobs.heartbeat_interval,
485
+ heartbeat_detection_time: heartbeat === void 0 ? knobs.heartbeat_interval : quantity(3 * heartbeat / 1e3, "s"),
486
+ retry_base: knobs.retry_base,
487
+ retry_cap: knobs.retry_cap,
488
+ keepalive_idle: knobs.keepalive_idle,
489
+ idle_refresh_interval: heartbeat === void 0 ? knobs.keepalive_idle : quantity(heartbeat / 2e3, "s"),
490
+ max_connection_age: knobs.max_connection_age,
491
+ close_bound: knobs.close_bound,
492
+ retrans_giveup_time: knobs.retrans_giveup_time
493
+ },
494
+ operations
495
+ };
496
+ const checks = (checkOptions) => stepChecks(name, checkOptions, {
497
+ "local-state": localStateCheck(() => status === "ready"),
498
+ "round-trip": roundTripCheck((sendOptions) => sendWithReissue({
499
+ clock,
500
+ bound: requestTimeout,
501
+ operation: void 0,
502
+ options: sendOptions,
503
+ attempt: (signal) => rpcAttempt(signal, "a passive exchange.declare", (ch) => ch.checkExchange(CHECK_EXCHANGE), () => void 0),
504
+ onReissue: () => {}
505
+ })),
506
+ dial: dialCheck(dial, spec.host, spec.port)
507
+ });
508
+ return {
509
+ name,
510
+ declaration,
511
+ handle,
512
+ meets: rabbitMeetings(name, knobs),
513
+ get status() {
514
+ return status;
515
+ },
516
+ joins,
517
+ publish,
518
+ get,
519
+ jobs,
520
+ checks,
521
+ attach: (svc, checkOptions) => attachClient(svc, name, declaration, handle, checks(checkOptions)),
522
+ problems: (description) => clientProblems(name, description, clientBounds(knobs))
523
+ };
524
+ };
525
+ //#endregion
526
+ //#region src/rabbitmq/machine.ts
527
+ /** The states where a generation is open, and the states where the client can close. */
528
+ const OPEN = ["ready", "blocked"];
529
+ const SERVING = [
530
+ "connecting",
531
+ ...OPEN,
532
+ "failing"
533
+ ];
534
+ const LIVE = [...SERVING, "closing"];
535
+ /**
536
+ * The RabbitMQ client of the service as a `mark1russell7.machine/1` document. It is one generation
537
+ * at a time: one connection of amqplib and its confirm channel. The machines `amqplib` and
538
+ * `rabbitmq-connection` of `@mark1russell7/machines` give the connection and its heartbeat. The states:
539
+ * - `ready`: the channel is open, and the client sends each call.
540
+ * - `blocked`: a resource alarm of the broker blocked the connection. Each call is not sent until the unblock.
541
+ * - `failing`: no generation is open. The client tries the next node after its backoff.
542
+ *
543
+ * The end of the channel or of the connection ends the generation. A test records the transitions
544
+ * of the client and replays them against this document.
545
+ */
546
+ const RABBIT_CLIENT_MACHINE = {
547
+ format: "mark1russell7.machine/1",
548
+ id: "service.machine.rabbitmq-client",
549
+ title: "The RabbitMQ client of the service, over amqplib 2",
550
+ layer: "driver",
551
+ version: "0.1.0",
552
+ sources: [{
553
+ ref: "packages/service/src/rabbitmq (the client implements this document)",
554
+ rung: "implemented"
555
+ }],
556
+ states: {
557
+ none: {
558
+ initial: true,
559
+ note: "the client exists, and its first connection does not"
560
+ },
561
+ connecting: { note: "a connection, its handshake, and the open of its confirm channel, in connect_bound" },
562
+ ready: { note: "the channel is open. A black hole stays hidden until the heartbeat or a timer of a call ends it" },
563
+ blocked: { note: "a resource alarm blocked the connection: the broker does not read its socket" },
564
+ failing: { note: "no generation is open. The client tries the next node after its backoff" },
565
+ closing: { note: "connection.close, in close_bound, then a destroy of the socket" },
566
+ end: {
567
+ final: true,
568
+ note: "the service closed the client"
569
+ }
570
+ },
571
+ events: {
572
+ "status.connecting": { kind: "local" },
573
+ join: {
574
+ kind: "rx",
575
+ note: "the confirm channel is open: a new generation (D1)"
576
+ },
577
+ "connect.failed": {
578
+ kind: "local",
579
+ note: "the connection or its channel failed, or did not open in connect_bound"
580
+ },
581
+ "retry.connect": {
582
+ kind: "timer",
583
+ note: "the backoff ended: a try on the next node"
584
+ },
585
+ "connection.error": {
586
+ kind: "rx",
587
+ note: "an error event of the connection. Its close follows"
588
+ },
589
+ "connection.end": {
590
+ kind: "rx",
591
+ note: "the connection closed: a reset, a FIN, the heartbeat, or a close of the broker"
592
+ },
593
+ "channel.error": {
594
+ kind: "rx",
595
+ note: "the broker closed the channel with a reply code"
596
+ },
597
+ "channel.end": {
598
+ kind: "rx",
599
+ note: "the channel closed on a connection that stays: the client closes that connection too"
600
+ },
601
+ "connection.blocked": {
602
+ kind: "rx",
603
+ note: "connection.blocked of the broker: a resource alarm"
604
+ },
605
+ "connection.unblocked": { kind: "rx" },
606
+ "command.reissue": { kind: "local" },
607
+ "close.request": { kind: "local" },
608
+ "close.bound": {
609
+ kind: "timer",
610
+ note: "connection.close got no close-ok in close_bound"
611
+ },
612
+ "close.destroy": {
613
+ kind: "local",
614
+ note: "the bound destroys a socket that a silent broker does not close"
615
+ },
616
+ "status.end": { kind: "local" }
617
+ },
618
+ knobs: {
619
+ connect_bound: clientKnob("socketOptions.timeout"),
620
+ heartbeat_interval: clientKnob("heartbeat"),
621
+ retry_base: clientKnob("retry.base"),
622
+ retry_cap: clientKnob("retry.cap"),
623
+ close_bound: clientKnob("close.bound")
624
+ },
625
+ timers: {
626
+ connect_timeout: {
627
+ duration: {
628
+ knob: "connect_bound",
629
+ unit: "s"
630
+ },
631
+ arms: ["enter:connecting"],
632
+ cancels: ["join", "connect.failed"],
633
+ fires: "connect.failed",
634
+ note: "the bound of the wrapper on the connection and its channel. amqplib bounds the connect and the handshake with the same time"
635
+ },
636
+ heartbeat: {
637
+ duration: {
638
+ expr: "3 * heartbeat_interval",
639
+ unit: "s"
640
+ },
641
+ arms: ["join"],
642
+ cancels: ["connection.end", "close.request"],
643
+ fires: "connection.end",
644
+ note: "the heartbeat of amqplib ends a silent connection 2 to 3 heartbeats after the last data. Each frame that arrives starts it again"
645
+ },
646
+ retry: {
647
+ duration: {
648
+ expr: "min(retry_cap, retry_base * pow(2, fired)) + retry_base",
649
+ unit: "s"
650
+ },
651
+ arms: [
652
+ "connect.failed",
653
+ "connection.end",
654
+ "channel.end"
655
+ ],
656
+ cancels: ["close.request"],
657
+ fires: "retry.connect",
658
+ backoff: { resets: ["join"] },
659
+ note: "the most delay: the backoff doubles from retry_base to retry_cap, with a jitter up to retry_base"
660
+ },
661
+ close: closeTimer()
662
+ },
663
+ transitions: [
664
+ transition("connect", "none", "status.connecting", "connecting"),
665
+ transition("join", "connecting", "join", "ready"),
666
+ transition("connect-failed", "connecting", "connect.failed", "failing", { timer: "connect_timeout" }),
667
+ transition("retry", "failing", "retry.connect", "connecting", { timer: "retry" }),
668
+ transition("connection-end", [...OPEN], "connection.end", "failing", { timer: "heartbeat" }),
669
+ transition("channel-end", [...OPEN], "channel.end", "failing"),
670
+ transition("blocked", "ready", "connection.blocked", "blocked"),
671
+ transition("unblocked", "blocked", "connection.unblocked", "ready"),
672
+ ...OPEN.map((state) => transition(`connection-error-${state}`, state, "connection.error", state)),
673
+ ...OPEN.map((state) => transition(`channel-error-${state}`, state, "channel.error", state)),
674
+ ...LIVE.map((state) => transition(`reissue-${state}`, state, "command.reissue", state)),
675
+ transition("close", [...SERVING], "close.request", "closing"),
676
+ transition("close-bound", "closing", "close.bound", "closing", { timer: "close" }),
677
+ transition("close-destroy", "closing", "close.destroy", "closing"),
678
+ transition("end", "closing", "status.end", "end")
679
+ ]
680
+ };
681
+ //#endregion
682
+ //#region src/rabbitmq/starting.ts
683
+ /**
684
+ * This poll is the poll of the checks that `RABBIT_STARTING_KNOBS` fits. It is `STARTING_POLL` of
685
+ * 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.
686
+ */
687
+ const RABBIT_STARTING_POLL = STARTING_POLL;
688
+ /**
689
+ * These knobs are a declaration to start from, from what the cells of R3 measured on amqplib 2.2.0
690
+ * against RabbitMQ 4.3.6. The runtime has no defaults: a service gives these knobs, or its own.
691
+ * Each value holds the borders that the cells measured:
692
+ * - `connect_bound` 5 s: the connect timeout of DP44. The timer stops when the connection opens.
693
+ * Thus it does not change a fault with a call in flight. At its defaults, amqplib has no connect timeout.
694
+ * - `request_timeout` 9.5 s: in the slowest poll of 10 s (O13). After a loss of the leader of a quorum
695
+ * queue, the first confirm came at 6.61 s, 6.68 s and 8.05 s (T11). A later confirm is indeterminate, not lost.
696
+ * - `heartbeat_interval` 15 s: the heartbeat ended a silent connection 45.009 s after the last data,
697
+ * before the kernel gave up at 53.016 s (O17). The frames each 7.5 s keep a flow through a router
698
+ * that forgets it at 30 s (O03). At the 60 s of the broker, the heartbeat came at 180.009 s.
699
+ * - `retry_base` 1 s: the cells connected to the next node 1 s after the end of a connection.
700
+ * - `retry_cap` 8 s: no cell measured it. With its jitter of 1 s, the most delay is 9 s, in the slowest poll of 10 s.
701
+ * - `keepalive_idle` "infinite": amqplib sets no keepalive, and the heartbeat refreshes an idle flow.
702
+ * Linux sends no keepalive probe while data is in flight.
703
+ * - `max_connection_age` "infinite": the client does not replace its connection at an age.
704
+ * - `close_bound` 2 s: behind a black hole, `connection.close` waits for its close-ok with no bound.
705
+ * - `retrans_giveup_time` 924.6 s: the give-up of Linux at `tcp_retries2 = 15`.
706
+ */
707
+ const RABBIT_STARTING_KNOBS = {
708
+ connect_bound: {
709
+ value: 5,
710
+ unit: "s"
711
+ },
712
+ request_timeout: {
713
+ value: 9.5,
714
+ unit: "s"
715
+ },
716
+ heartbeat_interval: {
717
+ value: 15,
718
+ unit: "s"
719
+ },
720
+ retry_base: {
721
+ value: 1,
722
+ unit: "s"
723
+ },
724
+ retry_cap: {
725
+ value: 8,
726
+ unit: "s"
727
+ },
728
+ keepalive_idle: {
729
+ value: "infinite",
730
+ unit: "s"
731
+ },
732
+ max_connection_age: {
733
+ value: "infinite",
734
+ unit: "s"
735
+ },
736
+ close_bound: {
737
+ value: 2,
738
+ unit: "s"
739
+ },
740
+ retrans_giveup_time: {
741
+ value: 924.6,
742
+ unit: "s"
743
+ }
744
+ };
745
+ //#endregion
746
+ export { AMQP_HEADER, MANIFESTATIONS, RABBIT_CLIENT_MACHINE, RABBIT_STARTING_KNOBS, RABBIT_STARTING_POLL, STARTING_POLL, amqpDial, deliveryCount, failureOutcome, firstDetector, nodeAmqp, rabbitClient, rabbitMeetings, rabbitOptions, readyWait, retryBackoff, retryProblems, specProblems, startJudge, unwatched };
747
+
748
+ //# sourceMappingURL=rabbitmq.js.map