@onlineapps/mq-client-core 5.0.0 → 5.1.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 +145 -0
- package/README.md +50 -8
- package/package.json +1 -1
- package/src/BaseClient.js +21 -2
- package/src/buffer/InMemoryBuffer.js +98 -13
- package/src/buffer/MessageBuffer.js +5 -3
- package/src/config/configSchema.js +12 -6
- package/src/config/defaultConfig.js +7 -6
- package/src/layers/PublishLayer.js +152 -23
- package/src/monitoring-initiated.js +6 -9
- package/src/transports/rabbitmqClient.js +546 -75
|
@@ -122,8 +122,35 @@ class PublishLayer {
|
|
|
122
122
|
* @param {string} queue
|
|
123
123
|
* @param {Buffer} buffer
|
|
124
124
|
* @param {Object} [options]
|
|
125
|
+
* @param {boolean} [options.bufferOnFailure=true] - Whether a message whose publish failed
|
|
126
|
+
* transiently is held in the buffer and replayed after the next reconnect. `false` is for
|
|
127
|
+
* a caller that keeps the message durably elsewhere (conf `step-publish-failure` 001):
|
|
128
|
+
* nothing is held, so nothing replays behind its back. On each of the three transient
|
|
129
|
+
* paths (retries exhausted, retry disabled, the awaited reconnect failed) the thrown
|
|
130
|
+
* error carries `buffered` (boolean) — `true` only when the buffer really holds the
|
|
131
|
+
* message. A non-boolean value is refused before any attempt.
|
|
132
|
+
*
|
|
133
|
+
* Every error this method throws also carries `sent` (boolean, d.1145, conf
|
|
134
|
+
* `mq-publish-buffer` 001): `true` when ANY attempt of this call wrote the frame to the
|
|
135
|
+
* channel, so the broker may hold — and after a memory alarm deliver — the message even
|
|
136
|
+
* though the call failed; `false` when no attempt got that far. The buffer entry records the
|
|
137
|
+
* same fact, and the flush after `unblocked` reads it.
|
|
138
|
+
* @throws {ValidationError} When `options.bufferOnFailure` is present and not a boolean.
|
|
125
139
|
*/
|
|
126
140
|
async publish(queue, buffer, options = {}) {
|
|
141
|
+
// Refused HERE, before any attempt: a value that cannot say yes or no must not be read
|
|
142
|
+
// as either — `'false'` is truthy, and `0` would be read as "no" by accident
|
|
143
|
+
// (`architecture-principles.md` §3, §4). The one owner of the option is this layer;
|
|
144
|
+
// `BaseClient.publish()` only carries it.
|
|
145
|
+
if (options.bufferOnFailure !== undefined && typeof options.bufferOnFailure !== 'boolean') {
|
|
146
|
+
throw new ValidationError(
|
|
147
|
+
`[PublishLayer] options.bufferOnFailure must be a boolean - got ${JSON.stringify(options.bufferOnFailure)} `
|
|
148
|
+
+ `for queue "${queue}". Expected: true (hold a transiently failed message and replay it after `
|
|
149
|
+
+ 'the reconnect — the default) or false (hold nothing; the caller keeps the message). '
|
|
150
|
+
+ 'Fix: pass bufferOnFailure: true or bufferOnFailure: false, or omit it.'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
127
154
|
// `priority` still orders the buffer's replay (critical first). What it no
|
|
128
155
|
// longer does is choose a DIFFERENT buffer: the persistent one was a stub
|
|
129
156
|
// that stored nothing, so routing critical messages to it dropped them
|
|
@@ -145,10 +172,40 @@ class PublishLayer {
|
|
|
145
172
|
try {
|
|
146
173
|
await this._client._publishOnce(queue, buffer, options);
|
|
147
174
|
} catch (err) {
|
|
148
|
-
|
|
175
|
+
const sent = this._sentOf(err, queue);
|
|
176
|
+
return await this._handlePublishError(err, queue, buffer, options, priority, sent);
|
|
149
177
|
}
|
|
150
178
|
}
|
|
151
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Whether the transport says the failed attempt's frame left the client.
|
|
182
|
+
*
|
|
183
|
+
* The transport writes `sent` on every error of `_publishOnce()` (d.1145). An error without
|
|
184
|
+
* it is a defect of the neighbour, and it is refused here by name rather than read as
|
|
185
|
+
* `false`: a message whose frame did leave would then be re-sent after `unblocked`, and
|
|
186
|
+
* that is the duplicate conf `mq-publish-buffer` 001 exists to prevent.
|
|
187
|
+
*
|
|
188
|
+
* @param {*} err - what `client._publishOnce()` threw
|
|
189
|
+
* @param {string} queue
|
|
190
|
+
* @returns {boolean}
|
|
191
|
+
* @throws {Error} when the error carries no boolean `sent`
|
|
192
|
+
* @private
|
|
193
|
+
*/
|
|
194
|
+
_sentOf(err, queue) {
|
|
195
|
+
if (err !== null && typeof err === 'object' && typeof err.sent === 'boolean') {
|
|
196
|
+
return err.sent;
|
|
197
|
+
}
|
|
198
|
+
throw new Error(
|
|
199
|
+
'[PublishLayer] The transport reported a failed attempt without saying whether its frame was sent - '
|
|
200
|
+
+ `queue "${queue}", error: ${err && err.message ? err.message : String(err)}. `
|
|
201
|
+
+ 'Expected: every error of client._publishOnce() carries sent (boolean) — RabbitMQClient writes it on '
|
|
202
|
+
+ 'every attempt (conf mq-publish-buffer 001). Fix: a stand-in transport must write err.sent as the '
|
|
203
|
+
+ 'real one does; the message is neither buffered nor retried, because nobody can say whether '
|
|
204
|
+
+ 'the broker may already hold it.',
|
|
205
|
+
{ cause: err }
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
152
209
|
/**
|
|
153
210
|
* Publish s retry logikou
|
|
154
211
|
* @private
|
|
@@ -171,6 +228,14 @@ class PublishLayer {
|
|
|
171
228
|
* (the transport's `channel._closeError` + `_closeFrameNamesThisSend()`), the event is
|
|
172
229
|
* the one already emitted, and the subscription lives exactly as long as this publish.
|
|
173
230
|
*/
|
|
231
|
+
// Whether ANY attempt of this call wrote the frame to the channel (d.1145). Written on
|
|
232
|
+
// every error this loop throws — `said()` — and on the buffer entry.
|
|
233
|
+
let sentAny = false;
|
|
234
|
+
const said = (error) => {
|
|
235
|
+
error.sent = sentAny;
|
|
236
|
+
return error;
|
|
237
|
+
};
|
|
238
|
+
|
|
174
239
|
let lateRefusal = null;
|
|
175
240
|
const onRefusedLate = (event) => {
|
|
176
241
|
if (event.queue === queue && event.verdict === 'PermanentPublishError' && event.error) {
|
|
@@ -196,7 +261,7 @@ class PublishLayer {
|
|
|
196
261
|
retryable: false,
|
|
197
262
|
refusedLate: true
|
|
198
263
|
});
|
|
199
|
-
throw lateRefusal;
|
|
264
|
+
throw said(lateRefusal);
|
|
200
265
|
}
|
|
201
266
|
|
|
202
267
|
attempt++;
|
|
@@ -234,6 +299,7 @@ class PublishLayer {
|
|
|
234
299
|
|
|
235
300
|
} catch (err) {
|
|
236
301
|
lastError = err;
|
|
302
|
+
sentAny = this._sentOf(err, queue) || sentAny;
|
|
237
303
|
|
|
238
304
|
// Non-retryable errors - fail immediately
|
|
239
305
|
if (err instanceof PermanentPublishError || err instanceof QueueNotFoundError) {
|
|
@@ -243,7 +309,7 @@ class PublishLayer {
|
|
|
243
309
|
error: err.message,
|
|
244
310
|
retryable: false
|
|
245
311
|
});
|
|
246
|
-
throw err;
|
|
312
|
+
throw said(err);
|
|
247
313
|
}
|
|
248
314
|
|
|
249
315
|
// Transient errors - check if we should retry or buffer
|
|
@@ -258,8 +324,8 @@ class PublishLayer {
|
|
|
258
324
|
this._logger.debug(`[PublishLayer] ✓ Reconnection completed, proceeding with retry ${attempt + 1}/${this._maxRetries}`);
|
|
259
325
|
continue; // Retry immediately after reconnect
|
|
260
326
|
} catch (reconnectErr) {
|
|
261
|
-
// Reconnection failed - buffer and fail
|
|
262
|
-
await this.
|
|
327
|
+
// Reconnection failed - buffer (unless the caller said not to) and fail
|
|
328
|
+
const held = await this._holdOnFailure(queue, buffer, options, priority, err, sentAny);
|
|
263
329
|
this._client.emit('publish:failed', {
|
|
264
330
|
queue,
|
|
265
331
|
attempt,
|
|
@@ -267,13 +333,15 @@ class PublishLayer {
|
|
|
267
333
|
retryable: false,
|
|
268
334
|
reconnectFailed: true
|
|
269
335
|
});
|
|
270
|
-
|
|
336
|
+
const failure = new PublishError(
|
|
271
337
|
`[PublishLayer] Publish failed: reconnection failed after ${attempt} attempts for queue "${queue}" - `
|
|
272
338
|
+ 'Expected: the transport to re-establish the connection before the retry. '
|
|
273
|
-
+
|
|
339
|
+
+ `Fix: read error.cause for the reconnect reason; ${held.sentence}`,
|
|
274
340
|
queue,
|
|
275
341
|
reconnectErr
|
|
276
342
|
);
|
|
343
|
+
failure.buffered = held.buffered;
|
|
344
|
+
throw said(failure);
|
|
277
345
|
}
|
|
278
346
|
}
|
|
279
347
|
|
|
@@ -305,8 +373,8 @@ class PublishLayer {
|
|
|
305
373
|
continue;
|
|
306
374
|
}
|
|
307
375
|
|
|
308
|
-
// Max retries exceeded - buffer and fail
|
|
309
|
-
await this.
|
|
376
|
+
// Max retries exceeded - buffer (unless the caller said not to) and fail
|
|
377
|
+
const held = await this._holdOnFailure(queue, buffer, options, priority, err, sentAny);
|
|
310
378
|
this._client.emit('publish:failed', {
|
|
311
379
|
queue,
|
|
312
380
|
attempt,
|
|
@@ -314,21 +382,23 @@ class PublishLayer {
|
|
|
314
382
|
retryable: true,
|
|
315
383
|
maxRetriesExceeded: true
|
|
316
384
|
});
|
|
317
|
-
|
|
385
|
+
const failure = new PublishError(
|
|
318
386
|
`[PublishLayer] Publish failed after ${attempt} attempts for queue "${queue}" - `
|
|
319
387
|
+ 'Expected: a retryable broker error to clear within maxRetries. '
|
|
320
|
-
+
|
|
388
|
+
+ `Fix: read error.cause for the last broker reason; ${held.sentence}`,
|
|
321
389
|
queue,
|
|
322
390
|
err
|
|
323
391
|
);
|
|
392
|
+
failure.buffered = held.buffered;
|
|
393
|
+
throw said(failure);
|
|
324
394
|
}
|
|
325
395
|
|
|
326
396
|
// Unknown error - fail immediately
|
|
327
|
-
throw err;
|
|
397
|
+
throw said(err);
|
|
328
398
|
}
|
|
329
399
|
}
|
|
330
400
|
|
|
331
|
-
throw (
|
|
401
|
+
throw said(
|
|
332
402
|
lastError
|
|
333
403
|
|| new PublishError(
|
|
334
404
|
`[PublishLayer] Publish failed for queue "${queue}" after ${attempt} attempts - `
|
|
@@ -349,44 +419,103 @@ class PublishLayer {
|
|
|
349
419
|
* Handle publish error - buffer transient errors
|
|
350
420
|
* @private
|
|
351
421
|
*/
|
|
352
|
-
async _handlePublishError(err, queue, buffer, options, priority) {
|
|
422
|
+
async _handlePublishError(err, queue, buffer, options, priority, sent) {
|
|
353
423
|
if (err instanceof QueueNotFoundError) {
|
|
354
424
|
this._logger.error(`[PublishLayer] QueueNotFoundError for '${queue}' (infra=${err.isInfrastructure}): ${err.message}`);
|
|
355
425
|
throw err;
|
|
356
426
|
}
|
|
357
427
|
|
|
358
428
|
if (err instanceof TransientPublishError) {
|
|
359
|
-
await this.
|
|
429
|
+
const held = await this._holdOnFailure(queue, buffer, options, priority, err, sent);
|
|
430
|
+
// The transport's own error, thrown as it came — this path has always done that —
|
|
431
|
+
// with the one fact only this layer knows written on it.
|
|
432
|
+
err.buffered = held.buffered;
|
|
360
433
|
throw err;
|
|
361
434
|
}
|
|
362
435
|
|
|
363
436
|
throw err;
|
|
364
437
|
}
|
|
365
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Hold a transiently failed message in the buffer — or, when the caller passed
|
|
441
|
+
* `bufferOnFailure: false`, deliberately do not — and say which happened.
|
|
442
|
+
*
|
|
443
|
+
* The one decision point of the three transient paths, so the option cannot be honoured
|
|
444
|
+
* on one path and forgotten on another.
|
|
445
|
+
*
|
|
446
|
+
* @private
|
|
447
|
+
* @returns {Promise<{buffered: boolean, sentence: string}>} `buffered` is `true` only when
|
|
448
|
+
* the buffer really holds the message; `sentence` ends the thrown error's `Fix:` with
|
|
449
|
+
* what the caller can rely on.
|
|
450
|
+
*/
|
|
451
|
+
async _holdOnFailure(queue, buffer, options, priority, err, sent) {
|
|
452
|
+
if (options.bufferOnFailure === false) {
|
|
453
|
+
this._logger.warn(
|
|
454
|
+
`[PublishLayer] Message for queue '${queue}' NOT buffered (bufferOnFailure: false) after a transient error: ${err.message}`
|
|
455
|
+
);
|
|
456
|
+
return {
|
|
457
|
+
buffered: false,
|
|
458
|
+
sentence: 'the message is NOT buffered (bufferOnFailure: false), so nothing replays it — '
|
|
459
|
+
+ 'the caller holds the only copy (error.buffered === false).'
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (await this._bufferMessage(queue, buffer, options, priority, err, sent)) {
|
|
463
|
+
return { buffered: true, sentence: 'the message is buffered, so it is not lost.' };
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
buffered: false,
|
|
467
|
+
sentence: 'the message is NOT buffered (the buffer refused it — see the error log line before this one), '
|
|
468
|
+
+ 'so nothing replays it (error.buffered === false).'
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
366
472
|
/**
|
|
367
473
|
* Buffer message při transient error
|
|
368
474
|
* @private
|
|
475
|
+
* @returns {Promise<boolean>} whether the buffer now holds the message
|
|
369
476
|
*/
|
|
370
|
-
async _bufferMessage(queue, buffer, options, priority, err) {
|
|
477
|
+
async _bufferMessage(queue, buffer, options, priority, err, sent) {
|
|
371
478
|
try {
|
|
372
|
-
await this._buffer.add(queue, buffer, options, { priority });
|
|
479
|
+
await this._buffer.add(queue, buffer, options, { priority, sent });
|
|
373
480
|
this._logger.warn(`[PublishLayer] Buffered message for queue '${queue}' due to transient error: ${err.message}`);
|
|
374
481
|
this._client.emit('publish:buffered', { queue });
|
|
482
|
+
return true;
|
|
375
483
|
} catch (bufferErr) {
|
|
376
484
|
this._logger.error(`[PublishLayer] Failed to buffer message for queue '${queue}': ${bufferErr.message}`);
|
|
485
|
+
return false;
|
|
377
486
|
}
|
|
378
487
|
}
|
|
379
488
|
|
|
380
489
|
/**
|
|
381
|
-
*
|
|
490
|
+
* Re-send what the buffer holds — the one flush, with two triggers (d.1145, conf
|
|
491
|
+
* `mq-publish-buffer` 001):
|
|
492
|
+
*
|
|
493
|
+
* - `'reconnected'` — a new connection: every entry is re-sent, as it always was;
|
|
494
|
+
* - `'unblocked'` — the broker ended a memory alarm on the SAME connection: only the
|
|
495
|
+
* entries whose frame never left the client are re-sent; the sent ones are dropped with a
|
|
496
|
+
* warning, because the broker delivers those itself (d.1143 measured it).
|
|
497
|
+
*
|
|
498
|
+
* Every entry is re-sent with the publish options it was held with — nothing else is
|
|
499
|
+
* merged in (d.1145 removed the `flushOptions` argument nobody passed).
|
|
500
|
+
*
|
|
501
|
+
* @param {'reconnected'|'unblocked'} trigger - what ended the outage
|
|
502
|
+
* @returns {Promise<number>} how many entries were re-sent successfully
|
|
503
|
+
* @throws {Error} when the trigger is not one of the two
|
|
382
504
|
*/
|
|
383
|
-
async flushBuffered(
|
|
505
|
+
async flushBuffered(trigger) {
|
|
506
|
+
const SENT_ENTRIES = { reconnected: 'resend', unblocked: 'drop' };
|
|
507
|
+
if (!Object.prototype.hasOwnProperty.call(SENT_ENTRIES, trigger)) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`[PublishLayer] flushBuffered() needs its trigger - got ${JSON.stringify(trigger)}. `
|
|
510
|
+
+ "Expected: 'reconnected' or 'unblocked' — they differ in what happens to a message whose frame "
|
|
511
|
+
+ 'was already sent (conf mq-publish-buffer 001). Fix: pass the event that ended the outage.'
|
|
512
|
+
);
|
|
513
|
+
}
|
|
384
514
|
const result = await this._buffer.flush(async (queue, buf, opts) => {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
});
|
|
515
|
+
await this._client._publishOnce(queue, buf, opts);
|
|
516
|
+
}, { sentEntries: SENT_ENTRIES[trigger] });
|
|
388
517
|
|
|
389
|
-
this._logger.info(`[PublishLayer] Flushed ${result} buffered message(s)`);
|
|
518
|
+
this._logger.info(`[PublishLayer] Flushed ${result} buffered message(s) after ${trigger}`);
|
|
390
519
|
return result;
|
|
391
520
|
}
|
|
392
521
|
}
|
|
@@ -8,15 +8,12 @@
|
|
|
8
8
|
* run `active` on it (`api/infra/api_monitoring/src/consumer/index.js`). A service that
|
|
9
9
|
* does not file it starts runs that are invisible for their whole life.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* half of the same wire (`docs/governance/confirmations/scheduler.md` 009). Both copies
|
|
18
|
-
* are retired by their own services in the change that adopts this module — no period
|
|
19
|
-
* with both (`architecture-principles.md` §11).
|
|
11
|
+
* One producer rail, by the owner's decision (`docs/governance/confirmations/scheduler.md`
|
|
12
|
+
* 009 and 024): the event is built and filed by this module, in the library that already
|
|
13
|
+
* owns the transport half of the same wire (`./monitoring-publish.js`). Every service that
|
|
14
|
+
* starts a run files it through here — the decision names the gateway and the scheduler.
|
|
15
|
+
* A service that builds the event in its own words is a second implementation of one
|
|
16
|
+
* concern, a defect by default (`change-discipline.md` § One rail per concern).
|
|
20
17
|
*
|
|
21
18
|
* WHAT THIS MODULE DOES NOT DO, deliberately:
|
|
22
19
|
*
|