@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
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
4
|
TransientPublishError,
|
|
5
|
+
RecoveryEndedPublishError,
|
|
5
6
|
PermanentPublishError,
|
|
6
7
|
QueueNotFoundError,
|
|
7
8
|
} = require('../utils/publishErrors');
|
|
8
|
-
const { ValidationError, PublishError } = require('../utils/errorHandler');
|
|
9
|
+
const { ValidationError, PublishError, carryVerdict } = require('../utils/errorHandler');
|
|
9
10
|
const MessageBuffer = require('../buffer/MessageBuffer');
|
|
10
11
|
const { assertLogger } = require('@onlineapps/logger-contract');
|
|
11
12
|
|
|
@@ -23,6 +24,8 @@ class PublishLayer {
|
|
|
23
24
|
* @param {Object} options.client - Instance RabbitMQClient
|
|
24
25
|
* @param {Object} options.logger - Logger s info/warn/error/debug (povinný)
|
|
25
26
|
* @param {Object} [options.bufferConfig] - Konfigurace pro MessageBuffer
|
|
27
|
+
* @param {{setTimeout: Function, clearTimeout: Function}} options.timers - The clock the
|
|
28
|
+
* buffer expires its entries on (required, d.1275; the transport passes its own timer rail).
|
|
26
29
|
* @param {boolean} [options.retryEnabled=true] - Zapnout retry
|
|
27
30
|
* @param {number} [options.maxRetries=3] - Max počet pokusů
|
|
28
31
|
* @param {number} [options.retryBaseDelay=100] - Base delay v ms
|
|
@@ -114,7 +117,11 @@ class PublishLayer {
|
|
|
114
117
|
this._buffer = new MessageBuffer({
|
|
115
118
|
inMemory: options.bufferConfig?.inMemory,
|
|
116
119
|
logger: this._logger,
|
|
120
|
+
timers: options.timers,
|
|
117
121
|
});
|
|
122
|
+
// Read by the one sentence that tells a caller how long the buffer holds its message
|
|
123
|
+
// (`_holdOnFailure()`); `InMemoryBuffer` has already refused a missing value above.
|
|
124
|
+
this._bufferTtlMs = options.bufferConfig.inMemory.ttlMs;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
/**
|
|
@@ -122,8 +129,35 @@ class PublishLayer {
|
|
|
122
129
|
* @param {string} queue
|
|
123
130
|
* @param {Buffer} buffer
|
|
124
131
|
* @param {Object} [options]
|
|
132
|
+
* @param {boolean} [options.bufferOnFailure=true] - Whether a message whose publish failed
|
|
133
|
+
* transiently is held in the buffer and replayed after the next reconnect. `false` is for
|
|
134
|
+
* a caller that keeps the message durably elsewhere (conf `step-publish-failure` 001):
|
|
135
|
+
* nothing is held, so nothing replays behind its back. On each of the three transient
|
|
136
|
+
* paths (retries exhausted, retry disabled, the awaited reconnect failed) the thrown
|
|
137
|
+
* error carries `buffered` (boolean) — `true` only when the buffer really holds the
|
|
138
|
+
* message. A non-boolean value is refused before any attempt.
|
|
139
|
+
*
|
|
140
|
+
* Every error this method throws also carries `sent` (boolean, d.1145, conf
|
|
141
|
+
* `mq-publish-buffer` 001): `true` when ANY attempt of this call wrote the frame to the
|
|
142
|
+
* channel, so the broker may hold — and after a memory alarm deliver — the message even
|
|
143
|
+
* though the call failed; `false` when no attempt got that far. The buffer entry records the
|
|
144
|
+
* same fact, and the flush after `unblocked` reads it.
|
|
145
|
+
* @throws {ValidationError} When `options.bufferOnFailure` is present and not a boolean.
|
|
125
146
|
*/
|
|
126
147
|
async publish(queue, buffer, options = {}) {
|
|
148
|
+
// Refused HERE, before any attempt: a value that cannot say yes or no must not be read
|
|
149
|
+
// as either — `'false'` is truthy, and `0` would be read as "no" by accident
|
|
150
|
+
// (`architecture-principles.md` §3, §4). The one owner of the option is this layer;
|
|
151
|
+
// `BaseClient.publish()` only carries it.
|
|
152
|
+
if (options.bufferOnFailure !== undefined && typeof options.bufferOnFailure !== 'boolean') {
|
|
153
|
+
throw new ValidationError(
|
|
154
|
+
`[PublishLayer] options.bufferOnFailure must be a boolean - got ${JSON.stringify(options.bufferOnFailure)} `
|
|
155
|
+
+ `for queue "${queue}". Expected: true (hold a transiently failed message and replay it after `
|
|
156
|
+
+ 'the reconnect — the default) or false (hold nothing; the caller keeps the message). '
|
|
157
|
+
+ 'Fix: pass bufferOnFailure: true or bufferOnFailure: false, or omit it.'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
127
161
|
// `priority` still orders the buffer's replay (critical first). What it no
|
|
128
162
|
// longer does is choose a DIFFERENT buffer: the persistent one was a stub
|
|
129
163
|
// that stored nothing, so routing critical messages to it dropped them
|
|
@@ -145,10 +179,40 @@ class PublishLayer {
|
|
|
145
179
|
try {
|
|
146
180
|
await this._client._publishOnce(queue, buffer, options);
|
|
147
181
|
} catch (err) {
|
|
148
|
-
|
|
182
|
+
const sent = this._sentOf(err, queue);
|
|
183
|
+
return await this._handlePublishError(err, queue, buffer, options, priority, sent);
|
|
149
184
|
}
|
|
150
185
|
}
|
|
151
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Whether the transport says the failed attempt's frame left the client.
|
|
189
|
+
*
|
|
190
|
+
* The transport writes `sent` on every error of `_publishOnce()` (d.1145). An error without
|
|
191
|
+
* it is a defect of the neighbour, and it is refused here by name rather than read as
|
|
192
|
+
* `false`: a message whose frame did leave would then be re-sent after `unblocked`, and
|
|
193
|
+
* that is the duplicate conf `mq-publish-buffer` 001 exists to prevent.
|
|
194
|
+
*
|
|
195
|
+
* @param {*} err - what `client._publishOnce()` threw
|
|
196
|
+
* @param {string} queue
|
|
197
|
+
* @returns {boolean}
|
|
198
|
+
* @throws {Error} when the error carries no boolean `sent`
|
|
199
|
+
* @private
|
|
200
|
+
*/
|
|
201
|
+
_sentOf(err, queue) {
|
|
202
|
+
if (err !== null && typeof err === 'object' && typeof err.sent === 'boolean') {
|
|
203
|
+
return err.sent;
|
|
204
|
+
}
|
|
205
|
+
throw new Error(
|
|
206
|
+
'[PublishLayer] The transport reported a failed attempt without saying whether its frame was sent - '
|
|
207
|
+
+ `queue "${queue}", error: ${err && err.message ? err.message : String(err)}. `
|
|
208
|
+
+ 'Expected: every error of client._publishOnce() carries sent (boolean) — RabbitMQClient writes it on '
|
|
209
|
+
+ 'every attempt (conf mq-publish-buffer 001). Fix: a stand-in transport must write err.sent as the '
|
|
210
|
+
+ 'real one does; the message is neither buffered nor retried, because nobody can say whether '
|
|
211
|
+
+ 'the broker may already hold it.',
|
|
212
|
+
{ cause: err }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
152
216
|
/**
|
|
153
217
|
* Publish s retry logikou
|
|
154
218
|
* @private
|
|
@@ -171,6 +235,14 @@ class PublishLayer {
|
|
|
171
235
|
* (the transport's `channel._closeError` + `_closeFrameNamesThisSend()`), the event is
|
|
172
236
|
* the one already emitted, and the subscription lives exactly as long as this publish.
|
|
173
237
|
*/
|
|
238
|
+
// Whether ANY attempt of this call wrote the frame to the channel (d.1145). Written on
|
|
239
|
+
// every error this loop throws — `said()` — and on the buffer entry.
|
|
240
|
+
let sentAny = false;
|
|
241
|
+
const said = (error) => {
|
|
242
|
+
error.sent = sentAny;
|
|
243
|
+
return error;
|
|
244
|
+
};
|
|
245
|
+
|
|
174
246
|
let lateRefusal = null;
|
|
175
247
|
const onRefusedLate = (event) => {
|
|
176
248
|
if (event.queue === queue && event.verdict === 'PermanentPublishError' && event.error) {
|
|
@@ -196,7 +268,7 @@ class PublishLayer {
|
|
|
196
268
|
retryable: false,
|
|
197
269
|
refusedLate: true
|
|
198
270
|
});
|
|
199
|
-
throw lateRefusal;
|
|
271
|
+
throw said(lateRefusal);
|
|
200
272
|
}
|
|
201
273
|
|
|
202
274
|
attempt++;
|
|
@@ -234,6 +306,7 @@ class PublishLayer {
|
|
|
234
306
|
|
|
235
307
|
} catch (err) {
|
|
236
308
|
lastError = err;
|
|
309
|
+
sentAny = this._sentOf(err, queue) || sentAny;
|
|
237
310
|
|
|
238
311
|
// Non-retryable errors - fail immediately
|
|
239
312
|
if (err instanceof PermanentPublishError || err instanceof QueueNotFoundError) {
|
|
@@ -243,7 +316,22 @@ class PublishLayer {
|
|
|
243
316
|
error: err.message,
|
|
244
317
|
retryable: false
|
|
245
318
|
});
|
|
246
|
-
throw err;
|
|
319
|
+
throw said(err);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// The attempt could not get its send channel because the connection stayed down
|
|
323
|
+
// (B1, d.1300): the recovery it waited for ended in standby or fatal, or none may
|
|
324
|
+
// start. Retrying NOW is pointless — the next use of the client starts the next
|
|
325
|
+
// cycle — so the message is held at once, on the same rail as the retry below that
|
|
326
|
+
// waited for a reconnect and saw it fail. Asked BEFORE the generic transient branch,
|
|
327
|
+
// because the class is a transient one (the message is held and replayed).
|
|
328
|
+
if (err instanceof RecoveryEndedPublishError) {
|
|
329
|
+
throw said(await this._failedAfterRecovery({
|
|
330
|
+
queue, buffer, options, priority, attempt, transient: err, recoveryError: err.cause || err, sentAny,
|
|
331
|
+
headline: '[PublishLayer] Publish failed: the recovery this publish waited for ended without a connection '
|
|
332
|
+
+ `(attempt ${attempt} for queue "${queue}") - ${err.message} `
|
|
333
|
+
+ 'Expected: a live connection before the send. '
|
|
334
|
+
}));
|
|
247
335
|
}
|
|
248
336
|
|
|
249
337
|
// Transient errors - check if we should retry or buffer
|
|
@@ -258,22 +346,12 @@ class PublishLayer {
|
|
|
258
346
|
this._logger.debug(`[PublishLayer] ✓ Reconnection completed, proceeding with retry ${attempt + 1}/${this._maxRetries}`);
|
|
259
347
|
continue; // Retry immediately after reconnect
|
|
260
348
|
} catch (reconnectErr) {
|
|
261
|
-
// Reconnection failed - buffer and fail
|
|
262
|
-
await this.
|
|
263
|
-
|
|
264
|
-
queue
|
|
265
|
-
attempt,
|
|
266
|
-
error: `Reconnection failed: ${reconnectErr.message}`,
|
|
267
|
-
retryable: false,
|
|
268
|
-
reconnectFailed: true
|
|
269
|
-
});
|
|
270
|
-
throw new PublishError(
|
|
271
|
-
`[PublishLayer] Publish failed: reconnection failed after ${attempt} attempts for queue "${queue}" - `
|
|
349
|
+
// Reconnection failed - buffer (unless the caller said not to) and fail
|
|
350
|
+
throw said(await this._failedAfterRecovery({
|
|
351
|
+
queue, buffer, options, priority, attempt, transient: err, recoveryError: reconnectErr, sentAny,
|
|
352
|
+
headline: `[PublishLayer] Publish failed: reconnection failed after ${attempt} attempts for queue "${queue}" - `
|
|
272
353
|
+ 'Expected: the transport to re-establish the connection before the retry. '
|
|
273
|
-
|
|
274
|
-
queue,
|
|
275
|
-
reconnectErr
|
|
276
|
-
);
|
|
354
|
+
}));
|
|
277
355
|
}
|
|
278
356
|
}
|
|
279
357
|
|
|
@@ -305,8 +383,8 @@ class PublishLayer {
|
|
|
305
383
|
continue;
|
|
306
384
|
}
|
|
307
385
|
|
|
308
|
-
// Max retries exceeded - buffer and fail
|
|
309
|
-
await this.
|
|
386
|
+
// Max retries exceeded - buffer (unless the caller said not to) and fail
|
|
387
|
+
const held = await this._holdOnFailure(queue, buffer, options, priority, err, sentAny);
|
|
310
388
|
this._client.emit('publish:failed', {
|
|
311
389
|
queue,
|
|
312
390
|
attempt,
|
|
@@ -314,21 +392,23 @@ class PublishLayer {
|
|
|
314
392
|
retryable: true,
|
|
315
393
|
maxRetriesExceeded: true
|
|
316
394
|
});
|
|
317
|
-
|
|
395
|
+
const failure = new PublishError(
|
|
318
396
|
`[PublishLayer] Publish failed after ${attempt} attempts for queue "${queue}" - `
|
|
319
397
|
+ 'Expected: a retryable broker error to clear within maxRetries. '
|
|
320
|
-
+
|
|
398
|
+
+ `Fix: read error.cause for the last broker reason; ${held.sentence}`,
|
|
321
399
|
queue,
|
|
322
400
|
err
|
|
323
401
|
);
|
|
402
|
+
failure.buffered = held.buffered;
|
|
403
|
+
throw said(failure);
|
|
324
404
|
}
|
|
325
405
|
|
|
326
406
|
// Unknown error - fail immediately
|
|
327
|
-
throw err;
|
|
407
|
+
throw said(err);
|
|
328
408
|
}
|
|
329
409
|
}
|
|
330
410
|
|
|
331
|
-
throw (
|
|
411
|
+
throw said(
|
|
332
412
|
lastError
|
|
333
413
|
|| new PublishError(
|
|
334
414
|
`[PublishLayer] Publish failed for queue "${queue}" after ${attempt} attempts - `
|
|
@@ -345,48 +425,174 @@ class PublishLayer {
|
|
|
345
425
|
}
|
|
346
426
|
}
|
|
347
427
|
|
|
428
|
+
/**
|
|
429
|
+
* The one ending for "the recovery this publish depended on did not arrive": hold the
|
|
430
|
+
* message (unless the caller said not to), report the failure, and build the error the
|
|
431
|
+
* caller gets — with `buffered` written on it and the verdict of a fatal ending carried on
|
|
432
|
+
* top (`carryVerdict()`, d.1297/d.1300).
|
|
433
|
+
*
|
|
434
|
+
* Two callers, one body (d.1300): the retry that waited for a reconnect in flight and saw
|
|
435
|
+
* it fail, and the attempt whose send channel could not be had because the connection
|
|
436
|
+
* stayed down (`RecoveryEndedPublishError`). Until d.1300 only the first held the message;
|
|
437
|
+
* the second threw the wait's ending raw, so two near-identical situations ended two ways
|
|
438
|
+
* (INFRA-DOCS ID254 point 1).
|
|
439
|
+
*
|
|
440
|
+
* @private
|
|
441
|
+
* @returns {Promise<PublishError>} the failure to throw; the caller writes `sent` on it
|
|
442
|
+
*/
|
|
443
|
+
async _failedAfterRecovery({ queue, buffer, options, priority, attempt, transient, recoveryError, sentAny, headline }) {
|
|
444
|
+
const held = await this._holdOnFailure(queue, buffer, options, priority, transient, sentAny);
|
|
445
|
+
this._client.emit('publish:failed', {
|
|
446
|
+
queue,
|
|
447
|
+
attempt,
|
|
448
|
+
error: `Reconnection failed: ${recoveryError.message}`,
|
|
449
|
+
retryable: false,
|
|
450
|
+
reconnectFailed: true
|
|
451
|
+
});
|
|
452
|
+
const failure = new PublishError(
|
|
453
|
+
`${headline}Fix: read error.cause for the reconnect reason; ${held.sentence}`,
|
|
454
|
+
queue,
|
|
455
|
+
recoveryError
|
|
456
|
+
);
|
|
457
|
+
failure.buffered = held.buffered;
|
|
458
|
+
return carryVerdict(failure, recoveryError);
|
|
459
|
+
}
|
|
460
|
+
|
|
348
461
|
/**
|
|
349
462
|
* Handle publish error - buffer transient errors
|
|
350
463
|
* @private
|
|
351
464
|
*/
|
|
352
|
-
async _handlePublishError(err, queue, buffer, options, priority) {
|
|
465
|
+
async _handlePublishError(err, queue, buffer, options, priority, sent) {
|
|
353
466
|
if (err instanceof QueueNotFoundError) {
|
|
354
467
|
this._logger.error(`[PublishLayer] QueueNotFoundError for '${queue}' (infra=${err.isInfrastructure}): ${err.message}`);
|
|
355
468
|
throw err;
|
|
356
469
|
}
|
|
357
470
|
|
|
358
471
|
if (err instanceof TransientPublishError) {
|
|
359
|
-
await this.
|
|
472
|
+
const held = await this._holdOnFailure(queue, buffer, options, priority, err, sent);
|
|
473
|
+
// The transport's own error, thrown as it came — this path has always done that —
|
|
474
|
+
// with the one fact only this layer knows written on it.
|
|
475
|
+
err.buffered = held.buffered;
|
|
360
476
|
throw err;
|
|
361
477
|
}
|
|
362
478
|
|
|
363
479
|
throw err;
|
|
364
480
|
}
|
|
365
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Hold a transiently failed message in the buffer — or, when the caller passed
|
|
484
|
+
* `bufferOnFailure: false`, deliberately do not — and say which happened.
|
|
485
|
+
*
|
|
486
|
+
* The one decision point of the three transient paths, so the option cannot be honoured
|
|
487
|
+
* on one path and forgotten on another.
|
|
488
|
+
*
|
|
489
|
+
* @private
|
|
490
|
+
* @returns {Promise<{buffered: boolean, sentence: string}>} `buffered` is `true` only when
|
|
491
|
+
* the buffer really holds the message; `sentence` ends the thrown error's `Fix:` with
|
|
492
|
+
* what the caller can rely on.
|
|
493
|
+
*/
|
|
494
|
+
async _holdOnFailure(queue, buffer, options, priority, err, sent) {
|
|
495
|
+
if (options.bufferOnFailure === false) {
|
|
496
|
+
this._logger.warn(
|
|
497
|
+
`[PublishLayer] Message for queue '${queue}' NOT buffered (bufferOnFailure: false) after a transient error: ${err.message}`
|
|
498
|
+
);
|
|
499
|
+
return {
|
|
500
|
+
buffered: false,
|
|
501
|
+
sentence: 'the message is NOT buffered (bufferOnFailure: false), so nothing replays it — '
|
|
502
|
+
+ 'the caller holds the only copy (error.buffered === false).'
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
// Asked AFTER the caller's explicit choice above (`architecture-principles.md` §8: the
|
|
506
|
+
// option the caller passed is what its sentence names), and before any hold.
|
|
507
|
+
// A client that will never reconnect on its own cannot replay anything: the two flushes
|
|
508
|
+
// are `reconnected` and `unblocked`, and neither comes after the connection is declared
|
|
509
|
+
// lost, after `disconnect()`, or with `reconnectEnabled: false`. Holding there is a loss
|
|
510
|
+
// with a delay (reported LOST at TTL), not a safety — so nothing is held, and the error
|
|
511
|
+
// says so (d.1301, the lead's decision narrowing B1 of d.1300). The transport writes the
|
|
512
|
+
// fact (`replayable`) where it knows the state; this layer only reads it.
|
|
513
|
+
if (err instanceof RecoveryEndedPublishError && err.replayable === false) {
|
|
514
|
+
this._logger.warn(
|
|
515
|
+
`[PublishLayer] Message for queue '${queue}' NOT buffered: this client will not reconnect, so no replay could come - ${err.message}`
|
|
516
|
+
);
|
|
517
|
+
return {
|
|
518
|
+
buffered: false,
|
|
519
|
+
sentence: 'the message is NOT buffered: this client will not reconnect on its own (the connection is '
|
|
520
|
+
+ 'permanently lost, the caller closed it, or reconnectEnabled is false), so no reconnect could replay '
|
|
521
|
+
+ 'it — the caller holds the only copy (error.buffered === false).'
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
if (await this._bufferMessage(queue, buffer, options, priority, err, sent)) {
|
|
525
|
+
// What the buffer WILL do, and nothing more (d.1275). It used to say "the message is
|
|
526
|
+
// buffered, so it is not lost" — while the only two flushes are the reconnect and
|
|
527
|
+
// `unblocked`, so an outage of the publisher channel alone ended with the message
|
|
528
|
+
// expiring (INFRA 531, W224). Whether a channel recovery should flush too is an open
|
|
529
|
+
// decision (a sent entry re-sent after it may be a duplicate); until it is made, the
|
|
530
|
+
// sentence states the gap instead of papering over it.
|
|
531
|
+
return {
|
|
532
|
+
buffered: true,
|
|
533
|
+
sentence: `the message is buffered for up to ${this._bufferTtlMs} ms (publishBufferTtlMs) and `
|
|
534
|
+
+ 'replayed only after a reconnect or after the broker unblocks the connection — a recovered '
|
|
535
|
+
+ 'publisher channel alone does not replay it; if neither comes within that time it expires and '
|
|
536
|
+
+ (sent
|
|
537
|
+
? 'is reported as an expired message whose frame had been sent (the broker may hold a copy) '
|
|
538
|
+
: 'is reported as LOST ')
|
|
539
|
+
+ '(an error line naming the queue or exchange), and nothing replays it then.'
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
buffered: false,
|
|
544
|
+
sentence: 'the message is NOT buffered (the buffer refused it — see the error log line before this one), '
|
|
545
|
+
+ 'so nothing replays it (error.buffered === false).'
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
366
549
|
/**
|
|
367
550
|
* Buffer message při transient error
|
|
368
551
|
* @private
|
|
552
|
+
* @returns {Promise<boolean>} whether the buffer now holds the message
|
|
369
553
|
*/
|
|
370
|
-
async _bufferMessage(queue, buffer, options, priority, err) {
|
|
554
|
+
async _bufferMessage(queue, buffer, options, priority, err, sent) {
|
|
371
555
|
try {
|
|
372
|
-
await this._buffer.add(queue, buffer, options, { priority });
|
|
556
|
+
await this._buffer.add(queue, buffer, options, { priority, sent });
|
|
373
557
|
this._logger.warn(`[PublishLayer] Buffered message for queue '${queue}' due to transient error: ${err.message}`);
|
|
374
558
|
this._client.emit('publish:buffered', { queue });
|
|
559
|
+
return true;
|
|
375
560
|
} catch (bufferErr) {
|
|
376
561
|
this._logger.error(`[PublishLayer] Failed to buffer message for queue '${queue}': ${bufferErr.message}`);
|
|
562
|
+
return false;
|
|
377
563
|
}
|
|
378
564
|
}
|
|
379
565
|
|
|
380
566
|
/**
|
|
381
|
-
*
|
|
567
|
+
* Re-send what the buffer holds — the one flush, with two triggers (d.1145, conf
|
|
568
|
+
* `mq-publish-buffer` 001):
|
|
569
|
+
*
|
|
570
|
+
* - `'reconnected'` — a new connection: every entry is re-sent, as it always was;
|
|
571
|
+
* - `'unblocked'` — the broker ended a memory alarm on the SAME connection: only the
|
|
572
|
+
* entries whose frame never left the client are re-sent; the sent ones are dropped with a
|
|
573
|
+
* warning, because the broker delivers those itself (d.1143 measured it).
|
|
574
|
+
*
|
|
575
|
+
* Every entry is re-sent with the publish options it was held with — nothing else is
|
|
576
|
+
* merged in (d.1145 removed the `flushOptions` argument nobody passed).
|
|
577
|
+
*
|
|
578
|
+
* @param {'reconnected'|'unblocked'} trigger - what ended the outage
|
|
579
|
+
* @returns {Promise<number>} how many entries were re-sent successfully
|
|
580
|
+
* @throws {Error} when the trigger is not one of the two
|
|
382
581
|
*/
|
|
383
|
-
async flushBuffered(
|
|
582
|
+
async flushBuffered(trigger) {
|
|
583
|
+
const SENT_ENTRIES = { reconnected: 'resend', unblocked: 'drop' };
|
|
584
|
+
if (!Object.prototype.hasOwnProperty.call(SENT_ENTRIES, trigger)) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
`[PublishLayer] flushBuffered() needs its trigger - got ${JSON.stringify(trigger)}. `
|
|
587
|
+
+ "Expected: 'reconnected' or 'unblocked' — they differ in what happens to a message whose frame "
|
|
588
|
+
+ 'was already sent (conf mq-publish-buffer 001). Fix: pass the event that ended the outage.'
|
|
589
|
+
);
|
|
590
|
+
}
|
|
384
591
|
const result = await this._buffer.flush(async (queue, buf, opts) => {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
});
|
|
592
|
+
await this._client._publishOnce(queue, buf, opts);
|
|
593
|
+
}, { sentEntries: SENT_ENTRIES[trigger] });
|
|
388
594
|
|
|
389
|
-
this._logger.info(`[PublishLayer] Flushed ${result} buffered message(s)`);
|
|
595
|
+
this._logger.info(`[PublishLayer] Flushed ${result} buffered message(s) after ${trigger}`);
|
|
390
596
|
return result;
|
|
391
597
|
}
|
|
392
598
|
}
|
|
@@ -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
|
*
|