@onlineapps/mq-client-core 3.2.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 +368 -0
- package/README.md +77 -6
- package/package.json +2 -2
- package/src/BaseClient.js +65 -4
- package/src/config/configSchema.js +7 -14
- package/src/config/defaultConfig.js +3 -7
- package/src/config/queueConfig.js +22 -2
- package/src/transports/rabbitmqClient.js +462 -77
- package/src/workers/RecoveryWorker.js +10 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,374 @@ 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
|
+
|
|
77
|
+
## [4.0.0] — 2026-09-16
|
|
78
|
+
|
|
79
|
+
### Changed — značku odmítnutí nese i deklarační rail, jednou funkcí pro obě cesty (d.582)
|
|
80
|
+
|
|
81
|
+
d.577 dala verdikt cestě `consume()`. `assertQueue()` — veřejný rail, kterým jde
|
|
82
|
+
**každá** deklarace a po kterém jde FÁZE 0.8 `@onlineapps/service-wrapper`, když
|
|
83
|
+
si služba zakládá vlastní business fronty — dál propouštěl syrovou chybu
|
|
84
|
+
z amqplib. Odmítnutá deklarace tak odcházela s `code` = **číslo** `406`
|
|
85
|
+
a bez jakékoli klasifikace: `@onlineapps/conn-orch-registry` přenáší, co
|
|
86
|
+
rozhodlo jádro (d.579), a neměl co přenést; wrapper, který končí fázi natrvalo na
|
|
87
|
+
`code: 'MQ_CONNECTION_FATAL'`, označil odmítnutí za přechodné.
|
|
88
|
+
|
|
89
|
+
Teď obě cesty odpovídají **jednou funkcí** `_asRefusal(kind, queue, err)`: `kind`
|
|
90
|
+
volí větu a třídu obalu, verdikt se píše jednou a v pravopisu, který už používá
|
|
91
|
+
`_failFatally()`. Odmítnutá deklarace vychází jako chyba s
|
|
92
|
+
`code: 'MQ_CONNECTION_FATAL'`, `reason: 'broker-refused'`, jménem fronty v `queue`
|
|
93
|
+
a původní chybou brokeru — včetně reply kódu — v `cause`. Dvě kopie jedné
|
|
94
|
+
klasifikace by se mohly rozejít (`change-discipline.md` § One rail per concern).
|
|
95
|
+
|
|
96
|
+
`_failFatally()` se ani tady nevolá: odmítnuta je fronta, ne spojení — klient
|
|
97
|
+
zůstává připojený a `isConnectionFatal()` je dál `false`.
|
|
98
|
+
|
|
99
|
+
Kdo z toho co má: po re-pinu na tuto verzi doručí `ensureQueues()`
|
|
100
|
+
v `@onlineapps/conn-orch-registry` (d.579) značku wrapperu, takže 406 z FÁZE 0.8
|
|
101
|
+
skončí boot natrvalo místo aby ho opakoval proti frontě, kterou služba nikdy
|
|
102
|
+
nesmí založit.
|
|
103
|
+
|
|
104
|
+
Chování, které se **nemění**: 404, „channel ended", 503 i odmítnutí volajícího
|
|
105
|
+
(`Queue lifetime is not the caller's to set`) odcházejí přesně tak, jak přišly;
|
|
106
|
+
přijatá deklarace dál vrací `queue.declare-ok` brokeru. Kontrolní noha d.295
|
|
107
|
+
v `connect-refusal-fails-fast.integration.test.js` čte reply kód nově o jedno
|
|
108
|
+
`cause` níž — její vlastní tvrzení (spojení nebylo v sázce, klient se nestaví
|
|
109
|
+
mimo provoz) platí beze změny.
|
|
110
|
+
|
|
111
|
+
### Changed — odmítnutí fronty brokerem nese stejný verdikt jako odmítnutí spojení (d.577)
|
|
112
|
+
|
|
113
|
+
Fronta, která na brokeru existuje s jinými argumenty, než jaké `queueConfig`
|
|
114
|
+
deklaruje, odpoví na `assertQueue()` kódem **406 PRECONDITION-FAILED**.
|
|
115
|
+
`_prepareQueueForConsume()` z toho stavěl `ConsumeError` **bez `code` a bez
|
|
116
|
+
`reason`** — kód brokeru přežil jen v `cause` (a o jedno obalení výš, v
|
|
117
|
+
`@onlineapps/conn-orch-registry`, až v `cause.cause`). `REFUSAL_REPLY_CODES` a
|
|
118
|
+
`_isBrokerRefusal()` se přitom ptaly jen v connect/reconnect.
|
|
119
|
+
|
|
120
|
+
Co to stálo: `@onlineapps/service-wrapper` ukončí FÁZI 0.7 trvale jen na
|
|
121
|
+
`code: 'MQ_CONNECTION_FATAL'`, takže 406 na frontě byl klasifikován jako
|
|
122
|
+
PŘECHODNÝ a služba opakovala boot proti frontě, kterou nikdy nesmí založit —
|
|
123
|
+
každý pokus dostane stejnou odpověď (změřeno sondou W532 proti živému brokeru).
|
|
124
|
+
|
|
125
|
+
Teď je to **jeden slovník pro jeden fakt**: každá chyba z cesty
|
|
126
|
+
`consume()` — klasifikace fronty, deklarace i registrace konzumenta — kterou
|
|
127
|
+
broker odmítl kódem z `REFUSAL_REPLY_CODES` (403/530/406), vychází jako
|
|
128
|
+
`ConsumeError` s `code: 'MQ_CONNECTION_FATAL'`, `reason: 'broker-refused'` a
|
|
129
|
+
původní chybou brokeru v `cause`. Klasifikuje se **kódem odpovědi**, ne zněním
|
|
130
|
+
hlášky (`_isBrokerRefusal()`, jediná kolej). Verdikt píše jedno místo — `catch`
|
|
131
|
+
v `consume()` transportu — a `BaseClient.consume()` takovou chybu přeposílá
|
|
132
|
+
**beze změny**: obalení by před přesnou větu brokeru postavilo obecnější a
|
|
133
|
+
značku zanořilo o jedno `cause` hlouběji.
|
|
134
|
+
|
|
135
|
+
Co se záměrně **nemění**: `_failFatally()` se tu nevolá. Odmítnuta je fronta, ne
|
|
136
|
+
spojení — klient zůstává připojený, publikační cesta nad týmž spojením nemá co
|
|
137
|
+
odpovídat a líné zotavení se nezastavuje. Chyba, kterou broker neodmítl, odchází
|
|
138
|
+
přesně tak, jak přišla: 404 dál nese `CONSUMER_QUEUE_MISSING`, chybějící
|
|
139
|
+
dead-letter route dál `CONSUMER_DEAD_LETTER_ROUTE_MISSING`, 503 a „channel
|
|
140
|
+
ended" dál žádnou značku.
|
|
141
|
+
|
|
142
|
+
### Documentation — kadence heartbeatu se tady nedeklaruje (d.574)
|
|
143
|
+
|
|
144
|
+
Dva komentáře v `src/config/queueConfig.js` psaly kadenci heartbeatu číslem
|
|
145
|
+
(„every 10s"). Balíček ji nevlastní: je to povinná volba konstruktoru
|
|
146
|
+
`RegistryClient` (`@onlineapps/conn-orch-registry`, `heartbeatInterval`), kterou
|
|
147
|
+
`@onlineapps/service-wrapper` plní z `BIZ_HEARTBEAT_INTERVAL_MS`. Ručně psané
|
|
148
|
+
číslo v komentáři je popisný fakt bez mechanismu, který ho drží pravdivý
|
|
149
|
+
(`.claude/rules/doc-code-binding.md` §1), takže obě věty teď ukazují na
|
|
150
|
+
vlastníka a číslo nenesou. Chování ani API se nemění.
|
|
151
|
+
|
|
152
|
+
### Changed — odmítnutí brokerem na cestě bootu nese verdikt (d.295)
|
|
153
|
+
|
|
154
|
+
`connect()` přeposílal syrovou chybu z amqplib bez jakéhokoli verdiktu, takže
|
|
155
|
+
volající nad ním nemohl poznat, jestli broker **odpověděl a odmítl** (403
|
|
156
|
+
ACCESS-REFUSED, 530 NOT-ALLOWED, 406 PRECONDITION-FAILED), nebo jestli **nikdo
|
|
157
|
+
neodpověděl** (ECONNREFUSED, timeout). `_isBrokerRefusal()` ten rozdíl umí od
|
|
158
|
+
d.339, ale četla ho jediná cesta — `_reconnectWithBackoff()`, tedy spojení
|
|
159
|
+
ztracené za běhu. Teď ho čte i první `connect()`: odmítnutí z něj vychází jako
|
|
160
|
+
chyba s `code: 'MQ_CONNECTION_FATAL'`, `reason: 'broker-refused'`, původní chybou
|
|
161
|
+
v `cause` a hláškou §5, která jmenuje credentials, vhost a topologii jako fix.
|
|
162
|
+
`isConnectionFatal()` je od té chvíle `true`, takže se proti odmítajícímu brokeru
|
|
163
|
+
už nerozbíhá líné zotavení.
|
|
164
|
+
|
|
165
|
+
Verdikt se na této cestě **neohlašuje** na `connection:fatal` ani přes `onFatal`:
|
|
166
|
+
volající na `connect()` čeká, takže se ho dozví výjimkou, a druhé oznámení téže
|
|
167
|
+
události je přesně ta škoda, kterou popisuje `_reportRecoveryFailure()`. Výsledek
|
|
168
|
+
bootu patří volajícímu bootu (`@onlineapps/service-wrapper`, poznámka `BOOT
|
|
169
|
+
budget` v `_initializeMQ()`), ne kanálu pro asynchronní ztrátu.
|
|
170
|
+
|
|
171
|
+
`BaseClient.connect()` verdikt propouští beze změny. Dosud ho obalil do obecné
|
|
172
|
+
`ConnectionError` s fixem „read error.cause" — přesná odpověď degradovaná na
|
|
173
|
+
vágní, a značka, na kterou volající přestává opakovat, se ztratila úplně
|
|
174
|
+
(`ConnectionError` bere `code` jako argument konstruktoru a to volání žádný
|
|
175
|
+
nepředávalo).
|
|
176
|
+
|
|
177
|
+
Co se **nemění**: `REFUSAL_REPLY_CODES` zůstávají `[403, 530, 406]` — 503
|
|
178
|
+
COMMAND-INVALID se nepřidává, žádné měření si to nevyžádalo. Neodpovídající
|
|
179
|
+
broker odchází z `connect()` přesně jako dřív, syrový a opakovatelný, protože
|
|
180
|
+
„vzdát to navždy" nad výpadkem zakazuje konfirmace
|
|
181
|
+
`mq-client-lifecycle-contract` 001 bod 3. A 406 z `assertQueue()` nad živým
|
|
182
|
+
spojením zůstává chybou fronty, ne ztrátou spojení.
|
|
183
|
+
|
|
184
|
+
### Changed — `publish()` cituje příčinu, nehádá ji (d.514)
|
|
185
|
+
|
|
186
|
+
`BaseClient.publish()` přepisoval všechno, co transport vyhodí, na jednu větu:
|
|
187
|
+
*„Failed to publish to queue "<q>" - Expected: the queue to exist and the channel
|
|
188
|
+
to be open. Fix: read error.cause for the broker reason."* Dvě ze tří jejích
|
|
189
|
+
klauzulí byly dohad. Transport odmítá publikaci i z důvodů, které s chybějící
|
|
190
|
+
frontou ani se zavřeným kanálem nemají nic společného — pravidlo vlastnictví
|
|
191
|
+
(`QueueNotFoundError`, `kind: 'unowned'`, d.419), vyčerpaný rozpočet opakování,
|
|
192
|
+
odmítnutí brokerem — a u každého z nich hláška posílala čtenáře prověřit frontu,
|
|
193
|
+
která existuje, a kanál, který je otevřený. Pravá věta ležela v `error.cause`,
|
|
194
|
+
kam se řádek logu nedívá.
|
|
195
|
+
|
|
196
|
+
Je to táž vada, kterou d.297 odstranila o volání vedle (`_consumerRefusal()`),
|
|
197
|
+
a odpovídá se stejně: vrstva **nejmenuje nejpravděpodobnější příčinu, cituje tu,
|
|
198
|
+
kterou skutečně chytila**. Nový tvar hlášky §5 je
|
|
199
|
+
`[BaseClient] Failed to publish to queue "<q>": <věta transportu> - Expected:
|
|
200
|
+
publish() to hand the message to the broker. Fix: act on the reason above …
|
|
201
|
+
error.cause carries the original error, error.queue names the target queue.`
|
|
202
|
+
|
|
203
|
+
Co se **nemění**: třída (`PublishError`), `error.queue`, `error.cause` ani objekt
|
|
204
|
+
v něm — volající, který se rozhoduje podle třídy, čte dál objekt. Nemění se ani
|
|
205
|
+
`ConnectionError` z nepřipojeného klienta a `SerializationError` z nepřevoditelné
|
|
206
|
+
zprávy; obě mají vlastní, pravdivou větu už teď. Odmítnutí, které vyhodí hodnotu
|
|
207
|
+
bez `message` (neErrorová rejekce), se do hlášky přenáší celé (`String(thrown)`),
|
|
208
|
+
protože „undefined" uprostřed odmítnutí je horší odpověď než řetězec, kterým
|
|
209
|
+
někdo odmítl.
|
|
210
|
+
|
|
211
|
+
### Removed (BREAKING) — `recoveryScope`: klíč, který po d.419 nic nečetlo (d.514)
|
|
212
|
+
|
|
213
|
+
`recoveryScope` byl poslední zbytek dveří, kterými klient zakládal frontu bez
|
|
214
|
+
vlastníka. d.419 odebrala `queueCreationFilter` i `queueCreationCallback`
|
|
215
|
+
a `RecoveryWorker.createQueue()`; hodnota `recoveryScope` ale dál putovala ze
|
|
216
|
+
schématu přes `rabbitmqClient.js` do `RecoveryWorker`, kde se přiřadila do pole
|
|
217
|
+
`this._scope`, **které nečetlo nic v celém balíčku** (měřeno 2026-09-15:
|
|
218
|
+
`grep -rn "_scope" src/` → jediný výskyt je přiřazení a jedna zmínka v komentáři).
|
|
219
|
+
Deklarace, kterou nikdo nečte, tvrdí čtenáři, že na ní něco závisí.
|
|
220
|
+
|
|
221
|
+
Odchází celý řetěz: `configSchema.recoveryScope`, `defaultConfig.recoveryScope`,
|
|
222
|
+
argument `scope` konstruktoru `RecoveryWorker` i pole `this._scope`. Schéma je
|
|
223
|
+
ZAVŘENÉ, takže konfigurace, která klíč pořád nese, je odmítnuta **jménem** při
|
|
224
|
+
konstrukci — ne tiše ignorována.
|
|
225
|
+
|
|
226
|
+
**Čtyři otázky** (`.claude/rules/change-discipline.md` § Removing something
|
|
227
|
+
removes its declaration): **(1) proč vznikl** — `scope` říkal, jestli tenhle
|
|
228
|
+
klient smí zakládat fronty; v `business` scope je zakládal, v `infrastructure`
|
|
229
|
+
ne. Byla to volba provozovatele nad ad-hoc jmény z doby, kdy neexistovala
|
|
230
|
+
centrální šablona, ze které by se dala fronta deklarovat. **(2) která část
|
|
231
|
+
koncepce ho nesla** — hlavička `RecoveryWorker`, § Queue Creation: *„ostatní
|
|
232
|
+
jména … smí založit, pokud to scope a `queueCreationFilter` dovolují"*. Ta část
|
|
233
|
+
**neplatí**: nahradila ji konfirmace `mq-consumer-contract` 006 — *„queues come
|
|
234
|
+
into being only from declarations"* — a d.419 podle ní zavřela obě zbylé dveře.
|
|
235
|
+
**(3) proč ho dnes nikdo nečte** — ustoupil rozhodnutí: s dveřmi zmizel jediný
|
|
236
|
+
čtenář a klíč zůstal ležet. Vacuózní byl ale už předtím, a je to zapsané v kódu,
|
|
237
|
+
který se teď maže: `ConnectorMQClient` nastavoval `'business'` **každé** biz
|
|
238
|
+
službě, takže „smí zakládat" platilo plošně a nerozhodovalo nic. **(4) je náhrada
|
|
239
|
+
koncepčnější** — ano. O tvaru hlášení rozhoduje třída vlastnictví, kterou
|
|
240
|
+
odpověděla centrální deklarace (`error.kind` z `QueueNotFoundError`: `infrastructure`
|
|
241
|
+
/ `business` / `unowned`). Ta plyne z `queueConfig`, ne z toho, kdo klienta
|
|
242
|
+
postavil, takže dva klienti namířené na tutéž frontu nemohou dostat různou
|
|
243
|
+
odpověď — a odpověď se nemůže rozejít s deklarací, protože je z ní odvozená.
|
|
244
|
+
|
|
245
|
+
**Pro volajícího:** `@onlineapps/conn-infra-mq` klíč jak je publikován pořád
|
|
246
|
+
posílá (`ConnectorMQClient.js`, `recoveryScope: config.recoveryScope || 'business'`;
|
|
247
|
+
pinuje `@onlineapps/mq-client-core` 2.0.1, kde klíč ještě existuje). Až se pin
|
|
248
|
+
pohne, bude odmítnut jménem při konstrukci. Úprava konektoru patří do dávky, která
|
|
249
|
+
pin posouvá, ne do téhle — stejně jako u `queueCreationCallback` v d.419.
|
|
250
|
+
|
|
251
|
+
### Removed — výjimka pro `rpc.reply.*` v `consume()`, nedosažitelná od d.259 (d.514)
|
|
252
|
+
|
|
253
|
+
`_prepareQueueForConsume()` měla třetí třídu jména: co začínalo `rpc.reply.`,
|
|
254
|
+
přeskočilo klasifikaci, rozlišení argumentů z centrální konfigurace i otázku na
|
|
255
|
+
brokera (`checkQueue`/`assertQueue`). Mezi ty dvě výjimky ale d.259 postavila
|
|
256
|
+
bránu nedoručitelných zpráv, a ta takové jméno **odmítne dřív**: `queueConfig`
|
|
257
|
+
ho neklasifikuje jako infrastrukturní ani business (měřeno:
|
|
258
|
+
`isInfrastructureQueue('rpc.reply.abc123') === false`, `isBusinessQueue` totéž),
|
|
259
|
+
takže pro něj nedeklaruje žádnou cestu, a `getDeadLetterRoute()` vrací `null`
|
|
260
|
+
při `isDeadLetterTarget() === false`. Druhá polovina výjimky tak byla mrtvý kód
|
|
261
|
+
a první nerozhodovala nic (obě klasifikace jsou pro to jméno `false` i bez ní).
|
|
262
|
+
|
|
263
|
+
**Čtyři otázky**: **(1) proč vznikla** — RPC odpovědní cesta: volající si sám
|
|
264
|
+
založil dočasnou, nedurabilní frontu s vlastní životností a konzumoval z ní, takže
|
|
265
|
+
`consume()` nad ní nesměl nic deklarovat. **(2) která část koncepce ji nesla** —
|
|
266
|
+
právě ta cesta. **Neplatí**: `createTemporaryQueue()` byla zrušena
|
|
267
|
+
(`conn-infra-mq` d.275b) a konfirmace `mq-consumer-contract` 003 bod 3 to říká
|
|
268
|
+
větou — *„`rpc.reply.*` is not a topology question — `createTemporaryQueue()` has
|
|
269
|
+
no caller, it is a removal question"*. **(3) proč ji dnes nic nečte** — ustoupila
|
|
270
|
+
rozhodnutí d.259: konzument bez cesty pro nedoručitelnou zprávu se nepřipojí,
|
|
271
|
+
a fronta bez deklarace žádnou cestu nemá. **(4) je náhrada koncepčnější** — ano,
|
|
272
|
+
je to jedna kolej: fronta je konzumovatelná právě tehdy, když centrální deklarace
|
|
273
|
+
říká, kam s odmítnutou zprávou. Neexistuje druhá třída jmen, která klasifikaci
|
|
274
|
+
a deklaraci přeskočí na slovo volajícího. Výjimka slibovala podporu, kterou d.259
|
|
275
|
+
odebrala, takže kód teď říká, co systém dělá.
|
|
276
|
+
|
|
277
|
+
Chování se **nemění** a je změřené proti živému brokeru: `rpc.reply.*` fronta,
|
|
278
|
+
která na brokeru **skutečně je** (nedurabilní, jak ji zakládala zrušená cesta), je
|
|
279
|
+
odmítnuta touž větou jako každé jiné jméno bez deklarované cesty, nic se k ní
|
|
280
|
+
nepřipojí a nic se nepředeklaruje
|
|
281
|
+
(`tests/integration/dead-letter-route-required.integration.test.js`).
|
|
282
|
+
|
|
283
|
+
### Changed — `amqplib` přišpendlen přesně na `0.10.9` (d.514)
|
|
284
|
+
|
|
285
|
+
`^0.10.3` je plovoucí rozsah, a tenhle balíček na konkrétním chování amqplib
|
|
286
|
+
**staví změřená tvrzení**: „ani `Connection`, ani `Channel` nenese vlastnost
|
|
287
|
+
`closed`" (d.260, amqplib 0.10.9, živý broker, 2026-09-12) je předpoklad
|
|
288
|
+
`isConnected()`, `getChannelState()`, všech tří `_ensure*Channel()`
|
|
289
|
+
a `_performHealthCheck()`. S plovoucím rozsahem může tentýž commit nainstalovat
|
|
290
|
+
jiný kód jiný den — přesně to, čemu pravidlo o pinech brání u `@onlineapps/*`
|
|
291
|
+
(`architecture-principles.md` § Version pinning: „a floating range makes the same
|
|
292
|
+
commit install different code on different days"). Hodnota `0.10.9` není nová,
|
|
293
|
+
je to co bylo v `package-lock.json` nainstalováno (měřeno:
|
|
294
|
+
`require('amqplib/package.json').version` → `0.10.9`); mění se jen to, že ji
|
|
295
|
+
`package.json` říká nahlas. `amqplib` není v SSOT `api/config/libraries.json`
|
|
296
|
+
(měřeno: grep bez shody), takže tenhle pin nevlastní publikační vlna.
|
|
297
|
+
|
|
298
|
+
### Changed (testy) — sady `liveness-health-publish*` říkají, proč mají zotavení vypnuté (d.514)
|
|
299
|
+
|
|
300
|
+
Obě běží s `reconnectEnabled: false`. Důvod je pravdivý a teď je napsaný: co v nich
|
|
301
|
+
umírá, je KANÁL, a obnova spojení má vlastní sady (`reconnect-cycles-lazy-retry`,
|
|
302
|
+
`reconnect-exhaustion`, `disconnect-ends-reconnect`). Napsané je i to, co ten
|
|
303
|
+
příznak nese: **nenese nic** — měřeno 2026-09-15, obě sady jsou zelené i s
|
|
304
|
+
`reconnectEnabled: true` (integrace 4/4 třikrát po sobě, unit 11/11) — a právě
|
|
305
|
+
proto neviděly d.467, kde `performHealthCheck()` spouštěl obnovovací cyklus
|
|
306
|
+
bezpodmínečně. Případ s obnovou se do nich **nedoplňuje**: ta otázka má vlastní
|
|
307
|
+
kolej se zapnutou obnovou (`health-check-does-not-recover-a-live-client`, unit
|
|
308
|
+
i integrace), a druhá akceptační sada nad jedním pojmem je duplicita, kterou
|
|
309
|
+
`change-discipline.md` § One rail per concern označuje za vadu.
|
|
310
|
+
|
|
311
|
+
### Fixed — pracující konzument s prefetchem 1 už není poplach (d.550)
|
|
312
|
+
|
|
313
|
+
Změřeno na BIZ-pdfgen a BIZ-converteru, dvakrát při každém bootu:
|
|
314
|
+
|
|
315
|
+
⚠️ Prefetch utilization high: queue 'biz-pdfgen.registry' has 1/1 messages
|
|
316
|
+
in-flight (100%, threshold: 80%)
|
|
317
|
+
|
|
318
|
+
Registrační fronta má prefetch 1, takže jedna zpráva v letu je 100 % okna — a
|
|
319
|
+
zároveň naprosto normální stav konzumenta, který právě dělá tu jedinou práci,
|
|
320
|
+
kterou smí. Poplach se proto spustil při KAŽDÉM doručení do takové fronty a
|
|
321
|
+
žádný práh na tom nic nemění: `1/1 >= 0.8` platí stejně jako `1/1 >= 0.99`.
|
|
322
|
+
Varování, které nemůže nenastat, neříká nic — jen učí čtenáře přehlížet řádek,
|
|
323
|
+
na kterém jednou bude stát skutečné zahlcení.
|
|
324
|
+
|
|
325
|
+
`_checkPrefetchUtilization()` nově okno jedné zprávy neměří: jeho jediné dva
|
|
326
|
+
stavy jsou 0 % (nečinný) a 100 % (pracuje) a ani jeden není zjištění, kvůli
|
|
327
|
+
kterému by měl kdo zasáhnout. Pro okno větší než jedna zpráva se nemění nic —
|
|
328
|
+
`prefetchUtilizationThreshold` zůstává, jak ho deklaruje
|
|
329
|
+
`src/config/defaultConfig.js`, žádný nový práh ani default nevzniká, a plné okno
|
|
330
|
+
dvou i deseti zpráv se hlásí dál se stejným textem (kontrolní případy v obou
|
|
331
|
+
tierech: `tests/unit/prefetch-of-one-is-not-a-warning.test.js`,
|
|
332
|
+
`tests/integration/prefetch-of-one-is-not-a-warning.integration.test.js`).
|
|
333
|
+
|
|
334
|
+
### Changed (testy) — kontrolní případ redakce jmenuje allow-list, ne jeho polovinu (d.514)
|
|
335
|
+
|
|
336
|
+
`tests/unit/redact-credentials.test.js` měl kontrolní případ nadepsaný *„URL bez
|
|
337
|
+
pověření se vrací nezměněná“*. Tvrzení zní jako obecné pravidlo, které od
|
|
338
|
+
`@onlineapps/logger-contract` 2.0.0 (d.476) neplatí: redakce je ALLOW-LIST —
|
|
339
|
+
zachová protokol, host, port a cestu, a všechno ostatní odejde bez ohledu na to,
|
|
340
|
+
jestli v tom pověření je. URL bez pověření, ale s dotazem (`?heartbeat=60`) se
|
|
341
|
+
nezměněná nevrací. Kontrolní případ, který jmenuje polovinu pravidla, zůstává
|
|
342
|
+
zelený i nad implementací, která dotaz propouští — tedy přesně nad vadou, kvůli
|
|
343
|
+
které allow-list vznikl.
|
|
344
|
+
|
|
345
|
+
Hodnota v assertu se **nemění** (`amqp://api_services_queuer:5672/oa_vhost` se
|
|
346
|
+
vrací identické); mění se věta, kterou test tvrdí, a důvod je u ní zapsaný.
|
|
347
|
+
Sousední případ, který zahození dotazu měří, zůstává jedinou kolejí pro to
|
|
348
|
+
tvrzení — nový se nepřidává. Žádná změna produkčního kódu.
|
|
349
|
+
|
|
350
|
+
### Documentation — terminální frontu zakládá vlastník prefixu, který ji jmenuje (d.568)
|
|
351
|
+
|
|
352
|
+
Deklarace není pokyn k založení. Zápis sekce v `queueConfig` říká, **jak** fronta
|
|
353
|
+
vypadá; **kdo** ji na brokeru založí, rozhoduje `options.queues` toho, kdo volá
|
|
354
|
+
`initInfrastructureQueues()`. Odvození celého seznamu z prefixů je jen VÝCHOZÍ větev
|
|
355
|
+
té funkce (`shared/infrastructure-tools/src/orchestration/initInfrastructureQueues.js`)
|
|
356
|
+
a žádný dnešní volající jí nenechá rozhodnout: gateway jmenuje přesně pět front
|
|
357
|
+
`workflow.*`, které vlastní (`infra/api_gateway/index.js`). `monitoring.dlq`
|
|
358
|
+
a `telemetry.dlq` tedy patří do volání monitoringu, ne gateway.
|
|
359
|
+
|
|
360
|
+
INFRA-monitoring to změřilo 2026-09-16 na dev brokeru: 78 front, `.dlq` mezi nimi jen
|
|
361
|
+
`workflow.dlq` a `delivery.dlq`. Sedm front rodin `monitoring.*` a `telemetry.*` nese od
|
|
362
|
+
d.475 `x-dead-letter-routing-key: '<prefix>.dlq'`, takže `nack(requeue=false)` nad nimi
|
|
363
|
+
směřuje na routing key, pod kterým žádná fronta není — a broker zprávu zahodí.
|
|
364
|
+
Terminál založí vlastník prefixu u sebe (INFRA-monitoring, W9c).
|
|
365
|
+
|
|
366
|
+
Opraveny věty v živém textu: JSDoc `monitoring.dlq` a `telemetry.dlq`
|
|
367
|
+
(`src/config/queueConfig.js`), úroveň § terminální fronty v `README.md` a hlavička
|
|
368
|
+
testu `tests/unit/dead-letter-topology.test.js`, která tvrdila, že zápis sekce JE pokyn
|
|
369
|
+
k založení. Dobový záznam v `[3.2.0]` se nemění. Deklarace sama je v pořádku a nově to
|
|
370
|
+
měří test: `declarationOptions('monitoring.dlq')` a `('telemetry.dlq')` vrací přesně
|
|
371
|
+
`{ durable: true, arguments: { 'x-max-length': 50000 } }` — žádné TTL, žádná další
|
|
372
|
+
cesta — s kontrolním případem `workflow.dlq` (totéž) a `monitoring.audit` (routovaná
|
|
373
|
+
fronta též rodiny si TTL, strop i cestu drží). Žádná změna produkčního kódu.
|
|
374
|
+
|
|
7
375
|
## [3.2.0] — 2026-09-15
|
|
8
376
|
|
|
9
377
|
### Added — dead-letter cesty pro rodiny `monitoring.*` a `telemetry.*` (d.475)
|
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
|
|
@@ -195,7 +198,12 @@ Owner decision: `api/docs/governance/confirmations/connector-logger-contract.md`
|
|
|
195
198
|
connection (see § Connection loss)
|
|
196
199
|
|
|
197
200
|
`publish()` and the workflow helpers built on it resolve to `undefined`; failure is
|
|
198
|
-
signalled by a thrown `PublishError`, never by a falsy return.
|
|
201
|
+
signalled by a thrown `PublishError`, never by a falsy return. That `PublishError`
|
|
202
|
+
**quotes the reason it caught** — `[BaseClient] Failed to publish to queue "<q>":
|
|
203
|
+
<the transport's own sentence> - Expected: … Fix: …` — and keeps the object it came
|
|
204
|
+
from in `error.cause`, so a caller deciding on the class still reads the object
|
|
205
|
+
while a log line already carries the reason. The layer names no likeliest cause of
|
|
206
|
+
its own; the same rule `consume()` follows (§ `errors` and `errorCodes`).
|
|
199
207
|
|
|
200
208
|
### `consume()` — the delivery contract
|
|
201
209
|
|
|
@@ -235,6 +243,17 @@ policy exists to end) and it is never silent: the client logs it at error level,
|
|
|
235
243
|
the queue, the attempts and the error, and the `message_dlq` event is published as for any
|
|
236
244
|
other rejection.
|
|
237
245
|
|
|
246
|
+
**A terminal queue is declared here and CREATED by the owner of the prefix that names
|
|
247
|
+
it.** A section entry says what the queue looks like, never who brings it into being:
|
|
248
|
+
`initInfrastructureQueues()` derives the full list from the sections only when its caller
|
|
249
|
+
passes no `queues` list, and every caller passes one — the gateway's names the five
|
|
250
|
+
`workflow.*` queues it owns and nothing else (`infra/api_gateway/index.js`). So
|
|
251
|
+
`monitoring.dlq` and `telemetry.dlq` belong to the monitoring service's own call. Until a
|
|
252
|
+
terminal exists on the broker, the drop above happens one step earlier and without the
|
|
253
|
+
`x-death` trail: the rejection is routed to a name no queue carries (measured 2026-09-16
|
|
254
|
+
on the dev broker — 78 queues, `.dlq` among them only `workflow.dlq` and
|
|
255
|
+
`delivery.dlq`).
|
|
256
|
+
|
|
238
257
|
```javascript
|
|
239
258
|
await client.consume(queue, async (msg, delivery) => {
|
|
240
259
|
// delivery = { attempt, maxAttempts, isFinalAttempt }
|
|
@@ -312,6 +331,39 @@ There is no third meaning. `requeueOnError: false` together with `maxAttempts`
|
|
|
312
331
|
above 1 is a contradiction and is **refused where it was written**, rather than
|
|
313
332
|
one of the two being silently ignored.
|
|
314
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
|
+
|
|
315
367
|
### Connection loss — the contract
|
|
316
368
|
|
|
317
369
|
When the broker goes away, the transport runs connection-level recovery with
|
|
@@ -459,9 +511,12 @@ arguments no longer match. The rule is enforced in two places, and both are
|
|
|
459
511
|
required: the 404 branch of `_publishOnce()` refuses the publish, and
|
|
460
512
|
`RecoveryWorker.handleQueueNotFound()` refuses to create the queue behind it.
|
|
461
513
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
514
|
+
Which of the three sentences the refusal carries is decided by the ownership class
|
|
515
|
+
in the error (`kind`), which the central declaration answers — never by how the
|
|
516
|
+
client was constructed. `recoveryScope` used to say which kind of service a client
|
|
517
|
+
belonged to; it never meant "may create business queues", and once d.419 closed the
|
|
518
|
+
door it had guarded, nothing read it. The key is retired (d.514) and a config still
|
|
519
|
+
carrying it is refused by name at construction.
|
|
465
520
|
|
|
466
521
|
**An exchange is named by a publisher, never declared by one.** `publish(queue,
|
|
467
522
|
message, { exchange, routingKey })` sends to the exchange the caller names and
|
|
@@ -521,12 +576,28 @@ for `'Connection closed unexpectedly'` while the transport emitted `'RabbitMQ co
|
|
|
521
576
|
closed unexpectedly'`, one capital letter apart, so the branch written to ignore that
|
|
522
577
|
error had never once run (fixed 2026-09-08, `errors.ConnectionError` now carries `code`).
|
|
523
578
|
|
|
524
|
-
`consume()` refuses for
|
|
579
|
+
`consume()` refuses for three named reasons, and they ask for OPPOSITE actions:
|
|
525
580
|
|
|
526
581
|
| `error.code` | what happened | what fixes it |
|
|
527
582
|
|---|---|---|
|
|
528
583
|
| `CONSUMER_QUEUE_MISSING` | the queue does not exist | start the service that owns the queue — a consumer never creates one |
|
|
529
584
|
| `CONSUMER_DEAD_LETTER_ROUTE_MISSING` | the queue exists, `queueConfig` declares no dead-letter route for it | declare `x-dead-letter-exchange`/`-routing-key` for that queue and bind the destination; the queue itself is fine |
|
|
585
|
+
| `MQ_CONNECTION_FATAL` (with `reason: 'broker-refused'`) | the BROKER refused this queue — 406 PRECONDITION-FAILED for a queue that exists with other arguments, 403/530 for one this account may not use | correct the topology or the permission the broker named in `error.cause`, then restart; retrying gets the same answer |
|
|
586
|
+
|
|
587
|
+
The third one is the SAME verdict a refused connection carries (§ *permanently lost*),
|
|
588
|
+
because it asks the lifecycle owner for the same thing: stop retrying. It refuses the
|
|
589
|
+
QUEUE, not the connection — the client stays connected and `isConnectionFatal()` stays
|
|
590
|
+
`false`. `@onlineapps/service-wrapper` reads `code` to end FÁZE 0.7 permanently; before
|
|
591
|
+
2026-09-16 a 406 on a queue carried no code at all, so the service kept booting against a
|
|
592
|
+
queue it will never be allowed to declare.
|
|
593
|
+
|
|
594
|
+
**`assertQueue()` answers the same way**, and for the same reason: a service declaring
|
|
595
|
+
its own business queues meets the refusal there, not on the consume path. A declaration
|
|
596
|
+
the broker refuses (403/530/406) leaves that rail as an error carrying
|
|
597
|
+
`code: 'MQ_CONNECTION_FATAL'`, `reason: 'broker-refused'`, the queue name in `queue` and
|
|
598
|
+
the broker's own error — reply code included — in `cause`. One function states both
|
|
599
|
+
(`_asRefusal(kind, queue, err)`): two copies of one classification would be free to
|
|
600
|
+
drift. Anything the broker did not refuse leaves either rail exactly as it arrived.
|
|
530
601
|
|
|
531
602
|
Any other failure carries **no** code and its `ConsumeError` says so: the reason is in
|
|
532
603
|
`error.cause`, and this layer does not name the likeliest-sounding one. Until 2026-09-14
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/mq-client-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
|
|
5
5
|
"oa": {
|
|
6
6
|
"category": "connector"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@onlineapps/logger-contract": "2.0.0",
|
|
27
27
|
"@onlineapps/runtime-config": "1.2.0",
|
|
28
28
|
"ajv": "^8.12.0",
|
|
29
|
-
"amqplib": "
|
|
29
|
+
"amqplib": "0.10.9"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"jest": "^29.7.0"
|
package/src/BaseClient.js
CHANGED
|
@@ -196,6 +196,24 @@ function invalidConfigurationMessage(unknownKeys, declaredKeys) {
|
|
|
196
196
|
* @throws {ValidationError} If the argument is not a plain object of properties, or
|
|
197
197
|
* redeclares a key the library already owns.
|
|
198
198
|
*/
|
|
199
|
+
/**
|
|
200
|
+
* What was thrown, rendered for a sentence a human reads in a log line.
|
|
201
|
+
*
|
|
202
|
+
* Neither branch invents anything: an `Error` is quoted by its own `message`,
|
|
203
|
+
* and a value that is not an `Error` is quoted whole, because "undefined" in the
|
|
204
|
+
* middle of a refusal is a worse answer than the string somebody rejected with.
|
|
205
|
+
* The object itself always stays reachable as `error.cause`.
|
|
206
|
+
*
|
|
207
|
+
* @param {*} thrown - Whatever the transport rejected with.
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
function describeThrown(thrown) {
|
|
211
|
+
if (thrown instanceof Error && typeof thrown.message === 'string' && thrown.message !== '') {
|
|
212
|
+
return thrown.message;
|
|
213
|
+
}
|
|
214
|
+
return String(thrown);
|
|
215
|
+
}
|
|
216
|
+
|
|
199
217
|
function schemaWithSubclassProperties(extraProperties) {
|
|
200
218
|
if (extraProperties === null || extraProperties === undefined) {
|
|
201
219
|
return configSchema;
|
|
@@ -395,6 +413,18 @@ class BaseClient {
|
|
|
395
413
|
this._logger.info('[BaseClient] Transport connected successfully');
|
|
396
414
|
this._connected = true;
|
|
397
415
|
} catch (err) {
|
|
416
|
+
// A refusal is already answered, so it passes through untouched. The
|
|
417
|
+
// transport reached the verdict (`code: 'MQ_CONNECTION_FATAL'`,
|
|
418
|
+
// `reason: 'broker-refused'`) and wrote the sentence that names
|
|
419
|
+
// credentials, vhost and topology as the fix; wrapping it here would
|
|
420
|
+
// replace that with "read error.cause" — a precise answer demoted to a
|
|
421
|
+
// vague one, and the marker the caller stops retrying on lost with it
|
|
422
|
+
// (`ConnectionError` takes its code as a constructor argument and this
|
|
423
|
+
// call passed none, so the wrap erased it). d.295.
|
|
424
|
+
if (err && err.code === 'MQ_CONNECTION_FATAL') {
|
|
425
|
+
throw err;
|
|
426
|
+
}
|
|
427
|
+
|
|
398
428
|
throw new ConnectionError(
|
|
399
429
|
'[BaseClient] Failed to connect to broker - Expected: a reachable broker at the configured host. '
|
|
400
430
|
+ 'Fix: read error.cause for the transport reason, then check the MQ url, credentials and network the service was started with.',
|
|
@@ -476,9 +506,23 @@ class BaseClient {
|
|
|
476
506
|
try {
|
|
477
507
|
await this._transport.publish(queue, buffer, options);
|
|
478
508
|
} catch (err) {
|
|
509
|
+
// The reason is QUOTED, never guessed. Until d.514 this sentence read
|
|
510
|
+
// "Expected: the queue to exist and the channel to be open", and two of its
|
|
511
|
+
// three clauses were a guess: the transport refuses a publish for reasons
|
|
512
|
+
// that have nothing to do with a missing queue or a closed channel — the
|
|
513
|
+
// ownership rule (`QueueNotFoundError`, `kind: 'unowned'`, d.419), a spent
|
|
514
|
+
// retry budget, a broker refusal — and for each of those the reader was sent
|
|
515
|
+
// to check a queue that exists and a channel that is open. The true sentence
|
|
516
|
+
// sat in `error.cause`, where a log line does not look. Same defect, same
|
|
517
|
+
// answer as d.297 one call over (`_consumerRefusal()`): name no likeliest
|
|
518
|
+
// cause, carry the one actually caught. `cause` is unchanged, so a caller
|
|
519
|
+
// deciding on the class still reads the object.
|
|
479
520
|
throw new PublishError(
|
|
480
|
-
`[BaseClient] Failed to publish to queue "${queue}"
|
|
481
|
-
+ '
|
|
521
|
+
`[BaseClient] Failed to publish to queue "${queue}": ${describeThrown(err)} - `
|
|
522
|
+
+ 'Expected: publish() to hand the message to the broker. '
|
|
523
|
+
+ 'Fix: act on the reason above — this layer does not know which failure it was, and naming the '
|
|
524
|
+
+ 'likeliest would send you to fix something that may not be broken. error.cause carries the '
|
|
525
|
+
+ 'original error, error.queue names the target queue.',
|
|
482
526
|
queue,
|
|
483
527
|
err
|
|
484
528
|
);
|
|
@@ -623,6 +667,18 @@ class BaseClient {
|
|
|
623
667
|
_consumerRefusal(queue, err) {
|
|
624
668
|
const code = err && err.code;
|
|
625
669
|
|
|
670
|
+
// The broker ANSWERED and refused this queue (403/530/406). Rethrown
|
|
671
|
+
// UNCHANGED, for the reason `@onlineapps/service-wrapper`'s `MQConnector`
|
|
672
|
+
// gives one layer up: that message carries the broker's own words and names
|
|
673
|
+
// the permission or the topology as the fix, and the verdict it carries
|
|
674
|
+
// (`code: 'MQ_CONNECTION_FATAL'`, `reason: 'broker-refused'`) is what the
|
|
675
|
+
// lifecycle owner reads to stop retrying a decision. Re-wrapping it here
|
|
676
|
+
// would put a sentence that knows less in front of one that knows more, and
|
|
677
|
+
// bury the marker one `cause` deeper (d.577).
|
|
678
|
+
if (code === 'MQ_CONNECTION_FATAL') {
|
|
679
|
+
return err;
|
|
680
|
+
}
|
|
681
|
+
|
|
626
682
|
if (code === CONSUMER_DEAD_LETTER_ROUTE_MISSING) {
|
|
627
683
|
return new ConsumeError(
|
|
628
684
|
`[BaseClient] Cannot consume from queue "${queue}": queueConfig declares no dead-letter route for it - `
|
|
@@ -1069,9 +1125,14 @@ class BaseClient {
|
|
|
1069
1125
|
}
|
|
1070
1126
|
|
|
1071
1127
|
/**
|
|
1072
|
-
* 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()`).
|
|
1073
1134
|
*
|
|
1074
|
-
* @returns {Object} `{ tracked: <count>, active: [<queue names>] }`
|
|
1135
|
+
* @returns {Object} `{ tracked: <count>, active: [<attached queue names>] }`
|
|
1075
1136
|
* @throws {ConnectionError} Before `connect()` has built a transport.
|
|
1076
1137
|
*/
|
|
1077
1138
|
getConsumerState() {
|