@onlineapps/mq-client-core 5.0.0 → 6.0.0
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/CHANGELOG.md +474 -0
- package/README.md +117 -13
- package/package.json +2 -2
- package/src/BaseClient.js +101 -68
- package/src/buffer/InMemoryBuffer.js +161 -13
- package/src/buffer/MessageBuffer.js +8 -3
- package/src/config/configSchema.js +15 -19
- package/src/config/defaultConfig.js +10 -11
- package/src/config/queueConfig.js +35 -1
- package/src/index.js +22 -0
- package/src/layers/PublishLayer.js +241 -35
- package/src/monitoring-initiated.js +6 -9
- package/src/transports/rabbitmqClient.js +912 -151
- package/src/utils/errorHandler.js +25 -0
- package/src/utils/publishErrors.js +49 -0
|
@@ -10,6 +10,19 @@ const { assertLogger } = require('@onlineapps/logger-contract');
|
|
|
10
10
|
* Primárně určený pro transient scénáře (health checky apod.).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* How a log line names where a buffered message was going: its exchange when it was published
|
|
15
|
+
* to one, its queue otherwise.
|
|
16
|
+
*
|
|
17
|
+
* @param {{queue: string, options: Object}} item
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function describeTarget(item) {
|
|
21
|
+
return item.options && typeof item.options.exchange === 'string' && item.options.exchange !== ''
|
|
22
|
+
? `exchange "${item.options.exchange}"`
|
|
23
|
+
: `queue "${item.queue}"`;
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
class InMemoryBuffer {
|
|
14
27
|
/**
|
|
15
28
|
* Both numbers are REQUIRED and have no stand-in here.
|
|
@@ -30,6 +43,10 @@ class InMemoryBuffer {
|
|
|
30
43
|
* @param {number} options.maxSize - Max messages held; 0 means hold none
|
|
31
44
|
* @param {number} options.ttlMs - How long one message may wait, in ms
|
|
32
45
|
* @param {Object} options.logger - Logger s info/warn/error/debug (povinný)
|
|
46
|
+
* @param {{setTimeout: Function, clearTimeout: Function}} options.timers - The clock the
|
|
47
|
+
* expiry runs on (required, d.1275): `setTimeout(callback, ms)` returns a handle,
|
|
48
|
+
* `clearTimeout(handle)` disarms it. The transport hands its own timer rail
|
|
49
|
+
* (`_setTimeout()` / `_clearTimeout()`), so `disconnect()` ends this timer too.
|
|
33
50
|
*/
|
|
34
51
|
constructor(options = {}) {
|
|
35
52
|
// The logger first, so a caller who forgot it gets the contract message
|
|
@@ -58,10 +75,23 @@ class InMemoryBuffer {
|
|
|
58
75
|
+ 'or pass ttlMs explicitly.'
|
|
59
76
|
);
|
|
60
77
|
}
|
|
78
|
+
if (!options.timers || typeof options.timers.setTimeout !== 'function'
|
|
79
|
+
|| typeof options.timers.clearTimeout !== 'function') {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'[InMemoryBuffer] Missing required option - timers is required and has no default here. '
|
|
82
|
+
+ 'Expected: { setTimeout(callback, ms), clearTimeout(handle) } — the clock a held message '
|
|
83
|
+
+ 'expires on at its TTL, rather than whenever the buffer is next touched. '
|
|
84
|
+
+ 'Fix: build this buffer through PublishLayer, which hands it the transport\'s timer rail, '
|
|
85
|
+
+ 'or pass options.timers explicitly.'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
61
88
|
this._maxSize = options.maxSize;
|
|
62
89
|
this._ttlMs = options.ttlMs;
|
|
90
|
+
this._timers = options.timers;
|
|
91
|
+
/** The one armed expiry timer, aimed at the oldest entry's deadline; null while empty. */
|
|
92
|
+
this._expiryTimer = null;
|
|
63
93
|
|
|
64
|
-
/** @type {Array<{queue:string, buffer:Buffer, options:Object, createdAt:number, priority:string}>} */
|
|
94
|
+
/** @type {Array<{queue:string, buffer:Buffer, options:Object, createdAt:number, priority:string, sent:boolean}>} */
|
|
65
95
|
this._items = [];
|
|
66
96
|
}
|
|
67
97
|
|
|
@@ -69,7 +99,18 @@ class InMemoryBuffer {
|
|
|
69
99
|
* Přidá zprávu do bufferu.
|
|
70
100
|
* Staré/expirující zprávy jsou průběžně čištěny, při překročení kapacity odhazujeme nejstarší.
|
|
71
101
|
*/
|
|
72
|
-
async add(queue, buffer, options = {}, priority = 'normal') {
|
|
102
|
+
async add(queue, buffer, options = {}, priority = 'normal', sent) {
|
|
103
|
+
// Whether the frame of this message already LEFT the client (d.1145, conf
|
|
104
|
+
// `mq-publish-buffer` 001): the flush after `unblocked` re-sends only the entries that
|
|
105
|
+
// never did, because the broker delivers the frames it was holding by itself. Required —
|
|
106
|
+
// an entry that cannot say would be re-sent or dropped by a guess.
|
|
107
|
+
if (typeof sent !== 'boolean') {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`[InMemoryBuffer] Cannot hold a message for queue '${queue}' without knowing whether its frame was sent - `
|
|
110
|
+
+ `got sent=${JSON.stringify(sent)}. Expected: a boolean from the failed publish (error.sent). `
|
|
111
|
+
+ 'Fix: pass it from PublishLayer, which reads it off the transport\'s error.'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
73
114
|
const now = Date.now();
|
|
74
115
|
this._cleanupExpired(now);
|
|
75
116
|
|
|
@@ -86,7 +127,9 @@ class InMemoryBuffer {
|
|
|
86
127
|
options,
|
|
87
128
|
createdAt: now,
|
|
88
129
|
priority,
|
|
130
|
+
sent,
|
|
89
131
|
});
|
|
132
|
+
this._armExpiry(now);
|
|
90
133
|
}
|
|
91
134
|
|
|
92
135
|
/**
|
|
@@ -94,7 +137,20 @@ class InMemoryBuffer {
|
|
|
94
137
|
* @param {Function} flushFn - async (queue, buffer, options) => void
|
|
95
138
|
* @returns {Promise<number>} - počet úspěšně flushnutých zpráv
|
|
96
139
|
*/
|
|
97
|
-
async flush(flushFn) {
|
|
140
|
+
async flush(flushFn, { sentEntries } = {}) {
|
|
141
|
+
// What happens to an entry whose frame already LEFT the client (d.1145, conf
|
|
142
|
+
// `mq-publish-buffer` 001): 'resend' after a reconnect — the old socket and whatever the
|
|
143
|
+
// broker had not read from it are gone; 'drop' after the broker unblocked the connection —
|
|
144
|
+
// it reads what was waiting in the socket and delivers those frames by itself, so a
|
|
145
|
+
// re-send would be one more copy. Required: a flush that guessed would do one or the other
|
|
146
|
+
// wrong.
|
|
147
|
+
if (sentEntries !== 'resend' && sentEntries !== 'drop') {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`[InMemoryBuffer] flush() needs to know what to do with a sent entry - got sentEntries=${JSON.stringify(sentEntries)}. `
|
|
150
|
+
+ "Expected: 'resend' (after a reconnect) or 'drop' (after the broker unblocked the connection). "
|
|
151
|
+
+ 'Fix: flush through PublishLayer.flushBuffered(trigger), which maps its trigger to one of them.'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
98
154
|
const now = Date.now();
|
|
99
155
|
this._cleanupExpired(now);
|
|
100
156
|
|
|
@@ -103,8 +159,24 @@ class InMemoryBuffer {
|
|
|
103
159
|
}
|
|
104
160
|
|
|
105
161
|
// Zkopírujeme a vyprázdníme interní pole, abychom se vyhnuli reentrancy problémům
|
|
106
|
-
|
|
162
|
+
let items = this._items.slice();
|
|
107
163
|
this._items = [];
|
|
164
|
+
// Nothing is held while the flush runs; a replay that fails re-arms through `add()`.
|
|
165
|
+
this._armExpiry(now);
|
|
166
|
+
|
|
167
|
+
if (sentEntries === 'drop') {
|
|
168
|
+
for (const item of items.filter((entry) => entry.sent)) {
|
|
169
|
+
this._logger.warn(
|
|
170
|
+
`[InMemoryBuffer] A buffered message for ${describeTarget(item)} is not re-sent after the broker unblocked `
|
|
171
|
+
+ `the connection: its frame had been sent, and it was buffered ${now - item.createdAt} ms ago - Expected: the broker delivers `
|
|
172
|
+
+ 'the frames it held during the alarm by itself once it reads the socket again, so a re-send would '
|
|
173
|
+
+ 'be one more copy. Fix: nothing to do here; a consumer that must not see a message twice '
|
|
174
|
+
+ 'deduplicates it (conf mq-consumer-contract 009), and a message the broker did NOT keep is '
|
|
175
|
+
+ 'a broker-side loss this client cannot see.'
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
items = items.filter((entry) => !entry.sent);
|
|
179
|
+
}
|
|
108
180
|
|
|
109
181
|
// Kritické zprávy (priority === 'critical') házíme dopředu
|
|
110
182
|
items.sort((a, b) => {
|
|
@@ -125,7 +197,9 @@ class InMemoryBuffer {
|
|
|
125
197
|
this._logger.warn(
|
|
126
198
|
`[InMemoryBuffer] Failed to flush message for queue '${item.queue}': ${err.message}`
|
|
127
199
|
);
|
|
128
|
-
|
|
200
|
+
// The replay is one more attempt: if IT wrote the frame, the entry says so from now on.
|
|
201
|
+
const sent = item.sent || (err !== null && typeof err === 'object' && err.sent === true);
|
|
202
|
+
await this.add(item.queue, item.buffer, item.options, item.priority, sent);
|
|
129
203
|
}
|
|
130
204
|
}
|
|
131
205
|
|
|
@@ -140,15 +214,89 @@ class InMemoryBuffer {
|
|
|
140
214
|
return this._items.length;
|
|
141
215
|
}
|
|
142
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Aim the one expiry timer at the oldest entry's deadline — or disarm it when nothing is held.
|
|
219
|
+
*
|
|
220
|
+
* WHY A TIMER (d.1275, INFRA 531). Until d.1275 expiry ran only inside `add()`, `flush()` and
|
|
221
|
+
* `size()`. The buffer is emptied by a reconnect or by `unblocked`; an outage of a CHANNEL
|
|
222
|
+
* alone ends with neither, so an entry outlived its TTL until some unrelated publish touched
|
|
223
|
+
* the buffer — measured on the dev scheduler at 584 843 ms and 5 267 969 ms against a TTL of
|
|
224
|
+
* 300 000, and the ERROR line saying the message was lost came that late. The deadline is
|
|
225
|
+
* the TTL; the line now comes at the deadline.
|
|
226
|
+
*
|
|
227
|
+
* One timer, re-aimed on every change, rather than one per entry: entries are FIFO by
|
|
228
|
+
* `createdAt`, so the oldest one's deadline is the next one that matters. Re-aiming always
|
|
229
|
+
* clears the previous handle first, so a handle the transport's teardown already swept
|
|
230
|
+
* (`_clearAllTimers()`) is never trusted to still be armed.
|
|
231
|
+
*
|
|
232
|
+
* @param {number} now - the clock reading the caller took
|
|
233
|
+
* @private
|
|
234
|
+
*/
|
|
235
|
+
_armExpiry(now) {
|
|
236
|
+
if (this._expiryTimer !== null) {
|
|
237
|
+
this._timers.clearTimeout(this._expiryTimer);
|
|
238
|
+
this._expiryTimer = null;
|
|
239
|
+
}
|
|
240
|
+
if (this._items.length === 0) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const oldest = this._items.reduce((min, item) => Math.min(min, item.createdAt), Infinity);
|
|
244
|
+
// `_cleanupExpired()` drops an entry once its age is MORE than the TTL, so the timer
|
|
245
|
+
// fires one millisecond past the deadline.
|
|
246
|
+
const delay = Math.max(0, oldest + this._ttlMs + 1 - now);
|
|
247
|
+
this._expiryTimer = this._timers.setTimeout(() => {
|
|
248
|
+
this._expiryTimer = null;
|
|
249
|
+
const firedAt = Date.now();
|
|
250
|
+
this._cleanupExpired(firedAt);
|
|
251
|
+
this._armExpiry(firedAt);
|
|
252
|
+
}, delay);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Drop the entries older than the TTL — and say, as an ERROR, what was dropped (d.1145, conf
|
|
257
|
+
* `mq-publish-buffer` 001).
|
|
258
|
+
*
|
|
259
|
+
* Until d.1145 this was one `info` line with a count and nothing else, the only trace of a
|
|
260
|
+
* message nobody would ever deliver (a memory alarm keeps the connection alive, so no
|
|
261
|
+
* reconnect flush comes — d.1143). One error per destination and `sent` value, with the
|
|
262
|
+
* count and the age of the oldest: a message whose frame never left is LOST, one whose frame
|
|
263
|
+
* had been sent may have been delivered by the broker on its own, and the two are never
|
|
264
|
+
* folded into one sentence.
|
|
265
|
+
*
|
|
266
|
+
* @param {number} now - the clock reading the caller took
|
|
267
|
+
* @private
|
|
268
|
+
*/
|
|
143
269
|
_cleanupExpired(now) {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
270
|
+
const expired = this._items.filter((item) => now - item.createdAt > this._ttlMs);
|
|
271
|
+
if (expired.length === 0) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this._items = this._items.filter((item) => now - item.createdAt <= this._ttlMs);
|
|
275
|
+
|
|
276
|
+
const groups = new Map();
|
|
277
|
+
for (const item of expired) {
|
|
278
|
+
const key = `${describeTarget(item)}|${item.sent}`;
|
|
279
|
+
const group = groups.get(key) || { target: describeTarget(item), sent: item.sent, count: 0, oldestMs: 0 };
|
|
280
|
+
group.count += 1;
|
|
281
|
+
group.oldestMs = Math.max(group.oldestMs, now - item.createdAt);
|
|
282
|
+
groups.set(key, group);
|
|
283
|
+
}
|
|
284
|
+
for (const { target, sent, count, oldestMs } of groups.values()) {
|
|
285
|
+
// `LOST after <ms>` is the phrase an operator greps for, and only a never-sent message
|
|
286
|
+
// carries it: a sent one may have been delivered by the broker (conf `mq-publish-buffer`
|
|
287
|
+
// 001 — the two are never folded into one sentence).
|
|
288
|
+
this._logger.error(
|
|
289
|
+
`[InMemoryBuffer] ${count} buffered message(s) for ${target} `
|
|
290
|
+
+ (sent ? `expired after ${oldestMs} ms` : `LOST after ${oldestMs} ms`)
|
|
291
|
+
+ ` (buffer TTL ${this._ttlMs} ms; oldest ${oldestMs} ms), sent=${sent} - `
|
|
292
|
+
+ (sent
|
|
293
|
+
? 'the frame had been sent, so the broker may have delivered it on its own; nothing re-sends it now. '
|
|
294
|
+
: 'the frame never left this client, so the message is LOST; nothing re-sends it now. ')
|
|
295
|
+
+ 'Expected: a reconnect, or the broker unblocking the connection, within publishBufferTtlMs '
|
|
296
|
+
+ '(a recovered publisher channel alone replays nothing). '
|
|
297
|
+
+ 'Fix: find why the broker stayed away that long (an alarm: management API /api/health/checks/alarms; '
|
|
298
|
+
+ 'an outage: the connection log); a message that must survive longer belongs to a caller that keeps '
|
|
299
|
+
+ 'it durably and publishes with bufferOnFailure: false.'
|
|
152
300
|
);
|
|
153
301
|
}
|
|
154
302
|
}
|
|
@@ -22,6 +22,8 @@ class MessageBuffer {
|
|
|
22
22
|
* @param {Object} options
|
|
23
23
|
* @param {Object} [options.inMemory] - In-memory buffer options
|
|
24
24
|
* @param {Object} options.logger - Logger s info/warn/error/debug (povinny)
|
|
25
|
+
* @param {{setTimeout: Function, clearTimeout: Function}} options.timers - The expiry clock,
|
|
26
|
+
* passed through unread: `InMemoryBuffer` owns the rule that it is required (d.1275).
|
|
25
27
|
*/
|
|
26
28
|
constructor(options = {}) {
|
|
27
29
|
this._logger = assertLogger(
|
|
@@ -35,6 +37,7 @@ class MessageBuffer {
|
|
|
35
37
|
maxSize: options.inMemory?.maxSize,
|
|
36
38
|
ttlMs: options.inMemory?.ttlMs,
|
|
37
39
|
logger: this._logger,
|
|
40
|
+
timers: options.timers,
|
|
38
41
|
});
|
|
39
42
|
|
|
40
43
|
}
|
|
@@ -50,7 +53,9 @@ class MessageBuffer {
|
|
|
50
53
|
async add(queue, buffer, options = {}, meta = {}) {
|
|
51
54
|
const priority = meta.priority || 'normal';
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
// `sent` is passed through unread: the in-memory buffer owns the rule that it is required
|
|
57
|
+
// (d.1145, conf `mq-publish-buffer` 001).
|
|
58
|
+
await this._inMemory.add(queue, buffer, options, priority, meta.sent);
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
/**
|
|
@@ -58,8 +63,8 @@ class MessageBuffer {
|
|
|
58
63
|
* @param {Function} flushFn - async (queue, buffer, options) => void
|
|
59
64
|
* @returns {Promise<number>} how many messages were replayed
|
|
60
65
|
*/
|
|
61
|
-
async flush(flushFn) {
|
|
62
|
-
return await this._inMemory.flush(flushFn);
|
|
66
|
+
async flush(flushFn, flushPolicy) {
|
|
67
|
+
return await this._inMemory.flush(flushFn, flushPolicy);
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
@@ -136,8 +136,12 @@ module.exports = {
|
|
|
136
136
|
type: 'integer',
|
|
137
137
|
minimum: 0,
|
|
138
138
|
description:
|
|
139
|
-
'AMQP heartbeat in SECONDS,
|
|
140
|
-
+ 'RABBITMQ_HEARTBEAT, else the module default
|
|
139
|
+
'AMQP heartbeat in SECONDS, proposed to the broker in the connection URL (?heartbeat=) at '
|
|
140
|
+
+ 'connect. Explicit value, else ENV RABBITMQ_HEARTBEAT, else the module default '
|
|
141
|
+
+ '(../defaults.js heartbeatSeconds). 0 does NOT switch the heartbeat off: amqplib\'s '
|
|
142
|
+
+ 'negotiation takes whichever side sets a limit, so the broker\'s own value applies '
|
|
143
|
+
+ '(RabbitMQ 60 s unless its configuration says otherwise; measured d.1126: a paused broker '
|
|
144
|
+
+ 'detected after 179 s). Only a broker that also offers 0 disables it.',
|
|
141
145
|
},
|
|
142
146
|
serviceName: {
|
|
143
147
|
type: 'string',
|
|
@@ -287,10 +291,12 @@ module.exports = {
|
|
|
287
291
|
type: 'integer',
|
|
288
292
|
minimum: 0,
|
|
289
293
|
description:
|
|
290
|
-
'How long this client waits for an ANSWER from the broker, in milliseconds: the
|
|
291
|
-
+ 'one publish attempt,
|
|
292
|
-
+ '
|
|
293
|
-
+ '
|
|
294
|
+
'How long this client waits for an ANSWER from the broker, in milliseconds — every wait: the '
|
|
295
|
+
+ 'confirm of one publish attempt, the close-ok of each handle disconnect() closes, and every '
|
|
296
|
+
+ 'request a channel sends and waits on (queue/exchange declarations, checkQueue, bindQueue, '
|
|
297
|
+
+ 'purge, delete, consume, cancel, prefetch, channel open; conf mq-broker-answer-budget 001, '
|
|
298
|
+
+ 'd.1135). One number for one question — it was named publishConfirmationTimeout until d.299, '
|
|
299
|
+
+ 'after the teardown had been spending it since d.283.',
|
|
294
300
|
},
|
|
295
301
|
connectTimeout: {
|
|
296
302
|
type: 'integer',
|
|
@@ -389,19 +395,9 @@ module.exports = {
|
|
|
389
395
|
healthCriticalCallback: callbackOrNull(
|
|
390
396
|
'Async function (health) => void, or null. Called when a health result is critical.'
|
|
391
397
|
),
|
|
392
|
-
criticalHealthShutdown:
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
'Whether a sustained critical health result makes the transport emit `health:shutdown`. '
|
|
396
|
-
+ 'Anything other than `false` means enabled. It only arms the event; ending the '
|
|
397
|
-
+ 'process remains the owner\'s decision.',
|
|
398
|
-
},
|
|
399
|
-
criticalHealthShutdownDelay: {
|
|
400
|
-
type: 'integer',
|
|
401
|
-
minimum: 0,
|
|
402
|
-
description:
|
|
403
|
-
'How long health must stay critical before that event is emitted, in milliseconds.',
|
|
404
|
-
},
|
|
398
|
+
// NO `criticalHealthShutdown` / `criticalHealthShutdownDelay` since d.1275: the
|
|
399
|
+
// `health:shutdown` event they armed reached no listener. `BaseClient` refuses both
|
|
400
|
+
// by name before this schema is applied (`NOT_A_KEY_OF_THIS_CLIENT`).
|
|
405
401
|
|
|
406
402
|
// ---------------------------------------------------------------------
|
|
407
403
|
// `queueCreationFilter` and `queueCreationCallback` stood here until d.419,
|
|
@@ -120,12 +120,13 @@ module.exports = {
|
|
|
120
120
|
publishRetryMaxDelay: 5000,
|
|
121
121
|
publishRetryBackoffMultiplier: 2,
|
|
122
122
|
|
|
123
|
-
// How long this client waits for an ANSWER from the broker, in ms
|
|
124
|
-
// of one publish attempt,
|
|
125
|
-
// closes
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// d.
|
|
123
|
+
// How long this client waits for an ANSWER from the broker, in ms — every wait:
|
|
124
|
+
// the confirm of one publish attempt, the `close-ok` of each handle
|
|
125
|
+
// `disconnect()` closes, and every request a channel sends and waits on
|
|
126
|
+
// (conf `mq-broker-answer-budget` 001, d.1135) — one number for all of them,
|
|
127
|
+
// which is why it carries the name of none. It was `publishConfirmationTimeout`
|
|
128
|
+
// until d.299, a name that was true of the publish path and false of the
|
|
129
|
+
// teardown that had used it since d.283.
|
|
129
130
|
brokerAnswerTimeout: 5000,
|
|
130
131
|
|
|
131
132
|
// How long the connect handshake may take before the attempt is abandoned, in
|
|
@@ -190,11 +191,9 @@ module.exports = {
|
|
|
190
191
|
healthReportCallback: null,
|
|
191
192
|
healthCriticalCallback: null,
|
|
192
193
|
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
criticalHealthShutdown: true,
|
|
197
|
-
criticalHealthShutdownDelay: 60000,
|
|
194
|
+
// NO `criticalHealthShutdown` / `criticalHealthShutdownDelay` (removed in d.1275,
|
|
195
|
+
// refused by name in `BaseClient`): they armed a `health:shutdown` event no one
|
|
196
|
+
// listened to. Critical health is reported; the process is the owner's.
|
|
198
197
|
|
|
199
198
|
// `recoveryScope` stood here until d.514, with the two callbacks it filtered
|
|
200
199
|
// (`queueCreationFilter`, `queueCreationCallback`, d.419) one batch ahead of
|
|
@@ -93,6 +93,28 @@ const INFRASTRUCTURE_PREFIX_LOOKUPS = Object.freeze({
|
|
|
93
93
|
|
|
94
94
|
const INFRASTRUCTURE_PREFIXES = Object.freeze(Object.keys(INFRASTRUCTURE_PREFIX_LOOKUPS));
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The OBSERVERS' COPIES of a run (d.1336): the queues `monitoring.workflow.fanout` copies a
|
|
98
|
+
* run's lifecycle events into for a reader that watches the run and is not part of it — the
|
|
99
|
+
* monitoring consumer's intake, the delivery endpoint's WS feed, the scheduler's end-of-run
|
|
100
|
+
* watch. A copy whose reader is away dies on its TTL and dead-letters while the run goes on,
|
|
101
|
+
* so a `message_dlq` from one of them is a lost copy of the run's telemetry, not a lost run;
|
|
102
|
+
* the monitoring consumer and the scheduler both decide on that (INFRA 559).
|
|
103
|
+
*
|
|
104
|
+
* DECLARED, NOT DERIVED — measured on this file: membership cannot be read off an entry. The
|
|
105
|
+
* binding to the fanout is each consumer's own act at boot (`bindQueueToFanout()`), written on
|
|
106
|
+
* the entries only as a comment; TTL plus a dead-letter route is what every family here
|
|
107
|
+
* carries. So the list is written ONCE, here, as `[section, key]` pairs, and every name is
|
|
108
|
+
* composed by `queueName()` — which refuses an entry this file does not declare, so a release
|
|
109
|
+
* that dropped one stops the importer instead of shortening the list. A new observer of a run
|
|
110
|
+
* joins by one line here, beside its declaration below.
|
|
111
|
+
*/
|
|
112
|
+
const WORKFLOW_OBSERVER_COPIES = Object.freeze([
|
|
113
|
+
['monitoring', 'workflow'],
|
|
114
|
+
['deliveryEvents', 'workflow.events'],
|
|
115
|
+
['scheduler', 'workflow.events']
|
|
116
|
+
]);
|
|
117
|
+
|
|
96
118
|
/** Every section declaring names under one prefix — derived, never a second list. */
|
|
97
119
|
const sectionsForPrefix = (prefix) =>
|
|
98
120
|
Object.keys(SECTION_PREFIXES).filter((section) => SECTION_PREFIXES[section] === prefix);
|
|
@@ -725,7 +747,8 @@ module.exports = {
|
|
|
725
747
|
|
|
726
748
|
/**
|
|
727
749
|
* monitoring.workflow.fanout - Fanout exchange for workflow events
|
|
728
|
-
* Publishers send to this exchange;
|
|
750
|
+
* Publishers send to this exchange; the queues bound to it — the observers' copies of a
|
|
751
|
+
* run — are listed once, in `WORKFLOW_OBSERVER_COPIES` at the top of this file.
|
|
729
752
|
*/
|
|
730
753
|
'workflow.fanout': {
|
|
731
754
|
type: 'exchange',
|
|
@@ -1302,6 +1325,17 @@ module.exports = {
|
|
|
1302
1325
|
return INFRASTRUCTURE_PREFIXES;
|
|
1303
1326
|
},
|
|
1304
1327
|
|
|
1328
|
+
/**
|
|
1329
|
+
* The names of the observers' copies of a run — `WORKFLOW_OBSERVER_COPIES` above, each
|
|
1330
|
+
* composed by `queueName()`. Published as `topology.workflowObserverCopyQueues`; this is
|
|
1331
|
+
* the one place the list is read from its declaration (d.1336).
|
|
1332
|
+
* @returns {ReadonlyArray<string>} Frozen queue names, in declaration order.
|
|
1333
|
+
* @throws {ValidationError} when a listed entry is not declared in this file.
|
|
1334
|
+
*/
|
|
1335
|
+
workflowObserverCopyQueues() {
|
|
1336
|
+
return Object.freeze(WORKFLOW_OBSERVER_COPIES.map(([section, key]) => this.queueName(section, key)));
|
|
1337
|
+
},
|
|
1338
|
+
|
|
1305
1339
|
/**
|
|
1306
1340
|
* Check if queue name is an infrastructure queue.
|
|
1307
1341
|
*
|
package/src/index.js
CHANGED
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
} = require('./utils/errorHandler');
|
|
24
24
|
const {
|
|
25
25
|
TransientPublishError,
|
|
26
|
+
RecoveryEndedPublishError,
|
|
26
27
|
PermanentPublishError,
|
|
27
28
|
QueueNotFoundError,
|
|
28
29
|
classifyPublishError,
|
|
@@ -87,6 +88,7 @@ module.exports.errorCodes = Object.freeze({
|
|
|
87
88
|
});
|
|
88
89
|
module.exports.publishErrors = {
|
|
89
90
|
TransientPublishError,
|
|
91
|
+
RecoveryEndedPublishError,
|
|
90
92
|
PermanentPublishError,
|
|
91
93
|
QueueNotFoundError,
|
|
92
94
|
classifyPublishError,
|
|
@@ -139,6 +141,26 @@ module.exports.queueConfig = queueConfig;
|
|
|
139
141
|
* gone. Frozen, because an importer must not be able to rewrite platform topology.
|
|
140
142
|
*/
|
|
141
143
|
module.exports.topology = Object.freeze({
|
|
144
|
+
/**
|
|
145
|
+
* The queue every infrastructure service publishes its health checks to and the
|
|
146
|
+
* registry drains (`docs/standards/queue-ownership.md` § Infrastructure Queue Creation
|
|
147
|
+
* Order). Declared in `queueConfig` all along; composed here since d.1307, because the
|
|
148
|
+
* name was carried as a literal one floor up — `@onlineapps/service-common`
|
|
149
|
+
* `src/defaults.js` `infrastructureHealthQueueName`, which the registry, the health
|
|
150
|
+
* publisher and the wrapper read and call "the one owner" — so the platform had two
|
|
151
|
+
* declaring owners of one wire name (finding d.1306). Which side cites this export is
|
|
152
|
+
* the owner's decision (d.1306 A/B); the export is the step both ways share.
|
|
153
|
+
*/
|
|
154
|
+
infrastructureHealthChecksQueue: queueConfig.queueName('infrastructure', 'health.checks'),
|
|
155
|
+
/**
|
|
156
|
+
* The observers' copies of a run (d.1336, INFRA 559): the queues bound to
|
|
157
|
+
* `monitoring.workflow.fanout` for a reader that watches the run and is not part of it.
|
|
158
|
+
* A dead letter from one of them is a lost copy, not a lost run — the monitoring consumer
|
|
159
|
+
* (`observer_copy`) and the scheduler both decide on that and selected the queues by hand,
|
|
160
|
+
* twice. Declared once in `queueConfig` (`WORKFLOW_OBSERVER_COPIES`), names composed by
|
|
161
|
+
* `queueName()`; a frozen array.
|
|
162
|
+
*/
|
|
163
|
+
workflowObserverCopyQueues: queueConfig.workflowObserverCopyQueues(),
|
|
142
164
|
/** Fanout exchange: registry publishes infrastructure health events here. */
|
|
143
165
|
infrastructureHealthEventsExchange: queueConfig.queueName('infrastructure', 'health.events'),
|
|
144
166
|
/** The monitoring consumer's queue bound to that exchange. */
|