@onlineapps/mq-client-core 5.1.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 +329 -0
- package/README.md +74 -12
- package/package.json +2 -2
- package/src/BaseClient.js +80 -66
- package/src/buffer/InMemoryBuffer.js +66 -3
- package/src/buffer/MessageBuffer.js +3 -0
- package/src/config/configSchema.js +3 -13
- package/src/config/defaultConfig.js +3 -5
- package/src/config/queueConfig.js +35 -1
- package/src/index.js +22 -0
- package/src/layers/PublishLayer.js +95 -18
- package/src/transports/rabbitmqClient.js +366 -76
- 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
|
/**
|
|
@@ -312,6 +319,21 @@ class PublishLayer {
|
|
|
312
319
|
throw said(err);
|
|
313
320
|
}
|
|
314
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
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
|
|
315
337
|
// Transient errors - check if we should retry or buffer
|
|
316
338
|
if (err instanceof TransientPublishError) {
|
|
317
339
|
// If we have more attempts, retry
|
|
@@ -325,23 +347,11 @@ class PublishLayer {
|
|
|
325
347
|
continue; // Retry immediately after reconnect
|
|
326
348
|
} catch (reconnectErr) {
|
|
327
349
|
// Reconnection failed - buffer (unless the caller said not to) and fail
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
queue
|
|
331
|
-
attempt,
|
|
332
|
-
error: `Reconnection failed: ${reconnectErr.message}`,
|
|
333
|
-
retryable: false,
|
|
334
|
-
reconnectFailed: true
|
|
335
|
-
});
|
|
336
|
-
const failure = new PublishError(
|
|
337
|
-
`[PublishLayer] Publish failed: reconnection failed after ${attempt} attempts for queue "${queue}" - `
|
|
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}" - `
|
|
338
353
|
+ 'Expected: the transport to re-establish the connection before the retry. '
|
|
339
|
-
|
|
340
|
-
queue,
|
|
341
|
-
reconnectErr
|
|
342
|
-
);
|
|
343
|
-
failure.buffered = held.buffered;
|
|
344
|
-
throw said(failure);
|
|
354
|
+
}));
|
|
345
355
|
}
|
|
346
356
|
}
|
|
347
357
|
|
|
@@ -415,6 +425,39 @@ class PublishLayer {
|
|
|
415
425
|
}
|
|
416
426
|
}
|
|
417
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
|
+
|
|
418
461
|
/**
|
|
419
462
|
* Handle publish error - buffer transient errors
|
|
420
463
|
* @private
|
|
@@ -459,8 +502,42 @@ class PublishLayer {
|
|
|
459
502
|
+ 'the caller holds the only copy (error.buffered === false).'
|
|
460
503
|
};
|
|
461
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
|
+
}
|
|
462
524
|
if (await this._bufferMessage(queue, buffer, options, priority, err, sent)) {
|
|
463
|
-
|
|
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
|
+
};
|
|
464
541
|
}
|
|
465
542
|
return {
|
|
466
543
|
buffered: false,
|