@onlineapps/mq-client-core 4.0.0 → 4.0.1
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 +70 -0
- package/README.md +37 -1
- package/package.json +1 -1
- package/src/BaseClient.js +7 -2
- package/src/transports/rabbitmqClient.js +207 -46
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,76 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [4.0.1] — 2026-09-17
|
|
8
|
+
|
|
9
|
+
### Fixed — zrušení konzumenta brokerem přestalo být tiché (d.605)
|
|
10
|
+
|
|
11
|
+
`basic.cancel` od brokera — smazaná fronta, přesun fronty při HA failoveru
|
|
12
|
+
(`x-cancel-on-ha-failover`) — doručí amqplib do callbacku `consume()` jako
|
|
13
|
+
zprávu `null`. Klient na ni odpovídal holým `return`: žádný záznam, žádná změna
|
|
14
|
+
stavu, žádné znovupřipojení. Značka konzumenta zůstala nastavená, a protože
|
|
15
|
+
právě ona je jediný fakt, kterým `performHealthCheck()` odpovídá „je konzument
|
|
16
|
+
TOHOTO klienta připojený", hlásil klient `healthy: true` o frontě, ze které už
|
|
17
|
+
nic nečetl. Ticho je vada stejné závažnosti jako špatný výsledek
|
|
18
|
+
(`automation-gates.md` §5).
|
|
19
|
+
|
|
20
|
+
Nově v tomto pořadí: značka se zahodí (týž fakt, který d.386b zavedl pro
|
|
21
|
+
odmítnutou re-registraci — od té chvíle je klient `unhealthy` a jmenuje frontu),
|
|
22
|
+
ztráta se zapíše na úrovni **error** se jménem fronty i zrušené značky, a
|
|
23
|
+
konzument se znovu připojí **kolejí, kterou už používá obnova kanálu**: tělo
|
|
24
|
+
smyčky `_reRegisterConsumers()` dostalo jméno (`_reRegisterConsumer()`) a cancel
|
|
25
|
+
volá tutéž funkci pro jednu frontu. Celá smyčka by znovu připojila i konzumenty,
|
|
26
|
+
kteří o své místo nepřišli.
|
|
27
|
+
|
|
28
|
+
Klasifikaci, deklaraci a prefetch tedy dělá `_prepareQueueForConsume()` —
|
|
29
|
+
vlastněná business fronta se znovu deklaruje, nevlastněná infrastrukturní se jen
|
|
30
|
+
`checkQueue` a její absence je odmítnutí se značkou `CONSUMER_QUEUE_MISSING`,
|
|
31
|
+
nikdy tiché založení. Odmítnuté znovupřipojení nechá konzumenta sledovaného bez
|
|
32
|
+
značky a důvod jede na události, kterou balíček už má —
|
|
33
|
+
`consumer:re-registration:failed`.
|
|
34
|
+
|
|
35
|
+
**Žádný nový veřejný kontrakt**: žádná nová metoda, žádná nová událost, žádná
|
|
36
|
+
změna podpisu. Každý stav, do kterého se cesta může dostat, už balíček hlásí.
|
|
37
|
+
|
|
38
|
+
Ticho zůstává tam, kde zrušení není vada: vlastní `cancelConsumer()` callback
|
|
39
|
+
vůbec nevolá (amqplib odregistruje konzumenta na `basic.cancel-ok`; broker
|
|
40
|
+
neposílá cancel), a cancel, který dorazí během `disconnect()`, je zapsán na
|
|
41
|
+
úrovni debug — úmyslné zavření není porucha.
|
|
42
|
+
|
|
43
|
+
Kdo z toho co má: každá služba, která konzumuje frontu, se po re-pinu na tuto
|
|
44
|
+
verzi dozví, že o konzumenta přišla — dnes to nepozná ani z logu, ani ze
|
|
45
|
+
zdravotní kontroly.
|
|
46
|
+
|
|
47
|
+
### Fixed — „připojený konzument" má jednu definici, a čte ji i levný reportér (d.613)
|
|
48
|
+
|
|
49
|
+
`getConsumerState()` vracelo jako `active` klíče `_activeConsumers`, tedy
|
|
50
|
+
konzumenty, které klient **sleduje**. `_performHealthCheck()` odpovídá na tutéž
|
|
51
|
+
otázku podle značky (`consumerInfo.consumerTag`), kterou d.386b a d.605 nechávají
|
|
52
|
+
nastavenou jen po dobu skutečného připojení. Jeden soubor, dvě definice slova
|
|
53
|
+
„aktivní" — a rozcházely se přesně tam, kde na tom záleží: konzument, jemuž
|
|
54
|
+
někdo smazal frontu a jehož znovupřipojení broker odmítl (406), zůstává
|
|
55
|
+
sledovaný bez značky, takže zdravotní kontrola hlásila 0 připojených, kdežto
|
|
56
|
+
`getConsumerState()` frontu dál jmenovalo jako aktivní.
|
|
57
|
+
|
|
58
|
+
To není kosmetika. `getConsumerState()` je ta **levná synchronní** odpověď, na
|
|
59
|
+
kterou se čtenář dostane: `docs/standards/rabbitmq-channel-best-practices.md`
|
|
60
|
+
§ „Consumers stop receiving after a reconnect" i
|
|
61
|
+
`docs/operations/rabbitmq-connection-recovery-troubleshooting.md` posílají
|
|
62
|
+
operátora právě sem pro tento případ — a dostal odpověď, že konzument je aktivní.
|
|
63
|
+
|
|
64
|
+
Nově obě čtení volají jednu privátní funkci `_isConsumerAttached(consumerInfo)`;
|
|
65
|
+
`active` jmenuje **připojené** fronty a `tracked` zůstává počtem sledovaných,
|
|
66
|
+
takže `tracked > active.length` je celá věta „tento klient z některé fronty nic
|
|
67
|
+
nebere". Tvar návratové hodnoty se nemění (žádný klíč nepřibyl ani nezmizel),
|
|
68
|
+
mění se pravdivost hodnoty `active`; mimo tento balíček ji dnes nečte žádný kód
|
|
69
|
+
(změřeno přes `api`, `api_biz/*`, `fe_adminui`), takže se nic dalšího
|
|
70
|
+
nepřepisuje.
|
|
71
|
+
|
|
72
|
+
Kdo z toho co má: `@onlineapps/conn-infra-mq` to po re-pinu pošle dál ve
|
|
73
|
+
`getHealth().consumers` a `@onlineapps/service-wrapper` z toho odvodí
|
|
74
|
+
`components.mq` — služba se ztraceným konzumentem přestane být v heartbeatu
|
|
75
|
+
`healthy`.
|
|
76
|
+
|
|
7
77
|
## [4.0.0] — 2026-09-16
|
|
8
78
|
|
|
9
79
|
### Changed — značku odmítnutí nese i deklarační rail, jednou funkcí pro obě cesty (d.582)
|
package/README.md
CHANGED
|
@@ -187,7 +187,10 @@ Owner decision: `api/docs/governance/confirmations/connector-logger-contract.md`
|
|
|
187
187
|
three channels, the consumers this client holds, and what is waiting in the
|
|
188
188
|
publish buffer (`{ size, inMemory }` — there is one buffer). All five refuse
|
|
189
189
|
before `connect()` has built a transport, rather than answering with an invented
|
|
190
|
-
zero.
|
|
190
|
+
zero. `getConsumerState()` answers `{ tracked, active }`: `tracked` counts the
|
|
191
|
+
consumers this client would restore, `active` names the ones ATTACHED right now
|
|
192
|
+
— the same question the health check asks — so `tracked > active.length` says
|
|
193
|
+
this client takes no message off some queue it registered.
|
|
191
194
|
- `disconnectAll()` (static, on the class and on the package export) - Close every
|
|
192
195
|
client this module holds; see § Shutdown.
|
|
193
196
|
- `onError(callback)` - Register error handler
|
|
@@ -328,6 +331,39 @@ There is no third meaning. `requeueOnError: false` together with `maxAttempts`
|
|
|
328
331
|
above 1 is a contradiction and is **refused where it was written**, rather than
|
|
329
332
|
one of the two being silently ignored.
|
|
330
333
|
|
|
334
|
+
#### A cancel from the BROKER — the consumer that was stopped without asking
|
|
335
|
+
|
|
336
|
+
`basic.cancel` sent by the broker is how a consumer learns its attachment is gone
|
|
337
|
+
while the channel and the connection stay up: the queue was deleted, or the broker
|
|
338
|
+
moved it (`x-cancel-on-ha-failover`). amqplib delivers it to the consume callback
|
|
339
|
+
as a `null` message, and it reaches nothing else in this client — the channel
|
|
340
|
+
handlers listen for `error` and `close`, and both stay silent.
|
|
341
|
+
|
|
342
|
+
It is **not** a silent event. In this order:
|
|
343
|
+
|
|
344
|
+
- the consumer's tag is dropped, because it names an attachment the broker no
|
|
345
|
+
longer holds — so `performHealthCheck()` stops counting that consumer as
|
|
346
|
+
attached the moment the cancel arrives, rather than one recovery later;
|
|
347
|
+
- the loss is logged at **error** level, naming the queue and the tag the broker
|
|
348
|
+
cancelled;
|
|
349
|
+
- the consumer is re-attached on the rail a channel recreate uses, for that queue
|
|
350
|
+
alone: the queue is classified, declared or checked and the prefetch re-applied
|
|
351
|
+
exactly as `consume()` does it. So a business queue the service owns is
|
|
352
|
+
re-declared, and an infrastructure queue this client does not own is only
|
|
353
|
+
checked — a deleted one is refused with `CONSUMER_QUEUE_MISSING`, never
|
|
354
|
+
silently recreated.
|
|
355
|
+
|
|
356
|
+
If that re-attachment is refused, the consumer stays **tracked with no tag** — the
|
|
357
|
+
same state a refused re-registration leaves — so the client reports itself
|
|
358
|
+
unhealthy and names the queue, `consumer:re-registration:failed` carries the
|
|
359
|
+
broker's reason and code, and the next channel recreate tries again.
|
|
360
|
+
|
|
361
|
+
Two cancels that are NOT this: `cancelConsumer()` on this client (the broker
|
|
362
|
+
answers `basic.cancel-ok`, the callback is never invoked, and the consumer is
|
|
363
|
+
dropped from the registry on purpose), and a cancel arriving while
|
|
364
|
+
`disconnect()` is running — an intentional close is not a fault, and it is
|
|
365
|
+
recorded at debug level.
|
|
366
|
+
|
|
331
367
|
### Connection loss — the contract
|
|
332
368
|
|
|
333
369
|
When the broker goes away, the transport runs connection-level recovery with
|
package/package.json
CHANGED
package/src/BaseClient.js
CHANGED
|
@@ -1125,9 +1125,14 @@ class BaseClient {
|
|
|
1125
1125
|
}
|
|
1126
1126
|
|
|
1127
1127
|
/**
|
|
1128
|
-
* The consumers this client currently holds: how many, and
|
|
1128
|
+
* The consumers this client currently holds: how many it TRACKS, and which of
|
|
1129
|
+
* them are ATTACHED right now. A consumer whose queue was deleted, or whose
|
|
1130
|
+
* re-registration the broker refused, stays tracked without its tag — so
|
|
1131
|
+
* `tracked > active.length` says this client takes no message off some queue
|
|
1132
|
+
* it registered (d.613; one definition of "attached",
|
|
1133
|
+
* `RabbitMQClient._isConsumerAttached()`).
|
|
1129
1134
|
*
|
|
1130
|
-
* @returns {Object} `{ tracked: <count>, active: [<queue names>] }`
|
|
1135
|
+
* @returns {Object} `{ tracked: <count>, active: [<attached queue names>] }`
|
|
1131
1136
|
* @throws {ConnectionError} Before `connect()` has built a transport.
|
|
1132
1137
|
*/
|
|
1133
1138
|
getConsumerState() {
|
|
@@ -525,13 +525,52 @@ class RabbitMQClient extends EventEmitter {
|
|
|
525
525
|
}
|
|
526
526
|
|
|
527
527
|
/**
|
|
528
|
-
*
|
|
529
|
-
*
|
|
528
|
+
* THE definition of "this client's consumer is attached to that queue".
|
|
529
|
+
*
|
|
530
|
+
* The tag is the one fact that is about THIS client: the broker gives it to
|
|
531
|
+
* the attachment it created, and since d.386b / d.605 the entry carries it
|
|
532
|
+
* only while that attachment exists — a refused re-registration and a cancel
|
|
533
|
+
* sent by the broker both drop it while the consumer stays TRACKED, because
|
|
534
|
+
* the next recovery must find it on the list.
|
|
535
|
+
*
|
|
536
|
+
* It is a method rather than an expression repeated twice for the reason
|
|
537
|
+
* `change-discipline.md` § One rail per concern states: until d.613
|
|
538
|
+
* `getConsumerState()` answered from `_activeConsumers.keys()` and
|
|
539
|
+
* `_performHealthCheck()` from the tag, so one file held two definitions of
|
|
540
|
+
* "active" — and the cheap synchronous reader, the one a service and an
|
|
541
|
+
* operator actually read, was the one that answered about a consumer the
|
|
542
|
+
* broker had already cancelled.
|
|
543
|
+
*
|
|
544
|
+
* @param {Object|undefined} consumerInfo - The entry `_activeConsumers` holds
|
|
545
|
+
* @returns {boolean}
|
|
546
|
+
* @private
|
|
547
|
+
*/
|
|
548
|
+
_isConsumerAttached(consumerInfo) {
|
|
549
|
+
return typeof consumerInfo?.consumerTag === 'string' && consumerInfo.consumerTag !== '';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Get current consumer state.
|
|
554
|
+
*
|
|
555
|
+
* `tracked` counts the consumers this client would restore; `active` names the
|
|
556
|
+
* ones that are ATTACHED right now (`_isConsumerAttached()`, the same question
|
|
557
|
+
* `_performHealthCheck()` asks). `tracked > active.length` is therefore the
|
|
558
|
+
* cheap, synchronous statement that this client takes no message off some
|
|
559
|
+
* queue it registered — the state
|
|
560
|
+
* `docs/operations/rabbitmq-connection-recovery-troubleshooting.md` sends an
|
|
561
|
+
* operator here to read, and which the queue-name list could not carry.
|
|
562
|
+
*
|
|
563
|
+
* @returns {{tracked: number, active: string[]}} Consumer state information
|
|
530
564
|
*/
|
|
531
565
|
getConsumerState() {
|
|
566
|
+
const active = [];
|
|
567
|
+
for (const [queue, consumerInfo] of this._activeConsumers.entries()) {
|
|
568
|
+
if (this._isConsumerAttached(consumerInfo)) active.push(queue);
|
|
569
|
+
}
|
|
570
|
+
|
|
532
571
|
return {
|
|
533
572
|
tracked: this._activeConsumers.size,
|
|
534
|
-
active
|
|
573
|
+
active
|
|
535
574
|
};
|
|
536
575
|
}
|
|
537
576
|
|
|
@@ -1858,54 +1897,160 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1858
1897
|
if (this._activeConsumers.size > 0) {
|
|
1859
1898
|
this._logger.info(`[RabbitMQClient] [mq-client-core] Re-registering ${this._activeConsumers.size} consumers...`);
|
|
1860
1899
|
for (const [queue, consumerInfo] of this._activeConsumers.entries()) {
|
|
1861
|
-
|
|
1862
|
-
await this._prepareQueueForConsume(queue, {
|
|
1863
|
-
durable: consumerInfo.options.durable,
|
|
1864
|
-
queueOptions: consumerInfo.options.queueOptions
|
|
1865
|
-
});
|
|
1866
|
-
|
|
1867
|
-
// The consumer's prefetch is re-applied here for the same reason the
|
|
1868
|
-
// queue step is: the channel is new, and QoS lives on the channel.
|
|
1869
|
-
await this._applyConsumerPrefetch(queue, consumerInfo.options.prefetch);
|
|
1870
|
-
|
|
1871
|
-
const consumeResult = await this._consumerChannel.consume(
|
|
1872
|
-
queue,
|
|
1873
|
-
consumerInfo.handler,
|
|
1874
|
-
{ noAck: consumerInfo.options.noAck }
|
|
1875
|
-
);
|
|
1876
|
-
consumerInfo.consumerTag = consumeResult.consumerTag;
|
|
1900
|
+
if (await this._reRegisterConsumer(queue, consumerInfo)) {
|
|
1877
1901
|
reRegistered++;
|
|
1878
|
-
|
|
1879
|
-
} catch (err) {
|
|
1902
|
+
} else {
|
|
1880
1903
|
failed++;
|
|
1881
|
-
this._logger.error(`[RabbitMQClient] [mq-client-core] Failed to re-register consumer for queue ${queue}:`, err.message);
|
|
1882
|
-
// The consumer STAYS tracked. Whatever refused it — a 406 against the
|
|
1883
|
-
// owner's declaration, a missing infrastructure queue, a channel that
|
|
1884
|
-
// went away mid-flight — is a condition somebody fixes at its source,
|
|
1885
|
-
// and the next recreate must find the consumer still on the list.
|
|
1886
|
-
//
|
|
1887
|
-
// Its TAG does not stay, and the two are not the same fact (d.386b). A
|
|
1888
|
-
// consumer tag names an attachment on ONE channel; the channel that
|
|
1889
|
-
// issued this one is gone and the new one refused to re-attach, so the
|
|
1890
|
-
// string the entry carried names nothing on the broker. Two readers
|
|
1891
|
-
// took it for an attachment that exists: `_performHealthCheck()` counts
|
|
1892
|
-
// `active` by "is the tag set", so a client whose only consumer had
|
|
1893
|
-
// just been refused reported `healthy: true` and `1/1 consumers active`
|
|
1894
|
-
// — the false OK d.386 exists to end, one layer below where d.386 could
|
|
1895
|
-
// reach it; and `cancelConsumer()` sent `basic.cancel` with it, asking
|
|
1896
|
-
// the broker to stop a consumer it does not hold.
|
|
1897
|
-
consumerInfo.consumerTag = null;
|
|
1898
|
-
// The broker's own code is what a listener acts on (406 = the queue
|
|
1899
|
-
// exists with different arguments), and the rail wraps it in a typed
|
|
1900
|
-
// error, so it is read from the cause when the wrapper carries none.
|
|
1901
|
-
const code = err.code !== undefined ? err.code : (err.cause ? err.cause.code : undefined);
|
|
1902
|
-
this.emit('consumer:re-registration:failed', { queue, error: err.message, code });
|
|
1903
1904
|
}
|
|
1904
1905
|
}
|
|
1905
1906
|
}
|
|
1906
1907
|
return { reRegistered, failed };
|
|
1907
1908
|
}
|
|
1908
1909
|
|
|
1910
|
+
/**
|
|
1911
|
+
* Re-attach ONE tracked consumer on the current consumer channel.
|
|
1912
|
+
*
|
|
1913
|
+
* This is the body of the loop above, given a name in d.605 because a second
|
|
1914
|
+
* event needs exactly it: `basic.cancel` from the broker takes ONE consumer
|
|
1915
|
+
* away while every other one stays attached, so re-running the whole loop
|
|
1916
|
+
* would re-attach consumers that never lost their place — a second
|
|
1917
|
+
* `basic.consume` for each of them on the broker. One rail, one set of
|
|
1918
|
+
* consequences (`change-discipline.md` § One rail per concern).
|
|
1919
|
+
*
|
|
1920
|
+
* Everything a first `consume()` does is done here: the queue is classified,
|
|
1921
|
+
* declared or checked by `_prepareQueueForConsume()` — so an owned business
|
|
1922
|
+
* queue is re-declared and an unowned infrastructure queue is only checked,
|
|
1923
|
+
* its absence answered with `CONSUMER_QUEUE_MISSING` rather than a silent
|
|
1924
|
+
* creation — and the prefetch is re-applied, because QoS lives on the channel.
|
|
1925
|
+
*
|
|
1926
|
+
* @param {string} queue - Queue name
|
|
1927
|
+
* @param {Object} consumerInfo - The entry `_activeConsumers` holds for it
|
|
1928
|
+
* @returns {Promise<boolean>} `true` when the consumer is attached again
|
|
1929
|
+
* @private
|
|
1930
|
+
*/
|
|
1931
|
+
async _reRegisterConsumer(queue, consumerInfo) {
|
|
1932
|
+
try {
|
|
1933
|
+
await this._prepareQueueForConsume(queue, {
|
|
1934
|
+
durable: consumerInfo.options.durable,
|
|
1935
|
+
queueOptions: consumerInfo.options.queueOptions
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
// The consumer's prefetch is re-applied here for the same reason the
|
|
1939
|
+
// queue step is: the channel is new, and QoS lives on the channel.
|
|
1940
|
+
await this._applyConsumerPrefetch(queue, consumerInfo.options.prefetch);
|
|
1941
|
+
|
|
1942
|
+
const consumeResult = await this._consumerChannel.consume(
|
|
1943
|
+
queue,
|
|
1944
|
+
consumerInfo.handler,
|
|
1945
|
+
{ noAck: consumerInfo.options.noAck }
|
|
1946
|
+
);
|
|
1947
|
+
consumerInfo.consumerTag = consumeResult.consumerTag;
|
|
1948
|
+
this._logger.info(`[RabbitMQClient] [mq-client-core] ✓ Re-registered consumer for queue: ${queue} (consumerTag: ${consumeResult.consumerTag})`);
|
|
1949
|
+
return true;
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
this._logger.error(`[RabbitMQClient] [mq-client-core] Failed to re-register consumer for queue ${queue}:`, err.message);
|
|
1952
|
+
// The consumer STAYS tracked. Whatever refused it — a 406 against the
|
|
1953
|
+
// owner's declaration, a missing infrastructure queue, a channel that
|
|
1954
|
+
// went away mid-flight — is a condition somebody fixes at its source,
|
|
1955
|
+
// and the next recreate must find the consumer still on the list.
|
|
1956
|
+
//
|
|
1957
|
+
// Its TAG does not stay, and the two are not the same fact (d.386b). A
|
|
1958
|
+
// consumer tag names an attachment on ONE channel; the channel that
|
|
1959
|
+
// issued this one is gone and the new one refused to re-attach, so the
|
|
1960
|
+
// string the entry carried names nothing on the broker. Two readers
|
|
1961
|
+
// took it for an attachment that exists: `_performHealthCheck()` counts
|
|
1962
|
+
// `active` by "is the tag set", so a client whose only consumer had
|
|
1963
|
+
// just been refused reported `healthy: true` and `1/1 consumers active`
|
|
1964
|
+
// — the false OK d.386 exists to end, one layer below where d.386 could
|
|
1965
|
+
// reach it; and `cancelConsumer()` sent `basic.cancel` with it, asking
|
|
1966
|
+
// the broker to stop a consumer it does not hold.
|
|
1967
|
+
consumerInfo.consumerTag = null;
|
|
1968
|
+
// The broker's own code is what a listener acts on (406 = the queue
|
|
1969
|
+
// exists with different arguments), and the rail wraps it in a typed
|
|
1970
|
+
// error, so it is read from the cause when the wrapper carries none.
|
|
1971
|
+
const code = err.code !== undefined ? err.code : (err.cause ? err.cause.code : undefined);
|
|
1972
|
+
this.emit('consumer:re-registration:failed', { queue, error: err.message, code });
|
|
1973
|
+
return false;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/**
|
|
1978
|
+
* The broker cancelled one of this client's consumers.
|
|
1979
|
+
*
|
|
1980
|
+
* `basic.cancel` sent BY THE BROKER is not an error frame and does not touch
|
|
1981
|
+
* the channel or the connection: the queue was deleted, or the broker moved it
|
|
1982
|
+
* (`x-cancel-on-ha-failover`). Nothing else in this client hears it — the
|
|
1983
|
+
* channel handlers listen for `error` and `close`, and both stay silent — so
|
|
1984
|
+
* this method is the whole of the client's answer, and until d.605 that answer
|
|
1985
|
+
* was nothing at all.
|
|
1986
|
+
*
|
|
1987
|
+
* Three things happen, in this order:
|
|
1988
|
+
*
|
|
1989
|
+
* 1. the TAG is dropped, first, because it is the one fact
|
|
1990
|
+
* `_performHealthCheck()` reads to answer "is THIS client's consumer
|
|
1991
|
+
* attached" (d.386b). Dropping it before anything else leaves no window in
|
|
1992
|
+
* which the check reports an attachment the broker has already cancelled;
|
|
1993
|
+
* 2. the loss is stated at ERROR level, naming the queue and the cancelled
|
|
1994
|
+
* tag. A consumer that stops reading a queue is not a debug-level event,
|
|
1995
|
+
* and the record is what tells an operator which queue went quiet;
|
|
1996
|
+
* 3. the consumer is re-attached through `_reRegisterConsumer()`, the rail a
|
|
1997
|
+
* channel recreate already uses. A refusal there keeps the consumer
|
|
1998
|
+
* tracked with no tag and travels on `consumer:re-registration:failed` —
|
|
1999
|
+
* which is why this method introduces no event and no public method of its
|
|
2000
|
+
* own: every state it can reach is a state the package already reports.
|
|
2001
|
+
*
|
|
2002
|
+
* @param {string} queue - The queue whose consumer the broker cancelled
|
|
2003
|
+
* @returns {Promise<void>}
|
|
2004
|
+
* @private
|
|
2005
|
+
*/
|
|
2006
|
+
async _onBrokerCancel(queue) {
|
|
2007
|
+
if (this._closedByCaller) {
|
|
2008
|
+
// An intentional close is not a fault — the doctrine `disconnect()` states
|
|
2009
|
+
// and both channel handlers already follow.
|
|
2010
|
+
this._logger.debug(
|
|
2011
|
+
`[RabbitMQClient] [mq-client-core] [CONSUMER] Broker cancelled the consumer for queue "${queue}" while the client was being closed by its caller - no alarm, no re-attachment`
|
|
2012
|
+
);
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
const consumerInfo = this._activeConsumers.get(queue);
|
|
2017
|
+
const cancelledTag = consumerInfo ? consumerInfo.consumerTag : null;
|
|
2018
|
+
|
|
2019
|
+
if (consumerInfo) {
|
|
2020
|
+
consumerInfo.consumerTag = null;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
this._logger.error(
|
|
2024
|
+
`[RabbitMQClient] Consumer cancelled by the broker - the consumer on queue "${queue}" `
|
|
2025
|
+
+ `(consumerTag: ${cancelledTag}) was stopped by the BROKER, not by this client: the queue `
|
|
2026
|
+
+ 'was deleted, or the broker moved it (x-cancel-on-ha-failover). '
|
|
2027
|
+
+ 'Expected: a consumer stays attached until cancelConsumer() or disconnect(). '
|
|
2028
|
+
+ 'Fix: none here - the client re-attaches it on the rail a channel recreate uses; if that '
|
|
2029
|
+
+ 'is refused, the consumer stays tracked with no tag, performHealthCheck() reports this '
|
|
2030
|
+
+ 'client unhealthy and names the queue, and consumer:re-registration:failed carries the '
|
|
2031
|
+
+ 'broker reason.',
|
|
2032
|
+
{ queue, consumerTag: cancelledTag }
|
|
2033
|
+
);
|
|
2034
|
+
|
|
2035
|
+
if (!consumerInfo) {
|
|
2036
|
+
// The client no longer holds this consumer, so there is nothing to
|
|
2037
|
+
// re-attach and no state left to correct. The loss is still stated above.
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
if (!this._isChannelAlive(this._consumerChannel)) {
|
|
2042
|
+
// Re-attaching on a dead channel is not this method's job: the channel
|
|
2043
|
+
// recovery re-registers every tracked consumer when it recreates the
|
|
2044
|
+
// channel (d.262), and the consumer stays tracked for exactly that.
|
|
2045
|
+
this._logger.warn(
|
|
2046
|
+
`[RabbitMQClient] [mq-client-core] [CONSUMER] Consumer for queue "${queue}" stays tracked and unattached - the consumer channel is not alive, so the channel recovery re-registers it`
|
|
2047
|
+
);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
await this._reRegisterConsumer(queue, consumerInfo);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
1909
2054
|
/**
|
|
1910
2055
|
* Disconnects: closes channel and connection.
|
|
1911
2056
|
* @returns {Promise<void>}
|
|
@@ -2061,8 +2206,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2061
2206
|
// this client's and everybody else's alike, so it cannot say whether
|
|
2062
2207
|
// the one asking is among them. The tag can: it is the name the
|
|
2063
2208
|
// broker gave THIS client's attachment, and since d.386b it is set
|
|
2064
|
-
// only while that attachment exists.
|
|
2065
|
-
|
|
2209
|
+
// only while that attachment exists. The question is asked through
|
|
2210
|
+
// `_isConsumerAttached()` — the one definition `getConsumerState()`
|
|
2211
|
+
// now reads too (d.613).
|
|
2212
|
+
if (this._isConsumerAttached(consumerInfo)) {
|
|
2066
2213
|
health.consumers.active++;
|
|
2067
2214
|
} else {
|
|
2068
2215
|
health.consumers.inactive.push(queue);
|
|
@@ -3532,7 +3679,21 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3532
3679
|
// Wrap handler to track message processing and prefetch utilization
|
|
3533
3680
|
const messageHandler = async (msg) => {
|
|
3534
3681
|
if (msg === null) {
|
|
3535
|
-
|
|
3682
|
+
// `null` is not a message. It is `basic.cancel` FROM THE BROKER,
|
|
3683
|
+
// handed to this callback by amqplib (`handleCancel()` →
|
|
3684
|
+
// `dispatchMessage(fields, null)`), and it is the only way a consumer
|
|
3685
|
+
// learns that its attachment is gone while the channel and the
|
|
3686
|
+
// connection stay up: the queue was deleted, or the broker moved it
|
|
3687
|
+
// (`x-cancel-on-ha-failover`).
|
|
3688
|
+
//
|
|
3689
|
+
// Until d.605 the answer was this bare `return`. Nothing was logged,
|
|
3690
|
+
// the tag stayed set — so `_performHealthCheck()` went on counting the
|
|
3691
|
+
// consumer as attached — and nothing re-attached it. The service
|
|
3692
|
+
// reported itself healthy while no message was taken off the queue,
|
|
3693
|
+
// which is the failure `automation-gates.md` §5 calls a defect of the
|
|
3694
|
+
// same severity as a wrong result.
|
|
3695
|
+
await this._onBrokerCancel(queue);
|
|
3696
|
+
return;
|
|
3536
3697
|
}
|
|
3537
3698
|
|
|
3538
3699
|
// IMPORTANT:
|