@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
|
@@ -125,11 +125,10 @@ const REFUSAL_REPLY_CODES = [403, 530, 406];
|
|
|
125
125
|
* - `workflow_id` is the only spelling the envelope contract admits, and a
|
|
126
126
|
* reader that accepts the camelCase alias is forbidden as such, not merely a
|
|
127
127
|
* producer that writes it (`docs/biz/70-contracts/workflow-message.md` §2.2,
|
|
128
|
-
* owner decision `workflow-message-aliases` 001).
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* a reader that
|
|
132
|
-
* retire, because nothing then shows which producers still write it.
|
|
128
|
+
* owner decision `workflow-message-aliases` 001). No producer in `shared/**`
|
|
129
|
+
* or `infra/**` writes the alias (re-measured d.1134: the gateway's headers
|
|
130
|
+
* carry `workflow_id` alone, `infra/api_gateway/shared/workflowMessageHeaders.js`),
|
|
131
|
+
* and a reader that honoured it would hide the next producer that did.
|
|
133
132
|
* - a missing id is reported as missing. `'unknown'` is a value, and a log line
|
|
134
133
|
* carrying it claims a workflow by that name — the same reason
|
|
135
134
|
* `BaseClient._emitDeadLetterEvent()` resolves an absent id to `null` and
|
|
@@ -282,6 +281,9 @@ class RabbitMQClient extends EventEmitter {
|
|
|
282
281
|
// socket whose channels are still being recreated is not yet usable (d.340).
|
|
283
282
|
// Channels do the same through `channel._alive` (see `_isChannelAlive()`).
|
|
284
283
|
this._connectionAlive = false;
|
|
284
|
+
// The broker's reason while it blocks this connection (`connection.blocked`), `null` otherwise
|
|
285
|
+
// (d.1145). Written by `_onConnectionBlocked()` / `_onConnectionUnblocked()`.
|
|
286
|
+
this._connectionBlockedBy = null;
|
|
285
287
|
this._channel = null; // ConfirmChannel for publish operations
|
|
286
288
|
this._queueChannel = null; // Regular channel for queue operations (assertQueue, checkQueue)
|
|
287
289
|
this._consumerChannel = null; // Dedicated channel for consume operations
|
|
@@ -366,12 +368,14 @@ class RabbitMQClient extends EventEmitter {
|
|
|
366
368
|
this._publishRetryBaseDelay = this._config.publishRetryBaseDelay;
|
|
367
369
|
this._publishRetryMaxDelay = this._config.publishRetryMaxDelay;
|
|
368
370
|
this._publishRetryBackoffMultiplier = this._config.publishRetryBackoffMultiplier;
|
|
369
|
-
// ONE number,
|
|
370
|
-
//
|
|
371
|
-
// publish confirm waits for it (`_publishOnce`),
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
371
|
+
// ONE number, and therefore a name that belongs to none of its consumers
|
|
372
|
+
// alone: how long this client waits for an ANSWER from the broker. The
|
|
373
|
+
// publish confirm waits for it (`_publishOnce`), so does the whole teardown
|
|
374
|
+
// of `disconnect()` (d.283 chose deliberately not to invent a second number
|
|
375
|
+
// for that; the name it inherited then said "publish confirmation", which was
|
|
376
|
+
// true of one consumer and false of the other), and so does every request a
|
|
377
|
+
// channel sends and waits on (`_askBroker()`, conf `mq-broker-answer-budget`
|
|
378
|
+
// 001, d.1135).
|
|
375
379
|
this._brokerAnswerTimeout = this._config.brokerAnswerTimeout;
|
|
376
380
|
// How long the connect handshake may take before the attempt is abandoned.
|
|
377
381
|
this._connectTimeout = this._config.connectTimeout;
|
|
@@ -785,6 +789,71 @@ class RabbitMQClient extends EventEmitter {
|
|
|
785
789
|
this.emit('error', err);
|
|
786
790
|
});
|
|
787
791
|
connection.on('close', () => this._onConnectionDied());
|
|
792
|
+
// A broker in a memory or disk alarm blocks a connection that publishes and reads nothing
|
|
793
|
+
// more from it until the alarm clears; the connection stays alive, so no reconnect — and
|
|
794
|
+
// no flush — follows (measured d.1143 on the platform's image). amqplib announces the
|
|
795
|
+
// capability and re-emits both frames (`lib/connect.js`, `lib/channel_model.js`).
|
|
796
|
+
this._connectionBlockedBy = null;
|
|
797
|
+
connection.on('blocked', (reason) => this._onConnectionBlocked(connection, reason));
|
|
798
|
+
connection.on('unblocked', () => this._onConnectionUnblocked(connection));
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* The broker blocked this connection (`connection.blocked`, d.1145).
|
|
803
|
+
*
|
|
804
|
+
* Recorded as state — the publish path names it when it refuses (`_publishPathTransient()`)
|
|
805
|
+
* — and reported once as a warning. A frame of a stale connection (one this client already
|
|
806
|
+
* replaced) says nothing about the current one and is ignored.
|
|
807
|
+
*
|
|
808
|
+
* @param {Object} connection - the amqplib connection that received the frame
|
|
809
|
+
* @param {string} reason - the broker's reason (RabbitMQ: "low on memory", "low on disk")
|
|
810
|
+
* @private
|
|
811
|
+
*/
|
|
812
|
+
_onConnectionBlocked(connection, reason) {
|
|
813
|
+
if (connection !== this._connection) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
this._connectionBlockedBy = String(reason);
|
|
817
|
+
this._logger.warn(
|
|
818
|
+
`[RabbitMQClient] The broker blocked this connection (reason: ${this._connectionBlockedBy}) - `
|
|
819
|
+
+ 'Expected: an alarm-free broker. It reads nothing this client sends until the alarm clears: '
|
|
820
|
+
+ 'publishes and every request wait, and end on brokerAnswerTimeout. '
|
|
821
|
+
+ 'Fix: clear the broker alarm (management API /api/health/checks/alarms names it); the buffer '
|
|
822
|
+
+ 're-sends what never left once the broker unblocks the connection.'
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* The broker unblocked this connection (`connection.unblocked`, d.1145).
|
|
828
|
+
*
|
|
829
|
+
* The flush after it is the one rail `PublishLayer.flushBuffered()` with the trigger
|
|
830
|
+
* `'unblocked'`: only the entries whose frame never left the client are re-sent; the broker
|
|
831
|
+
* delivers the rest by itself (conf `mq-publish-buffer` 001). A failed flush is reported the
|
|
832
|
+
* way the flush after a reconnect reports it.
|
|
833
|
+
*
|
|
834
|
+
* @param {Object} connection - the amqplib connection that received the frame
|
|
835
|
+
* @private
|
|
836
|
+
*/
|
|
837
|
+
_onConnectionUnblocked(connection) {
|
|
838
|
+
if (connection !== this._connection) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
const wasBlockedBy = this._connectionBlockedBy;
|
|
842
|
+
this._connectionBlockedBy = null;
|
|
843
|
+
this._logger.warn(
|
|
844
|
+
`[RabbitMQClient] The broker unblocked this connection (it had been blocked: ${wasBlockedBy === null ? 'no blocked frame seen' : wasBlockedBy}) - `
|
|
845
|
+
+ 're-sending the buffered messages whose frame never left this client.'
|
|
846
|
+
);
|
|
847
|
+
this._publishLayer.flushBuffered('unblocked').then(
|
|
848
|
+
(flushed) => {
|
|
849
|
+
this._publishMonitor.trackFlushed(flushed);
|
|
850
|
+
this._logger.info(`[RabbitMQClient] ✓ Flushed ${flushed} buffered messages after the broker unblocked the connection`);
|
|
851
|
+
},
|
|
852
|
+
(flushErr) => {
|
|
853
|
+
this._logger.warn(`[RabbitMQClient] Failed to flush buffered messages after unblocked: ${flushErr.message}`);
|
|
854
|
+
this.emit('buffer:flush:failed', { error: flushErr.message, bufferSize: this._publishLayer._buffer.size() });
|
|
855
|
+
}
|
|
856
|
+
);
|
|
788
857
|
}
|
|
789
858
|
|
|
790
859
|
/**
|
|
@@ -1390,6 +1459,8 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1390
1459
|
*/
|
|
1391
1460
|
_onConnectionDied() {
|
|
1392
1461
|
this._connectionAlive = false;
|
|
1462
|
+
// A dead connection is no longer blocked; a new one starts unblocked (d.1145).
|
|
1463
|
+
this._connectionBlockedBy = null;
|
|
1393
1464
|
this._channel = null;
|
|
1394
1465
|
this._telemetryChannel = null;
|
|
1395
1466
|
this._queueChannel = null;
|
|
@@ -1437,12 +1508,56 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1437
1508
|
return { status: 'absent', error: null };
|
|
1438
1509
|
}
|
|
1439
1510
|
|
|
1440
|
-
const
|
|
1511
|
+
const outcome = await this._awaitBrokerAnswer(() => handle.close(), budgetMs);
|
|
1512
|
+
|
|
1513
|
+
if (outcome.status === 'unanswered') {
|
|
1514
|
+
this._logger.warn(
|
|
1515
|
+
`[RabbitMQClient] The broker did not answer the ${what} close within the teardown budget `
|
|
1516
|
+
+ `(${this._brokerAnswerTimeout}ms) - `
|
|
1517
|
+
+ 'Expected: a close-ok frame for the close this client sent. '
|
|
1518
|
+
+ 'Fix: nothing to do here — the teardown stops waiting and drops the socket, so '
|
|
1519
|
+
+ 'the teardown ends (disconnect(), or a connect() that failed half-way) and the process '
|
|
1520
|
+
+ 'can exit; a broker that never answers a close is a broker-side or network fault, and '
|
|
1521
|
+
+ 'the declared brokerAnswerTimeout sets how long this client gives it.'
|
|
1522
|
+
);
|
|
1523
|
+
return { status: 'unanswered', error: null };
|
|
1524
|
+
}
|
|
1525
|
+
if (outcome.status === 'refused') {
|
|
1526
|
+
return { status: 'refused', error: outcome.error };
|
|
1527
|
+
}
|
|
1528
|
+
return { status: 'closed', error: null };
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* Wait for the broker's answer to ONE request, never longer than `budgetMs`.
|
|
1533
|
+
*
|
|
1534
|
+
* THE one clock for "how long does this client wait for the broker to answer" (conf
|
|
1535
|
+
* `mq-broker-answer-budget` 001, d.1135). Its two readers are `_askBroker()` — every
|
|
1536
|
+
* request a channel or the connection sends and waits on — and
|
|
1537
|
+
* `_closeWithinTeardownBudget()`, which asks it for the `close-ok` of a teardown with what
|
|
1538
|
+
* is left of that teardown's budget. The publish confirm keeps its own timer inside the
|
|
1539
|
+
* send callback (`_publishOnce()`), because it is not a promise to race: it is a callback
|
|
1540
|
+
* that also has to be disarmed, counted and attributed.
|
|
1541
|
+
*
|
|
1542
|
+
* `request` is called here, synchronously, so a request that throws before it is sent
|
|
1543
|
+
* reads as a refusal rather than escaping the budget as an exception.
|
|
1544
|
+
*
|
|
1545
|
+
* @param {() => Promise<*>} request - sends the request and answers its reply
|
|
1546
|
+
* @param {number} budgetMs - how long the answer may take
|
|
1547
|
+
* @returns {Promise<{status: 'answered', value: *, pending: Promise<*>}
|
|
1548
|
+
* |{status: 'refused', error: Error, pending: Promise<*>}
|
|
1549
|
+
* |{status: 'unanswered', pending: Promise<*>}>} `pending` is the request's own
|
|
1550
|
+
* promise, for a caller that must deal with an answer arriving after the budget
|
|
1551
|
+
* @private
|
|
1552
|
+
*/
|
|
1553
|
+
async _awaitBrokerAnswer(request, budgetMs) {
|
|
1554
|
+
const pending = new Promise((resolve) => resolve(request()));
|
|
1555
|
+
const UNANSWERED = Symbol('broker-answer-budget-spent');
|
|
1441
1556
|
let deadlineTimer = null;
|
|
1442
1557
|
const outcome = await Promise.race([
|
|
1443
|
-
|
|
1444
|
-
() => ({ status: '
|
|
1445
|
-
(
|
|
1558
|
+
pending.then(
|
|
1559
|
+
(value) => ({ status: 'answered', value }),
|
|
1560
|
+
(error) => ({ status: 'refused', error })
|
|
1446
1561
|
),
|
|
1447
1562
|
new Promise((resolve) => {
|
|
1448
1563
|
deadlineTimer = this._setTimeout(() => resolve(UNANSWERED), budgetMs);
|
|
@@ -1451,19 +1566,145 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1451
1566
|
this._clearTimeout(deadlineTimer);
|
|
1452
1567
|
|
|
1453
1568
|
if (outcome === UNANSWERED) {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1569
|
+
return { status: 'unanswered', pending };
|
|
1570
|
+
}
|
|
1571
|
+
return { ...outcome, pending };
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* Ask the broker one thing and wait for its answer within `brokerAnswerTimeout`.
|
|
1576
|
+
*
|
|
1577
|
+
* ONE rail for every request this client sends and then waits on (conf
|
|
1578
|
+
* `mq-broker-answer-budget` 001, verdict A; d.1135): queue and exchange declarations,
|
|
1579
|
+
* `checkQueue`, `bindQueue`, `purgeQueue`, `deleteQueue`, `basic.consume`, `basic.cancel`,
|
|
1580
|
+
* `basic.qos` and every channel open. Until d.1135 none of them had a limit. A broker in a
|
|
1581
|
+
* memory alarm keeps the connection open and its heartbeats flowing while it answers
|
|
1582
|
+
* nothing — measured by d.1126 on an isolated broker of the platform's image: every one of
|
|
1583
|
+
* these calls still pending after 300 s, settled only when the alarm was cleared; a paused
|
|
1584
|
+
* broker was ended by amqplib's heartbeat after ~90 s. The heartbeat is not part of this
|
|
1585
|
+
* rail: in an alarm it never fires, so only the budget ends the wait.
|
|
1586
|
+
*
|
|
1587
|
+
* The budget is `brokerAnswerTimeout` and never a second number — its declared meaning is
|
|
1588
|
+
* exactly this question (`config/configSchema.js`).
|
|
1589
|
+
*
|
|
1590
|
+
* WHEN THE BUDGET IS SPENT:
|
|
1591
|
+
* - the channel that carried the request is DISCARDED (`_discardUnansweredChannel()`):
|
|
1592
|
+
* amqplib queues the requests of one channel behind each other, so every later request
|
|
1593
|
+
* on it would wait behind the silent one. The next operation opens a fresh channel;
|
|
1594
|
+
* - a request that OPENS a channel has no channel yet; the one the broker may still open
|
|
1595
|
+
* later is closed as soon as it arrives, so an alarm does not leak channels;
|
|
1596
|
+
* - the caller gets `ConnectionError` naming the operation, its target and the budget.
|
|
1597
|
+
* The publish path translates it into `TransientPublishError`
|
|
1598
|
+
* (`_publishPathTransient()`), so retry, the buffer and `bufferOnFailure: false` apply
|
|
1599
|
+
* to it exactly as to any other transient failure.
|
|
1600
|
+
*
|
|
1601
|
+
* A bare `Error('Channel ended, no reply will be forthcoming')` — amqplib's
|
|
1602
|
+
* `_rejectPending()` answering a request whose channel or connection died first, no class,
|
|
1603
|
+
* no `code` — is the same fact ("no answer came") and becomes the same class. It is
|
|
1604
|
+
* recognised by the carrier's STATE, never by the sentence (d.683): no `code`, a plain
|
|
1605
|
+
* `Error`, and a carrier that is dead by the time the rejection arrives (amqplib emits
|
|
1606
|
+
* `close` synchronously before the rejection's handlers run). A numbered refusal is an
|
|
1607
|
+
* ANSWER and passes through untouched, as does anything already typed.
|
|
1608
|
+
*
|
|
1609
|
+
* @param {Object} request
|
|
1610
|
+
* @param {string} request.operation - the request's name, as the message states it
|
|
1611
|
+
* @param {string} request.target - what it is about (queue, exchange, channel)
|
|
1612
|
+
* @param {Object|null} request.channel - the channel carrying it; `null` for a channel
|
|
1613
|
+
* open, which the connection carries
|
|
1614
|
+
* @param {() => Promise<*>} send - sends the request and answers its reply
|
|
1615
|
+
* @returns {Promise<*>} the broker's answer
|
|
1616
|
+
* @throws {ConnectionError} when no answer came within the budget, or the carrier ended
|
|
1617
|
+
* before it could
|
|
1618
|
+
* @private
|
|
1619
|
+
*/
|
|
1620
|
+
async _askBroker({ operation, target, channel }, send) {
|
|
1621
|
+
const budgetMs = this._brokerAnswerTimeout;
|
|
1622
|
+
const outcome = await this._awaitBrokerAnswer(send, budgetMs);
|
|
1623
|
+
|
|
1624
|
+
if (outcome.status === 'answered') {
|
|
1625
|
+
return outcome.value;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
if (outcome.status === 'unanswered') {
|
|
1629
|
+
if (channel) {
|
|
1630
|
+
this._discardUnansweredChannel(channel, `${operation} ${target}`);
|
|
1631
|
+
} else {
|
|
1632
|
+
outcome.pending.then(
|
|
1633
|
+
(late) => this._closeLateChannel(late, target),
|
|
1634
|
+
() => {}
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
throw new ConnectionError(
|
|
1638
|
+
`[RabbitMQClient] The broker did not answer ${operation} ${target} within brokerAnswerTimeout `
|
|
1639
|
+
+ `(${budgetMs}ms) - Expected: an answer to the request this client sent; a broker that `
|
|
1640
|
+
+ 'blocks publishers (memory or disk alarm) or is frozen keeps the connection open and '
|
|
1641
|
+
+ 'answers nothing. Fix: check the broker for an alarm (management API '
|
|
1642
|
+
+ '/api/health/checks/alarms) or a paused node; the channel that carried the request '
|
|
1643
|
+
+ 'is discarded and the next operation opens a fresh one. brokerAnswerTimeout is the '
|
|
1644
|
+
+ 'one budget for every answer this client waits for.'
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
const err = outcome.error;
|
|
1649
|
+
const carrierDead = channel ? !this._isChannelAlive(channel) : !this._connectionAlive;
|
|
1650
|
+
const bare = err instanceof Error && err.constructor === Error && err.code === undefined;
|
|
1651
|
+
if (bare && carrierDead) {
|
|
1652
|
+
throw new ConnectionError(
|
|
1653
|
+
`[RabbitMQClient] ${operation} ${target} got no answer: the ${channel ? 'channel' : 'connection'} `
|
|
1654
|
+
+ 'carrying it ended before the broker answered - Expected: an answer to the request '
|
|
1655
|
+
+ 'this client sent. Fix: nothing is refused — the request never got a reply; run the '
|
|
1656
|
+
+ 'operation again once the channel is back (error.cause carries amqplib\'s rejection).',
|
|
1657
|
+
err
|
|
1462
1658
|
);
|
|
1463
|
-
return { status: 'unanswered', error: null };
|
|
1464
1659
|
}
|
|
1660
|
+
throw err;
|
|
1661
|
+
}
|
|
1465
1662
|
|
|
1466
|
-
|
|
1663
|
+
/**
|
|
1664
|
+
* Take a channel whose request the broker did not answer out of service.
|
|
1665
|
+
*
|
|
1666
|
+
* It is marked dead first (`_isChannelAlive()`, the one liveness source — d.260), then
|
|
1667
|
+
* dropped from whichever slot still holds it — only if it still holds THIS channel, the
|
|
1668
|
+
* same obituary rule the close handlers keep — and then asked to close. The close is not
|
|
1669
|
+
* awaited: it is an answer from the same silent broker. amqplib sends it at once and
|
|
1670
|
+
* ignores every frame but `close-ok` from then on, so a broker that wakes up closes the
|
|
1671
|
+
* channel cleanly (its consumers cancelled, its unacked deliveries requeued) instead of
|
|
1672
|
+
* keeping it open with nobody holding it; its `close` event then reaches the handlers
|
|
1673
|
+
* attached to it, exactly as for any other channel that ends.
|
|
1674
|
+
*
|
|
1675
|
+
* @param {Object} channel - the amqplib channel that carried the unanswered request
|
|
1676
|
+
* @param {string} reason - which request went unanswered, for the post-mortem
|
|
1677
|
+
* @private
|
|
1678
|
+
*/
|
|
1679
|
+
_discardUnansweredChannel(channel, reason) {
|
|
1680
|
+
channel._alive = false;
|
|
1681
|
+
channel._closeReason = `No answer from the broker to ${reason} within brokerAnswerTimeout (${this._brokerAnswerTimeout}ms)`;
|
|
1682
|
+
if (this._channel === channel) this._channel = null;
|
|
1683
|
+
if (this._telemetryChannel === channel) this._telemetryChannel = null;
|
|
1684
|
+
if (this._queueChannel === channel) this._queueChannel = null;
|
|
1685
|
+
if (this._consumerChannel === channel) this._consumerChannel = null;
|
|
1686
|
+
channel.close().catch((err) => {
|
|
1687
|
+
this._logger.debug(`[RabbitMQClient] Discarded channel (${reason}) did not close: ${err.message}`);
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* Close a channel the broker opened after this client stopped waiting for it. Nobody
|
|
1693
|
+
* holds it, so left open it would be a channel on the broker with no owner.
|
|
1694
|
+
*
|
|
1695
|
+
* @param {Object} channel - the late amqplib channel
|
|
1696
|
+
* @param {string} what - which channel was being opened, for the log
|
|
1697
|
+
* @private
|
|
1698
|
+
*/
|
|
1699
|
+
_closeLateChannel(channel, what) {
|
|
1700
|
+
this._logger.warn(
|
|
1701
|
+
`[RabbitMQClient] The broker opened the ${what} after brokerAnswerTimeout (${this._brokerAnswerTimeout}ms) - `
|
|
1702
|
+
+ 'Expected: nothing to do; the open had already been reported as unanswered. '
|
|
1703
|
+
+ 'Fix: none — the late channel is closed now, so it is not left open with no owner.'
|
|
1704
|
+
);
|
|
1705
|
+
channel.close().catch((err) => {
|
|
1706
|
+
this._logger.debug(`[RabbitMQClient] Late ${what} did not close: ${err.message}`);
|
|
1707
|
+
});
|
|
1467
1708
|
}
|
|
1468
1709
|
|
|
1469
1710
|
/**
|
|
@@ -1698,7 +1939,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1698
1939
|
this._attachConnectionHandlers(this._connection);
|
|
1699
1940
|
|
|
1700
1941
|
// Use ConfirmChannel to enable publisher confirms for publish operations
|
|
1701
|
-
this._channel = await this.
|
|
1942
|
+
this._channel = await this._askBroker(
|
|
1943
|
+
{ operation: 'createConfirmChannel', target: 'publisher channel', channel: null },
|
|
1944
|
+
() => this._connection.createConfirmChannel()
|
|
1945
|
+
);
|
|
1702
1946
|
|
|
1703
1947
|
// Track channel metadata for debugging
|
|
1704
1948
|
this._channel._createdAt = new Date().toISOString();
|
|
@@ -1711,7 +1955,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1711
1955
|
|
|
1712
1956
|
// Create a separate regular channel for queue operations (assertQueue, checkQueue)
|
|
1713
1957
|
// This avoids RPC reply queue issues with ConfirmChannel.assertQueue()
|
|
1714
|
-
this._queueChannel = await this.
|
|
1958
|
+
this._queueChannel = await this._askBroker(
|
|
1959
|
+
{ operation: 'createChannel', target: 'queue channel', channel: null },
|
|
1960
|
+
() => this._connection.createChannel()
|
|
1961
|
+
);
|
|
1715
1962
|
|
|
1716
1963
|
// Track channel metadata for debugging
|
|
1717
1964
|
this._queueChannel._createdAt = new Date().toISOString();
|
|
@@ -1724,7 +1971,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1724
1971
|
|
|
1725
1972
|
// Create a dedicated channel for consume operations
|
|
1726
1973
|
// This prevents channel conflicts between publish (ConfirmChannel) and consume operations
|
|
1727
|
-
this._consumerChannel = await this.
|
|
1974
|
+
this._consumerChannel = await this._askBroker(
|
|
1975
|
+
{ operation: 'createChannel', target: 'consumer channel', channel: null },
|
|
1976
|
+
() => this._connection.createChannel()
|
|
1977
|
+
);
|
|
1728
1978
|
|
|
1729
1979
|
// Track channel metadata for debugging
|
|
1730
1980
|
this._consumerChannel._createdAt = new Date().toISOString();
|
|
@@ -1743,10 +1993,15 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1743
1993
|
// Cleanup partially created resources
|
|
1744
1994
|
this._connectionAlive = false;
|
|
1745
1995
|
if (this._connection) {
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1996
|
+
// Within the one budget (d.1135): a connect that failed because the broker stopped
|
|
1997
|
+
// answering (a channel open above) would otherwise wait for a `close-ok` from that
|
|
1998
|
+
// same silent broker. What the close answers is not the caller's business here —
|
|
1999
|
+
// the failure that brought us into this catch is — so only a close that got no
|
|
2000
|
+
// answer at all is acted on: its socket is dropped, as the teardown does.
|
|
2001
|
+
const halfOpen = this._connection;
|
|
2002
|
+
const closing = await this._closeWithinTeardownBudget('connection', halfOpen, this._brokerAnswerTimeout);
|
|
2003
|
+
if (closing.status === 'unanswered') {
|
|
2004
|
+
this._dropUnansweredConnection(halfOpen);
|
|
1750
2005
|
}
|
|
1751
2006
|
this._connection = null;
|
|
1752
2007
|
}
|
|
@@ -1818,10 +2073,17 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1818
2073
|
|
|
1819
2074
|
const timeout = this._setTimeout(() => {
|
|
1820
2075
|
done();
|
|
2076
|
+
// The sentence names the lever that is READ (d.1138): the configuration key when
|
|
2077
|
+
// the caller wrote it, the derivation in the constructor otherwise. It named an
|
|
2078
|
+
// environment variable until d.1138 that nothing reads.
|
|
2079
|
+
const budgetSource = this._config.reconnectWaitTimeout === undefined
|
|
2080
|
+
? 'not set, so derived as max(60000, maxReconnectAttempts × reconnectMaxDelay)'
|
|
2081
|
+
: 'set in the client configuration';
|
|
1821
2082
|
reject(new ConnectionError(
|
|
1822
2083
|
`[RabbitMQClient] Reconnection timeout after ${this._reconnectWaitTimeout}ms - `
|
|
1823
|
-
+
|
|
1824
|
-
+ 'Fix: check the broker is up and reachable; raise
|
|
2084
|
+
+ `Expected: the broker to accept a new connection within reconnectWaitTimeout (${this._reconnectWaitTimeout}ms, ${budgetSource}). `
|
|
2085
|
+
+ 'Fix: check the broker is up and reachable; raise reconnectWaitTimeout in the configuration handed to '
|
|
2086
|
+
+ 'the client only if a longer outage must be survived in-process.'
|
|
1825
2087
|
));
|
|
1826
2088
|
}, this._reconnectWaitTimeout);
|
|
1827
2089
|
|
|
@@ -1979,7 +2241,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1979
2241
|
|
|
1980
2242
|
try {
|
|
1981
2243
|
// Create new ConfirmChannel
|
|
1982
|
-
const channel = await this.
|
|
2244
|
+
const channel = await this._askBroker(
|
|
2245
|
+
{ operation: 'createConfirmChannel', target: `${role} channel`, channel: null },
|
|
2246
|
+
() => this._connection.createConfirmChannel()
|
|
2247
|
+
);
|
|
1983
2248
|
|
|
1984
2249
|
// Track channel metadata for debugging
|
|
1985
2250
|
channel._createdAt = new Date().toISOString();
|
|
@@ -2030,6 +2295,24 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2030
2295
|
err
|
|
2031
2296
|
);
|
|
2032
2297
|
}
|
|
2298
|
+
// Třetí nosič (d.1135): broker na otevření kanálu neodpověděl v `brokerAnswerTimeout`
|
|
2299
|
+
// (alarm paměti, zmrazený broker) — `_askBroker()` to hlásí `ConnectionError`, jinou
|
|
2300
|
+
// chybu z otevření kanálu tahle třída nenese. Je to týž stav „odpověď nepřišla“, ne
|
|
2301
|
+
// odmítnutí, takže přechodný jako oba nosiče výše. Stojí ZA nimi: spojení, které
|
|
2302
|
+
// odchází, má svou vlastní (přesnější) větu i tehdy, když amqplib odmítnutí složil
|
|
2303
|
+
// `_askBroker()` do `ConnectionError`.
|
|
2304
|
+
if (err instanceof ConnectionError) {
|
|
2305
|
+
this._logger.warn(
|
|
2306
|
+
`[RabbitMQClient] [mq-client-core] Failed to recreate ${role} channel (no answer from the broker): ${msg}`
|
|
2307
|
+
);
|
|
2308
|
+
throw new TransientPublishError(
|
|
2309
|
+
`[RabbitMQClient] ${role} channel cannot be recreated: the broker did not answer the open (transient condition) - `
|
|
2310
|
+
+ `Expected: an answer within brokerAnswerTimeout (${this._brokerAnswerTimeout}ms). `
|
|
2311
|
+
+ 'Fix: retry the publish; PublishLayer does this on its own and buffers the message meanwhile. '
|
|
2312
|
+
+ 'error.cause names the unanswered request.',
|
|
2313
|
+
err
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2033
2316
|
|
|
2034
2317
|
// Ostatní chyby považujeme za permanentní problém na úrovni kanálu/spojení.
|
|
2035
2318
|
throw new PermanentPublishError(
|
|
@@ -2062,7 +2345,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2062
2345
|
|
|
2063
2346
|
try {
|
|
2064
2347
|
// Create new regular channel
|
|
2065
|
-
this._queueChannel = await this.
|
|
2348
|
+
this._queueChannel = await this._askBroker(
|
|
2349
|
+
{ operation: 'createChannel', target: 'queue channel', channel: null },
|
|
2350
|
+
() => this._connection.createChannel()
|
|
2351
|
+
);
|
|
2066
2352
|
|
|
2067
2353
|
// Track channel metadata for debugging
|
|
2068
2354
|
this._queueChannel._createdAt = new Date().toISOString();
|
|
@@ -2137,7 +2423,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2137
2423
|
|
|
2138
2424
|
try {
|
|
2139
2425
|
// Create new regular channel
|
|
2140
|
-
this._consumerChannel = await this.
|
|
2426
|
+
this._consumerChannel = await this._askBroker(
|
|
2427
|
+
{ operation: 'createChannel', target: 'consumer channel', channel: null },
|
|
2428
|
+
() => this._connection.createChannel()
|
|
2429
|
+
);
|
|
2141
2430
|
|
|
2142
2431
|
// Track channel metadata for debugging
|
|
2143
2432
|
this._consumerChannel._createdAt = new Date().toISOString();
|
|
@@ -2212,6 +2501,33 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2212
2501
|
return { reRegistered, failed };
|
|
2213
2502
|
}
|
|
2214
2503
|
|
|
2504
|
+
/**
|
|
2505
|
+
* The consumer channel, if it is alive — otherwise a refusal that names why.
|
|
2506
|
+
*
|
|
2507
|
+
* The one question every step that is about to use the consumer channel asks
|
|
2508
|
+
* (`_isChannelAlive()`, the one liveness source, d.260). A refusal here is not a broker
|
|
2509
|
+
* verdict: the consumer stays tracked and the next recovery re-attaches it.
|
|
2510
|
+
*
|
|
2511
|
+
* @param {string} action - what was about to happen, as the refusal states it
|
|
2512
|
+
* @param {string} queue - the queue it is about
|
|
2513
|
+
* @returns {Object} the live consumer channel
|
|
2514
|
+
* @throws {ConnectionError} when the channel is dead or gone
|
|
2515
|
+
* @private
|
|
2516
|
+
*/
|
|
2517
|
+
_liveConsumerChannel(action, queue) {
|
|
2518
|
+
const channel = this._consumerChannel;
|
|
2519
|
+
if (!this._isChannelAlive(channel)) {
|
|
2520
|
+
throw new ConnectionError(
|
|
2521
|
+
`[RabbitMQClient] Cannot ${action} ${queue}: consumer channel is not available - `
|
|
2522
|
+
+ 'Expected: a live consumer channel for the whole re-registration. '
|
|
2523
|
+
+ 'Fix: nothing to do by hand — the channel died during the recovery (closed by the broker, or '
|
|
2524
|
+
+ 'discarded after brokerAnswerTimeout); the consumer stays tracked and the next channel recreate '
|
|
2525
|
+
+ 're-attaches it. Listen for consumer:re-registration:failed to see which ones wait.'
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
return channel;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2215
2531
|
/**
|
|
2216
2532
|
* Re-attach ONE tracked consumer on the current consumer channel.
|
|
2217
2533
|
*
|
|
@@ -2235,19 +2551,29 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2235
2551
|
*/
|
|
2236
2552
|
async _reRegisterConsumer(queue, consumerInfo) {
|
|
2237
2553
|
try {
|
|
2554
|
+
// Asked before EACH re-registration, not once per loop (d.1138): a channel that
|
|
2555
|
+
// died after the previous consumer — closed by the broker, or discarded by the
|
|
2556
|
+
// broker-answer budget (d.1135) — leaves the slot empty, and reaching into it was a
|
|
2557
|
+
// TypeError naming a JavaScript accident instead of the channel's state. Asked
|
|
2558
|
+
// before the queue step, so a consumer that cannot be attached declares nothing.
|
|
2559
|
+
this._liveConsumerChannel('re-register the consumer of', queue);
|
|
2560
|
+
|
|
2238
2561
|
await this._prepareQueueForConsume(queue, {
|
|
2239
2562
|
durable: consumerInfo.options.durable,
|
|
2240
2563
|
queueOptions: consumerInfo.options.queueOptions
|
|
2241
2564
|
});
|
|
2242
2565
|
|
|
2566
|
+
// …and again after it: the queue step awaits the broker on another channel,
|
|
2567
|
+
// and the consumer channel can die meanwhile.
|
|
2568
|
+
const consumerChannel = this._liveConsumerChannel('re-register the consumer of', queue);
|
|
2569
|
+
|
|
2243
2570
|
// The consumer's prefetch is re-applied here for the same reason the
|
|
2244
2571
|
// queue step is: the channel is new, and QoS lives on the channel.
|
|
2245
|
-
await this._applyConsumerPrefetch(queue, consumerInfo.options.prefetch);
|
|
2572
|
+
await this._applyConsumerPrefetch(consumerChannel, queue, consumerInfo.options.prefetch);
|
|
2246
2573
|
|
|
2247
|
-
const consumeResult = await this.
|
|
2248
|
-
queue,
|
|
2249
|
-
consumerInfo.handler,
|
|
2250
|
-
{ noAck: consumerInfo.options.noAck }
|
|
2574
|
+
const consumeResult = await this._askBroker(
|
|
2575
|
+
{ operation: 'consume', target: queue, channel: consumerChannel },
|
|
2576
|
+
() => consumerChannel.consume(queue, consumerInfo.handler, { noAck: consumerInfo.options.noAck })
|
|
2251
2577
|
);
|
|
2252
2578
|
consumerInfo.consumerTag = consumeResult.consumerTag;
|
|
2253
2579
|
this._logger.info(`[RabbitMQClient] [mq-client-core] ✓ Re-registered consumer for queue: ${queue} (consumerTag: ${consumeResult.consumerTag})`);
|
|
@@ -2500,7 +2826,11 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2500
2826
|
try {
|
|
2501
2827
|
// Check if queue exists
|
|
2502
2828
|
this._trackChannelOperation(this._queueChannel, `checkQueue ${queue}`);
|
|
2503
|
-
const
|
|
2829
|
+
const queueChannel = this._queueChannel;
|
|
2830
|
+
const queueInfo = await this._askBroker(
|
|
2831
|
+
{ operation: 'checkQueue', target: queue, channel: queueChannel },
|
|
2832
|
+
() => queueChannel.checkQueue(queue)
|
|
2833
|
+
);
|
|
2504
2834
|
if (queueInfo) {
|
|
2505
2835
|
// Queue exists - is THIS client's consumer attached to it?
|
|
2506
2836
|
//
|
|
@@ -2854,7 +3184,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2854
3184
|
const channel = await this._requireQueueChannel('assertQueue', queue);
|
|
2855
3185
|
|
|
2856
3186
|
try {
|
|
2857
|
-
return await
|
|
3187
|
+
return await this._askBroker(
|
|
3188
|
+
{ operation: 'assertQueue', target: queue, channel },
|
|
3189
|
+
() => channel.assertQueue(queue, queueOptions)
|
|
3190
|
+
);
|
|
2858
3191
|
} catch (err) {
|
|
2859
3192
|
// The SAME verdict the consume rail states, through the SAME function
|
|
2860
3193
|
// (d.582). This is the rail FÁZE 0.8 walks: a service declaring its own
|
|
@@ -2984,10 +3317,34 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2984
3317
|
* @private
|
|
2985
3318
|
*/
|
|
2986
3319
|
_publishPathTransient(verb, queue, cause) {
|
|
3320
|
+
// The sentence is chosen from this client's STATE, never from the cause's text (d.1145,
|
|
3321
|
+
// finding of d.1143): since d.1135 this refusal also ends a pre-check the broker never
|
|
3322
|
+
// answered, and on the platform's image the connection was then alive and BLOCKED by a
|
|
3323
|
+
// memory alarm — "connection may be closed" sent the reader to a reconnect that was not
|
|
3324
|
+
// coming. `sent` is false on every one of these (no frame was written), which is what the
|
|
3325
|
+
// flush after `unblocked` reads.
|
|
3326
|
+
let state;
|
|
3327
|
+
let fix;
|
|
3328
|
+
if (!this._connectionAlive) {
|
|
3329
|
+
state = 'the connection is closed';
|
|
3330
|
+
fix = 'Fix: nothing to do here — a reconnect restores it; PublishLayer retries and buffers the message meanwhile, '
|
|
3331
|
+
+ 'and the buffer re-sends it after the reconnect.';
|
|
3332
|
+
} else if (this._connectionBlockedBy !== null) {
|
|
3333
|
+
state = `the broker has blocked this connection (reason: ${this._connectionBlockedBy}) and answers nothing until it clears the alarm`;
|
|
3334
|
+
fix = 'Fix: clear the broker alarm (management API /api/health/checks/alarms names it); PublishLayer retries and '
|
|
3335
|
+
+ 'buffers the message meanwhile, and the buffer re-sends it once the broker unblocks the connection (its frame never left).';
|
|
3336
|
+
} else {
|
|
3337
|
+
// Neither closed nor blocked: the cause says what happened (a request unanswered
|
|
3338
|
+
// within brokerAnswerTimeout, or a channel open the broker refused) — this sentence
|
|
3339
|
+
// does not guess which.
|
|
3340
|
+
state = 'the queue channel is not available on a live, unblocked connection (error.cause names the request '
|
|
3341
|
+
+ 'that went unanswered or was refused)';
|
|
3342
|
+
fix = 'Fix: read error.cause; an unanswered request points at a paused or overloaded broker. PublishLayer retries '
|
|
3343
|
+
+ 'and buffers the message meanwhile.';
|
|
3344
|
+
}
|
|
2987
3345
|
return new TransientPublishError(
|
|
2988
|
-
`[RabbitMQClient] Cannot ${verb} ${queue}:
|
|
2989
|
-
+ `Expected: an open queue channel before ${verb} on the publish path.
|
|
2990
|
-
+ 'Fix: retry the publish after the reconnect; PublishLayer does this on its own and buffers the message meanwhile.',
|
|
3346
|
+
`[RabbitMQClient] Cannot ${verb} ${queue}: ${state} - `
|
|
3347
|
+
+ `Expected: an answering broker on an open queue channel before ${verb} on the publish path. ${fix}`,
|
|
2991
3348
|
cause
|
|
2992
3349
|
);
|
|
2993
3350
|
}
|
|
@@ -3354,7 +3711,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3354
3711
|
...options,
|
|
3355
3712
|
durable: options.durable !== undefined ? options.durable : this._config.durable
|
|
3356
3713
|
};
|
|
3357
|
-
return await
|
|
3714
|
+
return await this._askBroker(
|
|
3715
|
+
{ operation: 'assertExchange', target: exchange, channel },
|
|
3716
|
+
() => channel.assertExchange(exchange, type, exchangeOptions)
|
|
3717
|
+
);
|
|
3358
3718
|
}
|
|
3359
3719
|
|
|
3360
3720
|
/**
|
|
@@ -3398,7 +3758,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3398
3758
|
}
|
|
3399
3759
|
|
|
3400
3760
|
const channel = await this._requireQueueChannel('bindQueue', `${queue} -> ${exchange}`);
|
|
3401
|
-
return await
|
|
3761
|
+
return await this._askBroker(
|
|
3762
|
+
{ operation: 'bindQueue', target: `${queue} -> ${exchange}`, channel },
|
|
3763
|
+
() => channel.bindQueue(queue, exchange, pattern)
|
|
3764
|
+
);
|
|
3402
3765
|
}
|
|
3403
3766
|
|
|
3404
3767
|
/**
|
|
@@ -3481,7 +3844,11 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3481
3844
|
}
|
|
3482
3845
|
|
|
3483
3846
|
this._trackChannelOperation(this._consumerChannel, `cancelConsumer ${queue}`);
|
|
3484
|
-
const
|
|
3847
|
+
const consumerChannel = this._consumerChannel;
|
|
3848
|
+
const result = await this._askBroker(
|
|
3849
|
+
{ operation: 'cancel', target: queue, channel: consumerChannel },
|
|
3850
|
+
() => consumerChannel.cancel(info.consumerTag)
|
|
3851
|
+
);
|
|
3485
3852
|
// Dropped only after the broker accepted the cancel: a consumer that is
|
|
3486
3853
|
// still delivering must stay on the list recovery re-attaches.
|
|
3487
3854
|
this._activeConsumers.delete(queue);
|
|
@@ -3498,7 +3865,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3498
3865
|
*/
|
|
3499
3866
|
async checkQueue(queue) {
|
|
3500
3867
|
const channel = await this._requireQueueChannel('checkQueue', queue);
|
|
3501
|
-
return await
|
|
3868
|
+
return await this._askBroker(
|
|
3869
|
+
{ operation: 'checkQueue', target: queue, channel },
|
|
3870
|
+
() => channel.checkQueue(queue)
|
|
3871
|
+
);
|
|
3502
3872
|
}
|
|
3503
3873
|
|
|
3504
3874
|
/**
|
|
@@ -3507,7 +3877,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3507
3877
|
*/
|
|
3508
3878
|
async purgeQueue(queue) {
|
|
3509
3879
|
const channel = await this._requireQueueChannel('purgeQueue', queue);
|
|
3510
|
-
return await
|
|
3880
|
+
return await this._askBroker(
|
|
3881
|
+
{ operation: 'purgeQueue', target: queue, channel },
|
|
3882
|
+
() => channel.purgeQueue(queue)
|
|
3883
|
+
);
|
|
3511
3884
|
}
|
|
3512
3885
|
|
|
3513
3886
|
/**
|
|
@@ -3517,7 +3890,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3517
3890
|
*/
|
|
3518
3891
|
async deleteQueue(queue, options = {}) {
|
|
3519
3892
|
const channel = await this._requireQueueChannel('deleteQueue', queue);
|
|
3520
|
-
return await
|
|
3893
|
+
return await this._askBroker(
|
|
3894
|
+
{ operation: 'deleteQueue', target: queue, channel },
|
|
3895
|
+
() => channel.deleteQueue(queue, options)
|
|
3896
|
+
);
|
|
3521
3897
|
}
|
|
3522
3898
|
|
|
3523
3899
|
/**
|
|
@@ -3536,10 +3912,50 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3536
3912
|
}
|
|
3537
3913
|
|
|
3538
3914
|
/**
|
|
3539
|
-
*
|
|
3915
|
+
* One publish attempt, without retry — and, when it fails, whether its frame LEFT THE
|
|
3916
|
+
* CLIENT (d.1145, conf `mq-publish-buffer` 001).
|
|
3917
|
+
*
|
|
3918
|
+
* Every error this attempt throws carries `sent` (boolean): `true` once `sendToQueue()` /
|
|
3919
|
+
* `publish()` on the channel returned without throwing — the frame was written to the
|
|
3920
|
+
* channel — and `false` for everything that failed before that (the pre-check, the channel
|
|
3921
|
+
* ensure, a throwing send). It says nothing about whether the broker stored the frame; it is
|
|
3922
|
+
* what tells a message the broker may still deliver (held in a blocked socket, measured
|
|
3923
|
+
* d.1143) from one it never received. `PublishLayer` folds the attempts of one call into the
|
|
3924
|
+
* fact it writes on its own error and on the buffer entry.
|
|
3925
|
+
*
|
|
3926
|
+
* A nested republish from the confirm path (see `_publishOneAttempt`) may throw an error
|
|
3927
|
+
* that already says `sent: true`; that fact is kept, never lowered.
|
|
3928
|
+
*
|
|
3929
|
+
* @param {string} queue
|
|
3930
|
+
* @param {Buffer} buffer
|
|
3931
|
+
* @param {Object} [options]
|
|
3932
|
+
* @returns {Promise<void>}
|
|
3933
|
+
* @throws {Error} the attempt's failure, with `sent` written on it
|
|
3540
3934
|
* @private
|
|
3541
3935
|
*/
|
|
3542
3936
|
async _publishOnce(queue, buffer, options = {}) {
|
|
3937
|
+
const attempt = { sent: false };
|
|
3938
|
+
try {
|
|
3939
|
+
return await this._publishOneAttempt(queue, buffer, options, attempt);
|
|
3940
|
+
} catch (err) {
|
|
3941
|
+
if (err !== null && typeof err === 'object') {
|
|
3942
|
+
err.sent = attempt.sent || err.sent === true;
|
|
3943
|
+
}
|
|
3944
|
+
throw err;
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
/**
|
|
3949
|
+
* The body of one publish attempt. `attempt.sent` turns `true` the moment the frame is
|
|
3950
|
+
* written to the channel; `_publishOnce()` reads it.
|
|
3951
|
+
*
|
|
3952
|
+
* @param {string} queue
|
|
3953
|
+
* @param {Buffer} buffer
|
|
3954
|
+
* @param {Object} options
|
|
3955
|
+
* @param {{sent: boolean}} attempt - written here, read by `_publishOnce()`
|
|
3956
|
+
* @private
|
|
3957
|
+
*/
|
|
3958
|
+
async _publishOneAttempt(queue, buffer, options, attempt) {
|
|
3543
3959
|
// Whose channel this send goes out on (d.724). Read once, here: the role cannot
|
|
3544
3960
|
// change inside one attempt, and the channel it names can — so everything below asks
|
|
3545
3961
|
// `_sendChannelOf(sendRole)` rather than holding a member.
|
|
@@ -3620,7 +4036,11 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3620
4036
|
}
|
|
3621
4037
|
throw railErr;
|
|
3622
4038
|
}
|
|
3623
|
-
|
|
4039
|
+
const checkedOn = queueChannel;
|
|
4040
|
+
await this._askBroker(
|
|
4041
|
+
{ operation: 'checkQueue', target: queue, channel: checkedOn },
|
|
4042
|
+
() => checkedOn.checkQueue(queue)
|
|
4043
|
+
);
|
|
3624
4044
|
// Queue exists - proceed to publish
|
|
3625
4045
|
} catch (checkErr) {
|
|
3626
4046
|
// If queue doesn't exist (404), this je ERROR – ale rozlišujeme infra vs. non-infra:
|
|
@@ -3714,6 +4134,12 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3714
4134
|
// - not already typed — `_publishPathTransient()` above throws into
|
|
3715
4135
|
// this same `catch`, and wrapping a verdict in itself would bury the
|
|
3716
4136
|
// rail's own sentence one level deeper for no reader.
|
|
4137
|
+
//
|
|
4138
|
+
// Since d.1135 the check goes through `_askBroker()`, so what arrives here
|
|
4139
|
+
// is its `ConnectionError`: either the budget ran out (the channel is then
|
|
4140
|
+
// discarded — dead) or amqplib's bare rejection, folded where it arrived. Both
|
|
4141
|
+
// meet the three conditions unchanged; the bare error sits one level down, in
|
|
4142
|
+
// `cause.cause`.
|
|
3717
4143
|
throw this._publishPathTransient('checkQueue', queue, checkErr);
|
|
3718
4144
|
} else {
|
|
3719
4145
|
// Other error - rethrow
|
|
@@ -3844,12 +4270,20 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3844
4270
|
return this.publish(queue, buffer, options);
|
|
3845
4271
|
}).then(resolve).catch(reject);
|
|
3846
4272
|
} else {
|
|
3847
|
-
//
|
|
3848
|
-
//
|
|
4273
|
+
// No reconnect is in flight YET — which says nothing about whether one
|
|
4274
|
+
// follows. When the connection dies, amqplib closes its channels before
|
|
4275
|
+
// it announces the connection's own `close` (`lib/connection.js`,
|
|
4276
|
+
// `toClosed()`), and that announcement is what starts the reconnect, so
|
|
4277
|
+
// this callback runs before it (d.1134). The sentence therefore states
|
|
4278
|
+
// what is known here — the channel closed with the confirm outstanding —
|
|
4279
|
+
// and the classification makes it transient, so the publish path
|
|
4280
|
+
// retries and buffers it.
|
|
3849
4281
|
reject(new PublishError(
|
|
3850
|
-
`[RabbitMQClient] Channel closed during publish to queue "${queue}"
|
|
3851
|
-
+ '
|
|
3852
|
-
+ '
|
|
4282
|
+
`[RabbitMQClient] Channel closed during publish to queue "${queue}" before the broker confirmed the message - `
|
|
4283
|
+
+ 'message delivery not guaranteed. '
|
|
4284
|
+
+ 'Expected: the channel to stay open until the broker confirms. '
|
|
4285
|
+
+ 'Fix: republish the message — the confirm never arrived, so it may or may not have been stored; '
|
|
4286
|
+
+ 'error.buffered says whether the buffer holds it for the next reconnect.',
|
|
3853
4287
|
queue,
|
|
3854
4288
|
null
|
|
3855
4289
|
));
|
|
@@ -3890,6 +4324,8 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3890
4324
|
resolve();
|
|
3891
4325
|
}
|
|
3892
4326
|
});
|
|
4327
|
+
// The frame is written to the channel: `sendToQueue()` returned without throwing.
|
|
4328
|
+
attempt.sent = true;
|
|
3893
4329
|
|
|
3894
4330
|
// Set a safety timeout - if callback wasn't invoked and channel closed, retry.
|
|
3895
4331
|
// A broker confirm can land inside `sendToQueue()` itself, so the
|
|
@@ -4077,6 +4513,8 @@ class RabbitMQClient extends EventEmitter {
|
|
|
4077
4513
|
resolve();
|
|
4078
4514
|
}
|
|
4079
4515
|
});
|
|
4516
|
+
// The frame is written to the channel: `publish()` returned without throwing.
|
|
4517
|
+
attempt.sent = true;
|
|
4080
4518
|
});
|
|
4081
4519
|
|
|
4082
4520
|
await confirmPromise;
|
|
@@ -4118,17 +4556,22 @@ class RabbitMQClient extends EventEmitter {
|
|
|
4118
4556
|
* A non-numeric prefetch (the caller asked for none) sets none, and records
|
|
4119
4557
|
* none: there is nothing to measure utilisation against.
|
|
4120
4558
|
*
|
|
4559
|
+
* @param {Object} consumerChannel - The consumer channel the caller holds; the
|
|
4560
|
+
* re-registration hands over the one it has just asked is alive (d.1138)
|
|
4121
4561
|
* @param {string} queue - Queue name
|
|
4122
4562
|
* @param {number|null|undefined} prefetch - The consumer's prefetch
|
|
4123
4563
|
* @returns {Promise<void>}
|
|
4124
4564
|
* @private
|
|
4125
4565
|
*/
|
|
4126
|
-
async _applyConsumerPrefetch(queue, prefetch) {
|
|
4566
|
+
async _applyConsumerPrefetch(consumerChannel, queue, prefetch) {
|
|
4127
4567
|
if (typeof prefetch !== 'number') {
|
|
4128
4568
|
return;
|
|
4129
4569
|
}
|
|
4130
4570
|
|
|
4131
|
-
await this.
|
|
4571
|
+
await this._askBroker(
|
|
4572
|
+
{ operation: 'prefetch', target: queue, channel: consumerChannel },
|
|
4573
|
+
() => consumerChannel.prefetch(prefetch)
|
|
4574
|
+
);
|
|
4132
4575
|
|
|
4133
4576
|
const tracking = this._prefetchTracking.get(queue);
|
|
4134
4577
|
if (tracking) {
|
|
@@ -4326,7 +4769,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
4326
4769
|
// If missing, fail-fast. The owning infra service must recreate on startup.
|
|
4327
4770
|
const queueChannel = await this._requireQueueChannel('checkQueue', queue);
|
|
4328
4771
|
try {
|
|
4329
|
-
await
|
|
4772
|
+
await this._askBroker(
|
|
4773
|
+
{ operation: 'checkQueue', target: queue, channel: queueChannel },
|
|
4774
|
+
() => queueChannel.checkQueue(queue)
|
|
4775
|
+
);
|
|
4330
4776
|
this._logger.debug(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Infrastructure queue ${queue} exists (consumer will proceed)`);
|
|
4331
4777
|
} catch (checkErr) {
|
|
4332
4778
|
if (checkErr.code === AMQP_NOT_FOUND) {
|
|
@@ -4451,7 +4897,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
4451
4897
|
// Set prefetch if provided (on consumer channel). ONE rail, shared with
|
|
4452
4898
|
// `_reRegisterConsumers()` — prefetch belongs to the consumer, not to the
|
|
4453
4899
|
// channel that happened to be open when it first attached.
|
|
4454
|
-
await this._applyConsumerPrefetch(queue, prefetch);
|
|
4900
|
+
await this._applyConsumerPrefetch(this._consumerChannel, queue, prefetch);
|
|
4455
4901
|
|
|
4456
4902
|
// CRITICAL: Log before calling amqplib's consume()
|
|
4457
4903
|
// amqplib's consume() may internally call assertQueue() without parameters if queue doesn't exist
|
|
@@ -4666,10 +5112,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
4666
5112
|
}
|
|
4667
5113
|
};
|
|
4668
5114
|
|
|
4669
|
-
const
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
{ noAck }
|
|
5115
|
+
const consumerChannel = this._consumerChannel;
|
|
5116
|
+
const consumeResult = await this._askBroker(
|
|
5117
|
+
{ operation: 'consume', target: queue, channel: consumerChannel },
|
|
5118
|
+
() => consumerChannel.consume(queue, messageHandler, { noAck })
|
|
4673
5119
|
);
|
|
4674
5120
|
|
|
4675
5121
|
// Track consumer for auto re-registration if channel closes
|
|
@@ -4778,13 +5224,23 @@ class RabbitMQClient extends EventEmitter {
|
|
|
4778
5224
|
this._logger.info(
|
|
4779
5225
|
'[RabbitMQClient] Client was closed by its caller during recovery - closing the re-established connection'
|
|
4780
5226
|
);
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
5227
|
+
// Within the one budget (d.1138, conf `mq-broker-answer-budget` 001): this close
|
|
5228
|
+
// waits for a `close-ok` like every teardown close, and a broker that stopped
|
|
5229
|
+
// answering right now would otherwise hold this round — and `_reconnecting` —
|
|
5230
|
+
// open forever. Unanswered, the socket is dropped the way the teardown drops it.
|
|
5231
|
+
const closing = await this._closeWithinTeardownBudget(
|
|
5232
|
+
'connection re-established after disconnect',
|
|
5233
|
+
revived,
|
|
5234
|
+
this._brokerAnswerTimeout
|
|
5235
|
+
);
|
|
5236
|
+
if (closing.status === 'refused') {
|
|
4784
5237
|
this._logger.warn(
|
|
4785
|
-
`[RabbitMQClient] Error closing the connection re-established after disconnect: ${
|
|
5238
|
+
`[RabbitMQClient] Error closing the connection re-established after disconnect: ${closing.error.message}`
|
|
4786
5239
|
);
|
|
4787
5240
|
}
|
|
5241
|
+
if (closing.status === 'unanswered') {
|
|
5242
|
+
this._dropUnansweredConnection(revived);
|
|
5243
|
+
}
|
|
4788
5244
|
this._connection = null;
|
|
4789
5245
|
this._connectionAlive = false;
|
|
4790
5246
|
this._reconnecting = false;
|
|
@@ -4831,7 +5287,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
4831
5287
|
|
|
4832
5288
|
// Recreate consumer channel and re-register all consumers
|
|
4833
5289
|
try {
|
|
4834
|
-
this._consumerChannel = await this.
|
|
5290
|
+
this._consumerChannel = await this._askBroker(
|
|
5291
|
+
{ operation: 'createChannel', target: 'consumer channel', channel: null },
|
|
5292
|
+
() => this._connection.createChannel()
|
|
5293
|
+
);
|
|
4835
5294
|
this._consumerChannel._createdAt = new Date().toISOString();
|
|
4836
5295
|
this._consumerChannel._closeReason = null;
|
|
4837
5296
|
this._consumerChannel._lastOperation = 'Recreated via _reconnectWithBackoff()';
|
|
@@ -4882,7 +5341,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
4882
5341
|
// Flush buffered messages po reconnectu
|
|
4883
5342
|
let bufferFlushResult = { inMemory: 0, flushed: false };
|
|
4884
5343
|
try {
|
|
4885
|
-
const flushResult = await this._publishLayer.flushBuffered();
|
|
5344
|
+
const flushResult = await this._publishLayer.flushBuffered('reconnected');
|
|
4886
5345
|
bufferFlushResult = { inMemory: flushResult, flushed: true };
|
|
4887
5346
|
this._publishMonitor.trackFlushed(bufferFlushResult.inMemory);
|
|
4888
5347
|
this._logger.info(`[RabbitMQClient] ✓ Flushed ${bufferFlushResult.inMemory} buffered messages after reconnection`);
|
|
@@ -5033,7 +5492,19 @@ class RabbitMQClient extends EventEmitter {
|
|
|
5033
5492
|
this.emit('error', err);
|
|
5034
5493
|
});
|
|
5035
5494
|
|
|
5036
|
-
|
|
5495
|
+
// PREPENDED, not appended (d.1134). amqplib registers its own `close` listener in the
|
|
5496
|
+
// channel's constructor — the one that answers every unconfirmed publish with the bare
|
|
5497
|
+
// `new Error('channel closed')` (`lib/channel.js`, the `Channel` constructor) — and a
|
|
5498
|
+
// constructor runs before anybody can attach anything, so an appended handler always
|
|
5499
|
+
// ran SECOND. The publish callbacks then read a channel that was still alive, took the
|
|
5500
|
+
// branch meant for a broker answer on a live channel, and the sentinel reached the
|
|
5501
|
+
// classification untyped: permanent, not buffered, not retried — for a message the
|
|
5502
|
+
// broker said nothing about (měřeno d.1126, `320 CONNECTION_FORCED` with a confirm in
|
|
5503
|
+
// flight). `connection.toClosed()` closes every channel before the connection's own
|
|
5504
|
+
// events (`lib/connection.js`), so this handler is the only one that can mark the
|
|
5505
|
+
// channel dead in time. Prepending makes it run before amqplib's own listener — an
|
|
5506
|
+
// order the emitter guarantees, with no timer and no race.
|
|
5507
|
+
channel.prependListener('close', async () => {
|
|
5037
5508
|
if (this._closedByCaller) {
|
|
5038
5509
|
return;
|
|
5039
5510
|
}
|