@onlineapps/mq-client-core 5.1.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +329 -0
- package/README.md +74 -12
- package/package.json +2 -2
- package/src/BaseClient.js +80 -66
- package/src/buffer/InMemoryBuffer.js +66 -3
- package/src/buffer/MessageBuffer.js +3 -0
- package/src/config/configSchema.js +3 -13
- package/src/config/defaultConfig.js +3 -5
- package/src/config/queueConfig.js +35 -1
- package/src/index.js +22 -0
- package/src/layers/PublishLayer.js +95 -18
- package/src/transports/rabbitmqClient.js +366 -76
- package/src/utils/errorHandler.js +25 -0
- package/src/utils/publishErrors.js +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,335 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [6.0.0] — 2026-10-12
|
|
8
|
+
|
|
9
|
+
### Added — `topology.workflowObserverCopyQueues`: pozorovatelské kopie běhu mají jeden seznam (d.1336, INFRA 559)
|
|
10
|
+
|
|
11
|
+
Které fronty jsou „pozorovatelské kopie běhu“ (fronty na `monitoring.workflow.fanout` pro čtenáře,
|
|
12
|
+
který běh sleduje a není jeho součástí; dead letter z nich = ztracená kopie telemetrie, ne ztracený
|
|
13
|
+
běh) se vybíralo ručně na dvou místech — monitoring `src/consumer/topology.js` `OBSERVER_COPY_QUEUES`
|
|
14
|
+
a plánovač `config/config.js` `observerCopyQueues`. Obsah se shoduje (změřeno hodnotami):
|
|
15
|
+
`monitoring.workflow`, `delivery.workflow.events`, `scheduler.workflow.events`.
|
|
16
|
+
|
|
17
|
+
- `queueConfig` deklaruje seznam jednou (`WORKFLOW_OBSERVER_COPIES`, páry `[section, key]`) — odvodit
|
|
18
|
+
ho z deklarace nejde: vazbu na fanout dělá každý konzument sám při bootu (`bindQueueToFanout()`), na
|
|
19
|
+
položce je jen komentářem, a TTL + dead-letter route nese každá rodina. Nový pozorovatel = jeden
|
|
20
|
+
řádek vedle své deklarace.
|
|
21
|
+
- `queueConfig.workflowObserverCopyQueues()` a `topology.workflowObserverCopyQueues` vrací zmrazené pole
|
|
22
|
+
jmen složených `queueName()` (žádný literál; neznámá položka odmítnuta `ValidationError`).
|
|
23
|
+
- Komentář `monitoring.workflow.fanout` („both receive copies“ — byly tři) ukazuje na seznam.
|
|
24
|
+
- Deklarace front beze změny.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Tests — management API má v IT tieru jeden čtenář: `createMgmtAsk` ze `scripts/tests/sweepOrphanQueues.js` (d.1330)
|
|
28
|
+
|
|
29
|
+
`tests/vhost.js § mgmt` byl po d.1324/d.1326 poslední vlastní čtenář management API v repu
|
|
30
|
+
(vlastní `fetch`, vlastní mez `MGMT_ASK_TIMEOUT_MS` 5 000 ms, vlastní věta `MGMT_NO_ANSWER`);
|
|
31
|
+
`tests/integration/setup.js` navíc bránu alarmu napojoval adaptérem `{ status, json }` nad ním.
|
|
32
|
+
|
|
33
|
+
- `mgmt(path, init)` je tenké volání sdíleného čtenáře: požadavek, oba rozpočty a věta pro
|
|
34
|
+
management API, které neodpoví (`[mq-client-core/integration] The management API did not
|
|
35
|
+
answer - …` / `… did not finish within 10000ms …`), jsou čtenáře; tento soubor přidává jen
|
|
36
|
+
bránu stavů (`allow`, `MGMT_REFUSED`) a tvar `{ status, body }`, který sady balíčku čtou
|
|
37
|
+
(devět IT sad + `mgmtRead`/`mgmtWrite` beze změny). `MGMT_ASK_TIMEOUT_MS`, `MGMT_NO_ANSWER`
|
|
38
|
+
a `config.rabbitmq.mgmtAuthHeader` jsou pryč (bez čtenáře).
|
|
39
|
+
- `tests/config.js` dodává `timeouts: { connection: 5000, operation: 10000 }` (hodnoty d.1324 /
|
|
40
|
+
registry d.744b, důvod u klíče) a `tier: 'mq-client-core/integration'`; `rabbitmq` nese
|
|
41
|
+
`user`/`password` pro čtenáře.
|
|
42
|
+
- `setup.js`: brána `refuseBrokerUnderAlarm` dostává `ask: createMgmtAsk(...)` přímo — adaptér
|
|
43
|
+
odešel.
|
|
44
|
+
- Sonda `tests/unit/mgmt-reader-has-one-rail.test.js`: žádný vlastní `fetch(` v `tests/**`,
|
|
45
|
+
`setup.js` bez adaptéru a s `createMgmtAsk(`, `vhost.js` requiruje sdíleného čtenáře; chování
|
|
46
|
+
`mgmt()` nad atrapou čtenáře (tvar, `allow`, `MGMT_REFUSED`, chyba čtenáře prochází netknutá).
|
|
47
|
+
`src/**` netknuto.
|
|
48
|
+
|
|
49
|
+
### Tests — brána „broker pod alarmem“ IT tieru má jedno tělo v repu (d.1326)
|
|
50
|
+
|
|
51
|
+
- `tests/brokerAlarm.js` (`refuseBrokerUnderAlarm(ask)`, d.807 — „one concern, two
|
|
52
|
+
implementations, on purpose and on record“ do dne, kdy bude sdílené místo) odstraněn; sdílené
|
|
53
|
+
místo je `api/scripts/tests/brokerAlarm.js` vedle `sweepOrphanQueues.js` (d.1320). Čtenář
|
|
54
|
+
zůstává kolej tohoto balíčku `tests/vhost.js` § `mgmt` — `integration/setup.js` ji předává
|
|
55
|
+
s `allow: [503]` a v tvaru Response (`status`, `json()`), který sdílené tělo čte.
|
|
56
|
+
- `tests/unit/broker-alarm-gate.test.js` míří na sdílené tělo a tvrdí totéž (resource, node,
|
|
57
|
+
status, jedna otázka `/health/checks/alarms`, nedokumentovaný tvar = „unreported“, bez `ask`
|
|
58
|
+
odmítnuto jménem) + nově: mlčící čtenář = jeho vlastní chyba projde beze změny (žádný vymyšlený
|
|
59
|
+
alarm), HTTP mimo 200/503 = „did not answer the alarm check“. Tvrzení `init.allow === [503]`
|
|
60
|
+
odešlo z unit testu do adaptéru v `setup.js` (detail koleje, ne pravidla).
|
|
61
|
+
|
|
62
|
+
Příští MAJOR (vlna 5). Tři nálezy INFRA z diagnózy dev plánovače (W224, INFRA 531, 5. 10. 2026):
|
|
63
|
+
N3 je odebrání (BREAKING), N1 a N2 jsou opravy (d.1275).
|
|
64
|
+
|
|
65
|
+
### Added — `topology.infrastructureHealthChecksQueue` = `infrastructure.health.checks` (d.1307)
|
|
66
|
+
|
|
67
|
+
Nejčtenější jméno platformy — fronta, na kterou každá infra služba publikuje health checky a
|
|
68
|
+
kterou odebírá registry (`docs/standards/queue-ownership.md` § Infrastructure Queue Creation
|
|
69
|
+
Order) — mělo deklaraci tady (`queueConfig.infrastructure['health.checks']`), ale žádný složený
|
|
70
|
+
export: `topology` (d.270) nesl `health.events`, monitoring a delivery, ne `health.checks`.
|
|
71
|
+
Jméno proto žije podruhé jako literál v `@onlineapps/service-common` `src/defaults.js`
|
|
72
|
+
(`infrastructureHealthQueueName`), který registry, health publisher i wrapper čtou a v hláškách
|
|
73
|
+
označují za „one owner“ — dva deklarující vlastníci jednoho jména na drátě (nález d.1306).
|
|
74
|
+
|
|
75
|
+
- `topology.infrastructureHealthChecksQueue` je složené z deklarace
|
|
76
|
+
(`queueConfig.queueName('infrastructure', 'health.checks')`), ne přepsané: přejmenování
|
|
77
|
+
v `queueConfig` export přesune, smazaný záznam padne rychle; objekt zůstává zmrazený.
|
|
78
|
+
- Čtenář zatím žádný — `@onlineapps/service-common` / `@onlineapps/infrastructure-tools` se
|
|
79
|
+
přepnou podle rozhodnutí vlastníka A/B (d.1306: service-common cituje export × čtenáři
|
|
80
|
+
citují export přímo a klíč service-common zaniká). Export je krok společný oběma cestám.
|
|
81
|
+
- Žádná změna chování transportu; test `tests/unit/queueConfig-public-export.test.js`
|
|
82
|
+
(hodnota, složení z deklarace a nepřítomnost literálu v `src/index.js`, argumenty fronty,
|
|
83
|
+
kontrola zmrazení a nezměněných jmen). README § topology — řádek tabulky.
|
|
84
|
+
|
|
85
|
+
### Removed — BREAKING — `criticalHealthShutdown`, `criticalHealthShutdownDelay` a událost `health:shutdown` (d.1275, N3)
|
|
86
|
+
|
|
87
|
+
Slovo vlastníka 2026-10-06 (dialog 7 (4)): „volbu i událost ZRUŠIT, žádný slib — o životě
|
|
88
|
+
procesu rozhoduje wrapper/služba“.
|
|
89
|
+
|
|
90
|
+
Co to znamená pro volajícího — hodnotami:
|
|
91
|
+
|
|
92
|
+
- `new BaseClient({ …, criticalHealthShutdown: <cokoli> })` i `criticalHealthShutdownDelay`
|
|
93
|
+
hází `ValidationError` `[BaseClient] Configuration key "criticalHealthShutdown" does not exist
|
|
94
|
+
- Expected: no such key; a shutdown on sustained critical health is not something this client
|
|
95
|
+
has. Fix: remove "criticalHealthShutdown" from the configuration. …` — odmítnutí jménem
|
|
96
|
+
kolejí `NOT_A_KEY_OF_THIS_CLIENT`, jako `persistentBufferEnabled` (d.343); ani `false` se
|
|
97
|
+
nepřijme, protože už není co vypínat;
|
|
98
|
+
- `defaultConfig` ani schéma oba klíče nedeklarují;
|
|
99
|
+
- transport po žádné délce kritického zdraví neemituje `health:shutdown` a nepíše řádky
|
|
100
|
+
„Critical health will trigger shutdown in <ms> if not resolved“ ani „… - triggering shutdown“;
|
|
101
|
+
- beze změny: `health:critical` a `healthCriticalCallback` při každé selhané kontrole, řádky
|
|
102
|
+
„Critical health detected at …“ a „Health recovered after <ms> of critical state“.
|
|
103
|
+
|
|
104
|
+
Čtyři otázky `change-discipline.md` § Removing:
|
|
105
|
+
|
|
106
|
+
1. **Proč vznikl a co měl dělat.** Volba přišla s monitorováním zdraví transportu: po
|
|
107
|
+
`criticalHealthShutdownDelay` (60 s) kritického zdraví emitovat `health:shutdown`, aby
|
|
108
|
+
aplikace proces ukončila a orchestrátor kontejneru ho nahodil znovu. Výchozí `true`.
|
|
109
|
+
2. **Která část konceptu ji nesla a platí ještě.** Žádná živá: koncept životního cyklu říká
|
|
110
|
+
opak — knihovna proces neukončuje, ukončení je rozhodnutí vlastníka životního cyklu
|
|
111
|
+
(`_failFatally()` v `transports/rabbitmqClient.js`; trvalou ztrátu spojení hlásí
|
|
112
|
+
`connection:fatal` + injektovaný `onFatal`). `docs/architecture/rabbitmq-channel-lifecycle.md`
|
|
113
|
+
ji popisuje jako knob, ne jako rozhodnutí.
|
|
114
|
+
3. **Proč ji dnes nic nečte.** Ustoupila rozhodnutí: `@onlineapps/service-wrapper` posluchače
|
|
115
|
+
`health:shutdown` odebral 2026-09-07 (jeho CHANGELOG § „Removed — the MQ `health:shutdown`
|
|
116
|
+
policy that could never apply“), žádná infra služba ho nikdy neměla — změřeno 2026-10-06
|
|
117
|
+
`git grep -n -e "health:shutdown" -e "criticalHealthShutdown"`: mimo tento balíček jen
|
|
118
|
+
komentáře a testy wrapperu, dvě věty v `docs/` a záznamy `TODO.md`, žádný kód. Výchozí `true`
|
|
119
|
+
tak každé kritické okno každého klienta zakončil slibem, který nikdo nesplnil (INFRA 531:
|
|
120
|
+
plánovač na devu hlásil „will trigger shutdown“, proces běžel dál — správně).
|
|
121
|
+
4. **Je náhrada koncepčnější.** Ano: jedna kolej pro „proces končí“ (`onFatal` /
|
|
122
|
+
`connection:fatal`, vlastník životního cyklu rozhoduje), a pro trvalé kritické zdraví
|
|
123
|
+
hlášení bez rozhodnutí (`health:critical`, `healthCriticalCallback`). Slib bez vykonavatele
|
|
124
|
+
koncepčnější není nikdy.
|
|
125
|
+
|
|
126
|
+
### Fixed — `consumers.active` se počítá z vlastního stavu klienta, ne z toho, zda doběhla sonda fronty (d.1275, N1)
|
|
127
|
+
|
|
128
|
+
Dosud se `active` přičítalo jen uvnitř smyčky `checkQueue` v `_performHealthCheck()`. Mrtvý
|
|
129
|
+
frontový (queue) kanál smyčku vůbec nespustil a `checkQueue` bez odpovědi v `brokerAnswerTimeout`
|
|
130
|
+
ji ukončil — v obou případech `active: 0` a verdikt „No consumer of this client is attached …
|
|
131
|
+
this client takes no message off any queue“ o klientovi, jehož consumer kanál žil a přijímal
|
|
132
|
+
(W224 na dev plánovači 5. 10.: spojení i consumer kanál žily, queue kanál thrashoval).
|
|
133
|
+
|
|
134
|
+
- „Připojený konzument“ má jednu definici `_isConsumerAttached()`: consumer kanál je živý
|
|
135
|
+
**a** záznam nese tag brokera. Čtou ji `performHealthCheck()` i `getConsumerState()` (d.613),
|
|
136
|
+
takže se nemění jen zdraví: **`getConsumerState().active` je při mrtvém consumer kanálu `[]`**
|
|
137
|
+
(dosud vypisoval frontu, dokud záznam držel tag kanálu, který už neexistuje).
|
|
138
|
+
- `consumers.active` / `consumers.inactive` se počítají z toho před sondou, nezávisle na ní.
|
|
139
|
+
Mrtvý queue kanál je nález kanálu (`Queue channel is closed or missing`), neúspěšná sonda
|
|
140
|
+
nález sondy (`Error checking queue <q>: …`); věta „No consumer …“ jen když opravdu žádný
|
|
141
|
+
konzument připojený není.
|
|
142
|
+
- Nové pole `queues.consumers`: `{ <fronta>: <consumerCount brokera> | 'unknown' }` — počet
|
|
143
|
+
konzumentů na frontě podle brokera (všech, ne jen tohoto klienta), `'unknown'` kde sonda
|
|
144
|
+
neběžela nebo nedostala odpověď. Nikdy 0 za „neptal jsem se“.
|
|
145
|
+
- Issue u fronty bez připojeného konzumenta jmenuje příčinu: `(consumerTag missing)` při živém
|
|
146
|
+
consumer kanálu, `(consumer channel is not alive)` při mrtvém.
|
|
147
|
+
|
|
148
|
+
### Fixed — buffer publishe expiruje podle vlastních hodin a `PublishError` neslibuje „not lost“ (d.1275, N2)
|
|
149
|
+
|
|
150
|
+
- Záznam bufferu se zahodí **v okamžiku svého TTL** (`publishBufferTtlMs`) jedním časovačem
|
|
151
|
+
bufferu, ne až při příštím `add()`/`flush()`/`getBufferState()`. Změřeno na dev plánovači:
|
|
152
|
+
stáří při líné expiraci 584 843 ms a 5 267 969 ms místo 300 000. Časovač běží po koleji
|
|
153
|
+
časovačů klienta (`_setTimeout()`/`_clearTimeout()`, handle v `_activeTimers`), takže ho
|
|
154
|
+
`disconnect()` ukončí; prázdný buffer žádný časovač nedrží. `InMemoryBuffer`, `MessageBuffer`
|
|
155
|
+
a `PublishLayer` (interní, neexportované) proto vyžadují `timers: { setTimeout, clearTimeout }`
|
|
156
|
+
a bez nich odmítnou konstrukci jménem.
|
|
157
|
+
- ERROR řádek expirace: `sent=false` → `[InMemoryBuffer] <n> buffered message(s) for <cíl> LOST
|
|
158
|
+
after <ms> ms (buffer TTL <ttl> ms; …)`, `sent=true` → `… expired after <ms> ms …` (broker ji
|
|
159
|
+
mohl doručit sám; conf `mq-publish-buffer` 001 — obě věty se nikdy neslučují). Věta `Expected:`
|
|
160
|
+
nově říká, že samotná obnova publisher kanálu nic nepřehraje.
|
|
161
|
+
- `PublishError` z `PublishLayer` s `buffered: true` (vyčerpané pokusy, selhaný reconnect, na
|
|
162
|
+
který retry čekal) už ve `Fix:` neříká „the message is buffered, so it is not lost“, ale: „the message is buffered
|
|
163
|
+
for up to <ttl> ms (publishBufferTtlMs) and replayed only after a reconnect or after the broker
|
|
164
|
+
unblocks the connection — a recovered publisher channel alone does not replay it; if neither
|
|
165
|
+
comes within that time it expires and is reported as LOST …“ (u `sent: true` „reported as an
|
|
166
|
+
expired message whose frame had been sent (the broker may hold a copy)“).
|
|
167
|
+
- **Neudělané (čeká na rozhodnutí):** flush bufferu po obnově samotného publisher kanálu.
|
|
168
|
+
Obnova kanálu na živém spojení je třetí spouštěč, o kterém conf `mq-publish-buffer` 001 nemluví
|
|
169
|
+
(rozhodl jen `reconnected` a `unblocked`), a u záznamu s `sent: true` je volba mezi druhou
|
|
170
|
+
kopií a ztrátou — §1.2 `confirmation-triggers.md`.
|
|
171
|
+
|
|
172
|
+
### Fixed — `consume()`, které čekalo na obnovu, jež nedorazila, dostane chybu toho čekání, ne „queue channel is not available“ (d.1278, INFRA-DOCS ID254)
|
|
173
|
+
|
|
174
|
+
Dosud `_ensureConsumerChannel()` chybu z čekání na obnovu (`_waitForReconnection()`) zapsalo
|
|
175
|
+
jako WARN a odpovědělo `{ reRegistered: 0, failed: n }`, jako by to byl výsledek; `consume()`
|
|
176
|
+
pokračovalo bez kanálu a volající dostal odmítnutí koleje frontového kanálu „Cannot checkQueue
|
|
177
|
+
<q>: queue channel is not available … Fix: await client.connect()“ — bez `cause`, bez jména
|
|
178
|
+
cyklu, a verdikt fatálního cyklu (`MQ_CONNECTION_FATAL`) dostal jen `onFatal`.
|
|
179
|
+
|
|
180
|
+
- `_ensureConsumerChannel()` po neúspěšném čekání **hází** `ConnectionError` `[RabbitMQClient]
|
|
181
|
+
Cannot recreate the consumer channel: the recovery it waited for did not arrive - <věta
|
|
182
|
+
chyby čekání> Expected: … Fix: read error.cause for the verdict of that recovery …` — stejně
|
|
183
|
+
jako publisher kolej (`_ensurePublisherChannel()`) chybu čekání propouští.
|
|
184
|
+
- `error.cause` je **tatáž** chyba, kterou cyklus vydal, ne kopie: po posledním cyklu objekt s
|
|
185
|
+
`code: 'MQ_CONNECTION_FATAL'`, který už dostal `onFatal` (volán právě jednou — hození není
|
|
186
|
+
druhé hlášení); po vyčerpaném cyklu `ConnectionError` jmenující cyklus (`recovery cycle 2/3
|
|
187
|
+
spent its 2 attempts …`) **bez `code`** — klient si `MQ_CONNECTION_STANDBY` nechává
|
|
188
|
+
(`docs/standards/rabbitmq-channel-best-practices.md` § Recovery).
|
|
189
|
+
- Přes `BaseClient.consume()` je to `ConsumeError` → `cause` = ta `ConnectionError` → `cause.cause`
|
|
190
|
+
= verdikt cyklu.
|
|
191
|
+
- Beze změny: `consume()` na živém kanálu; `consume()` po návratu brokeru spustí další cyklus a
|
|
192
|
+
konzumenta připojí (líná polovina obnovy, d.339); cesta `onFatal`.
|
|
193
|
+
|
|
194
|
+
### Fixed — `consume()` na klientovi, který obnovu nespustí, řekne proč — ne „queue channel is not available“ (d.1281, nález d.1278)
|
|
195
|
+
|
|
196
|
+
Zbylé dvě větve `_ensureConsumerChannel()`, které po d.1278 ještě vracely `{ reRegistered: 0,
|
|
197
|
+
failed: n }` (volající pak dostal odmítnutí frontového kanálu „… queue channel is not available
|
|
198
|
+
… Fix: await client.connect()“ bez `cause`), **hází** `ConnectionError` jmenující stav:
|
|
199
|
+
|
|
200
|
+
- spojení trvale ztracené (`isConnectionFatal()`): `… the connection is permanently lost and no
|
|
201
|
+
recovery may start - <věta verdiktu> …`; `error.cause` je **tentýž objekt**, který dostal
|
|
202
|
+
`onFatal` / `connection:fatal` (`code: 'MQ_CONNECTION_FATAL'`) — klient si ho nově drží
|
|
203
|
+
(`_connectionFatalError`), `onFatal` se nevolá podruhé;
|
|
204
|
+
- zavřeno volajícím (`disconnect()`): `… the caller closed this client (disconnect()) and no
|
|
205
|
+
recovery may start - … Fix: await client.connect() before consume(); only an explicit connect()
|
|
206
|
+
re-arms recovery.` — bez `cause`;
|
|
207
|
+
- obnova vypnutá (`reconnectEnabled: false`): `… the connection is closed and recovery is disabled
|
|
208
|
+
by configuration (reconnectEnabled: false) - …` — bez `cause`;
|
|
209
|
+
- čekání skončilo `reconnected`, ale spojení už zase neživé: `… the recovery it waited for
|
|
210
|
+
completed, but the connection is not alive any more - … Fix: run consume() again …` — bez
|
|
211
|
+
`cause` (druhou smrt nic nedrží jako objekt).
|
|
212
|
+
|
|
213
|
+
Beze změny: `consume()` na živém kanálu, `onFatal` cesta, publish cesta.
|
|
214
|
+
|
|
215
|
+
### Fixed — verdikt `MQ_CONNECTION_FATAL` je na chybě z `consume()` čitelný nahoře, ne o `cause` níž (d.1297, nález d.1294)
|
|
216
|
+
|
|
217
|
+
Chyby z d.1278 a d.1281 (`consume()` čekající na obnovu, která skončila fatálně; `consume()` na
|
|
218
|
+
trvale ztraceném klientovi) nesly verdikt cyklu jen jako `cause`. `BaseClient._consumerRefusal()`
|
|
219
|
+
propouští odmítnutí beze změny jen s `code === 'MQ_CONNECTION_FATAL'` na chybě samé (d.577:
|
|
220
|
+
značku nikdy nepohřbít o `cause` hlouběji) — obalilo je tedy `ConsumeError` a vlastník životního
|
|
221
|
+
cyklu, který čte `error.code`, verdikt neviděl.
|
|
222
|
+
|
|
223
|
+
- Transport verdikt **přenáší nahoru** (`_carryVerdict()`, jedna kolej pro obě místa): je-li
|
|
224
|
+
příčina fatální verdikt, chyba nese `code: 'MQ_CONNECTION_FATAL'` a `reason` verdiktu
|
|
225
|
+
(`'cycles-spent'` | `'broker-refused'`); `cause` zůstává tentýž objekt, který dostal `onFatal`.
|
|
226
|
+
- `BaseClient.consume()` takovou chybu hází **beze změny** (`ConnectionError`, ne `ConsumeError`) —
|
|
227
|
+
tak jako každé jiné odmítnutí s verdiktem.
|
|
228
|
+
- Nic nechodí řetězem `cause` (žádný fallback nad pohřbenou značkou): vyčerpaný cyklus (standby),
|
|
229
|
+
`disconnect()` a `reconnectEnabled: false` dál nenesou `code` ani `reason`.
|
|
230
|
+
- Věty `Fix:` obou chyb říkají „read error.code“ místo „read error.cause“.
|
|
231
|
+
|
|
232
|
+
### Changed — BREAKING — `publish()`, jehož čekání na obnovu skončí standby/fatal, zprávu DRŽÍ v bufferu (`buffered: true`) a nese verdikt nahoře (d.1300, B1)
|
|
233
|
+
|
|
234
|
+
Slovo vlastníka 2026-10-06 (dialog 9): „B1 — držet v bufferu, `buffered: true`“ — jedna kolej
|
|
235
|
+
pro „čekání na reconnect selhalo“ se sousední cestou `PublishLayer` (přechodná chyba → čekání na
|
|
236
|
+
běžící reconnect → selhání → držení). Dosud (INFRA-DOCS ID254 bod 1, sonda d.1278): send kanál,
|
|
237
|
+
který pokus nedostal, protože spojení zůstalo dole, hodil chybu čekání syrově — volající dostal
|
|
238
|
+
`PublishError` se `sent: false` a **bez `buffered`**, zpráva držena nebyla.
|
|
239
|
+
|
|
240
|
+
Co se mění pro volajícího — hodnotami:
|
|
241
|
+
|
|
242
|
+
- `publish()` čekající na obnovu, která skončí **standby** (vyčerpaný cyklus) nebo **dokončením
|
|
243
|
+
do mrtvého spojení** — tedy tam, kde na tomto klientovi ještě může přijít `reconnected` —
|
|
244
|
+
odmítne `PublishError` s `buffered: true`, `sent: false`; zpráva je v bufferu a **přehraje se
|
|
245
|
+
po příštím `reconnected` / `unblocked`** (nic jiného ji nepřehrává), TTL `publishBufferTtlMs`
|
|
246
|
+
běží od držení, při expiraci ERROR řádek d.1275 (`LOST after <ms>`). Věta `Fix:` jmenuje stav
|
|
247
|
+
(`recovery cycle 2/3 spent its 2 attempts …`) a pravidla přehrání.
|
|
248
|
+
- Tam, kde `reconnected` už nikdy nepřijde — čekání skončilo **fatálem**, klient je **trvale
|
|
249
|
+
ztracený**, zavřený **`disconnect()`**, nebo má **`reconnectEnabled: false`** — se **nic
|
|
250
|
+
nedrží** (`buffered: false`, věta „NOT buffered: this client will not reconnect on its own …
|
|
251
|
+
the caller holds the only copy“; d.1301 zúžení B1 rozhodnutím leada: držení má smysl jen tam,
|
|
252
|
+
kde přijde `reconnected`; jinak je to ztráta s odkladem hlášená až při TTL). Třída to nese v
|
|
253
|
+
povinném `replayable` (boolean), které transport odvozuje ze stavu po skončení čekání.
|
|
254
|
+
- `bufferOnFailure: false` platí i tady: `buffered: false`, nic se nedrží (orchestrátor,
|
|
255
|
+
`publish_pending`).
|
|
256
|
+
- **Verdikt nahoře** (publishová polovina d.1297): skončí-li obnova fatálně, nese `PublishError`
|
|
257
|
+
z `BaseClient.publish()` i chyba `PublishLayer` `code: 'MQ_CONNECTION_FATAL'` a `reason`
|
|
258
|
+
(`'cycles-spent'` | `'broker-refused'`), `cause` řetěz končí tímtéž objektem, který dostal
|
|
259
|
+
`onFatal`. Standby, `disconnect()` a `reconnectEnabled: false` `code` nenesou (kontrakt).
|
|
260
|
+
- Nová třída `RecoveryEndedPublishError extends TransientPublishError` (export
|
|
261
|
+
`publishErrors.RecoveryEndedPublishError`): co transport hodí z `_ensurePublisherChannel()`,
|
|
262
|
+
když spojení zůstalo dole; `PublishLayer` ji drží okamžitě (žádný retry — další cyklus
|
|
263
|
+
spouští až příští použití klienta) týmž pomocníkem `_failedAfterRecovery()` jako selhané
|
|
264
|
+
čekání retry. Při `publishRetryEnabled: false` se hází tato chyba sama s `buffered`.
|
|
265
|
+
- Událost `publish:failed` nese `reconnectFailed: true`, `retryable: false` — jako u sousední
|
|
266
|
+
cesty; `publish:buffered` při držení.
|
|
267
|
+
- `carryVerdict()` z `utils/errorHandler.js` je jedno tělo přenosu verdiktu (transport,
|
|
268
|
+
`PublishLayer`, `BaseClient.publish()`).
|
|
269
|
+
- Beze změny: sousední cesta (přechodná chyba → čekání na běžící reconnect → selhání) — stejný
|
|
270
|
+
tvar jako dosud; publish na živém spojení; `onFatal` právě jednou.
|
|
271
|
+
|
|
272
|
+
### Fixed — `BaseClient.publish()`/`consume()` po `connection:fatal` odmítá s verdiktem nahoře, ne radou „await client.connect()“ (d.1301, nález d.1294 §4/2)
|
|
273
|
+
|
|
274
|
+
Dosud po fatálu `_connected = false` → `ConnectionError` „Cannot publish/consume: client is not
|
|
275
|
+
connected … Fix: await client.connect()“ bez `code`, ačkoli klient verdikt držel (`_fatalError`) —
|
|
276
|
+
rada nepravdivá (po fatálu rozhoduje vlastník životního cyklu z `onFatal`, ne `connect()`) a
|
|
277
|
+
větve „trvale ztracený“ transportu (d.1281, d.1300) byly přes `BaseClient` nedosažitelné.
|
|
278
|
+
|
|
279
|
+
- `BaseClient` po fatálu volání **pouští do transportu**, který stav zná a odpoví sám (jedna
|
|
280
|
+
kolej, žádná druhá věta): `publish()` → `PublishError` s `code: 'MQ_CONNECTION_FATAL'`,
|
|
281
|
+
`reason`, `buffered: false`, `sent: false`, `cause` řetěz končí objektem z `onFatal`;
|
|
282
|
+
`consume()` → `ConnectionError` s týmž `code`/`reason`, `cause` = tentýž objekt
|
|
283
|
+
(`_consumerRefusal()` ji propouští beze změny). Nikde slovo `connect()` jako rada.
|
|
284
|
+
- Beze změny: před prvním `connect()` a po čistém `disconnect()` zůstává „not connected … Fix:
|
|
285
|
+
await client.connect()“ bez `code` — tam je rada pravdivá. Explicitní `connect()` verdikt
|
|
286
|
+
smaže (staví nový transport).
|
|
287
|
+
|
|
288
|
+
### Fixed — každá operace na brokeru po `connection:fatal` odpovídá verdiktem nahoře, ne radou „await client.connect()“ (d.1314, nález d.1301 §6/2)
|
|
289
|
+
|
|
290
|
+
Rozšíření d.1301 na zbylé strážené metody `BaseClient`: `assertQueue`, `assertExchange`,
|
|
291
|
+
`bindQueue`, `checkQueue`, `purgeQueue`, `deleteQueue`, `cancelConsumer`, `ack`, `nack` (měřeno:
|
|
292
|
+
všech devět po fatálu hlásilo „Cannot <op>: client is not connected … Fix: await client.connect()“
|
|
293
|
+
bez `code`).
|
|
294
|
+
|
|
295
|
+
- `BaseClient`: jediná stráž `_requireUsable(op)` pro všech jedenáct operací (dosud jedenáct kopií);
|
|
296
|
+
odmítá jen před prvním `connect()` a po `disconnect()` — toutéž větou jako dosud; po fatálu
|
|
297
|
+
pouští volání do transportu.
|
|
298
|
+
- Transport: jeden stavitel odmítnutí `_refusalWithoutRecovery(headline, verb)` (dosud jen pro
|
|
299
|
+
consumer kanál) odpovídá i v koleji frontového kanálu (`_requireQueueChannel`: assert/check/
|
|
300
|
+
purge/delete/bind/assertExchange), v `cancelConsumer()` a v `ack()`/`nack()`, je-li spojení
|
|
301
|
+
trvale ztracené: `ConnectionError` s `code: 'MQ_CONNECTION_FATAL'`, `reason`, `cause` = verdikt z
|
|
302
|
+
`onFatal`; věta „… the connection is permanently lost and no recovery may start - …“ místo
|
|
303
|
+
„queue channel is not available … wait for the reconnect“ / „consumer channel is not initialized
|
|
304
|
+
… await client.connect()“.
|
|
305
|
+
- Stavové metody po fatálu nehází a říkají pravdu (beze změny): `isConnected()` `false`,
|
|
306
|
+
`isConnectionFatal()` `true`, `getConsumerState()` `{ tracked, active: [] }`,
|
|
307
|
+
`performHealthCheck()` `healthy: false` s `connection.closed: true`, metriky a stav bufferu.
|
|
308
|
+
|
|
309
|
+
### Fixed — transport: „spojení dole a obnova nemůže začít“ odpovídá na každé koleji touž větou stavu; predikát „obnova může začít“ má jedno tělo (d.1319, nálezy d.1314)
|
|
310
|
+
|
|
311
|
+
- (A) Po `disconnect()` a při `reconnectEnabled: false` (spojení dole) radily přímé koleje transportu
|
|
312
|
+
podle staré věty — frontový kanál „queue channel is not available … wait for the reconnect“,
|
|
313
|
+
`ack`/`nack` „consumer channel is not initialized … await client.connect() and start the consumer“,
|
|
314
|
+
`cancelConsumer` „call cancelConsumer() again once the connection is back“ — ačkoli žádný reconnect
|
|
315
|
+
nepřijde. Teď všechny (assert/check/purge/delete/bind/assertExchange, `cancelConsumer`, `ack`,
|
|
316
|
+
`nack`) odpovídají `_refusalWithoutRecovery()` (tři stavy jako consume d.1281): „… the caller closed
|
|
317
|
+
this client (disconnect()) and no recovery may start - …“ resp. „… recovery is disabled by
|
|
318
|
+
configuration (reconnectEnabled: false) - …“, bez `code` (není fatál). Beze změny: standby
|
|
319
|
+
(cyklus může přijít) dál „wait for the reconnect“; po fatálu verdikt (d.1314). Přes `BaseClient`
|
|
320
|
+
tyto dva stavy dál chytá jeho stráž („not connected … Fix: await client.connect()“).
|
|
321
|
+
- (B) `_recoveryMayStart()` je jediné tělo predikátu „obnova může začít“ (ne fatál, ne `disconnect()`,
|
|
322
|
+
`reconnectEnabled: true`); čtou ho `_resumeRecovery()`, `replayable` v `_recoveryEndedForPublish()`
|
|
323
|
+
a koleje z (A). Chování beze změny.
|
|
324
|
+
|
|
325
|
+
### Fixed — zavření spojení z opuštěného handshaku čeká na `close-ok` nejvýš `brokerAnswerTimeout` (d.1322, položka vlny 5)
|
|
326
|
+
|
|
327
|
+
`_closeAbandonedHandshake()` (handshake dokončený po `connectTimeout`, který klientovi nepatří)
|
|
328
|
+
čekalo na `late.close()` bez rozpočtu: broker, který na close neodpověděl, nechal pozdní spojení,
|
|
329
|
+
jeho socket i heartbeat žít do konce procesu (únik handshaků). Teď jde zavření toutéž kolejí jako
|
|
330
|
+
teardown (`_closeWithinTeardownBudget()` → `_awaitBrokerAnswer()`, jediný rozpočet čekání na broker —
|
|
331
|
+
conf `mq-broker-answer-budget` 001): po `brokerAnswerTimeout` WARN „The broker did not answer the
|
|
332
|
+
abandoned <connect|reconnect> handshake close within the teardown budget (<ms>ms) - …“ a socket s
|
|
333
|
+
heartbeatem se zahodí (`_dropUnansweredConnection()`, jako u napůl otevřeného spojení v `connect()`).
|
|
334
|
+
Časovač rozpočtu je na koleji klienta a s odpovědí se ruší; odmítnuté close hlásí dosavadní WARN.
|
|
335
|
+
|
|
7
336
|
## [5.1.0] — 2026-10-03
|
|
8
337
|
|
|
9
338
|
### Added
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Uniform: [library/connector](../connector/conn-orch-validator/manifests/library.
|
|
|
6
6
|
|
|
7
7
|
Duty sections that apply:
|
|
8
8
|
|
|
9
|
-
- `all`: L-MAIN, L-ENGINES, L-TESTS, L-TEST-SCRIPT, L-PACK-TESTS, L-PINS, L-NO-FILE-RANGE, L-CHANGELOG, L-README, L-README-REGION, L-CONSUMER
|
|
9
|
+
- `all`: L-MAIN, L-ENGINES, L-TESTS, L-TEST-SCRIPT, L-PACK-TESTS, L-PINS, L-NO-FILE-RANGE, L-CHANGELOG, L-PACK-CHANGELOG, L-README, L-README-REGION, L-CONSUMER
|
|
10
10
|
- `connector`: L-CONNECTOR-ENV
|
|
11
11
|
<!-- END GENERATED: library-uniform -->
|
|
12
12
|
|
|
@@ -107,6 +107,14 @@ config → env (`RABBITMQ_MAX_RECONNECT_ATTEMPTS`, `RABBITMQ_MAX_RECONNECT_CYCLE
|
|
|
107
107
|
attempts inside ONE recovery cycle, the second how many cycles there may be — see
|
|
108
108
|
§ Connection loss.
|
|
109
109
|
|
|
110
|
+
**`criticalHealthShutdown` and `criticalHealthShutdownDelay` are refused by name** (removed
|
|
111
|
+
2026-10-06, d.1275, on the owner's word). They made the transport, after a minute
|
|
112
|
+
of critical health, emit `health:shutdown` and log that it was "triggering shutdown" — but this
|
|
113
|
+
library never ends the process (see § Connection loss), and nothing on the platform listened
|
|
114
|
+
for the event, so the line promised an action that never came. The event went with the keys.
|
|
115
|
+
Critical health is reported — `health:critical` and `healthCriticalCallback` on every failing
|
|
116
|
+
check — and what to do about the process is the lifecycle owner's decision (`onFatal`).
|
|
117
|
+
|
|
110
118
|
### A subclass declares its own keys
|
|
111
119
|
|
|
112
120
|
`new BaseClient(config, extraProperties?)`. The second argument is the JSON Schema
|
|
@@ -172,7 +180,17 @@ Owner decision: `api/docs/governance/confirmations/connector-logger-contract.md`
|
|
|
172
180
|
healthy** — `healthy: false`, with an issue naming how many it tracks — because
|
|
173
181
|
such a client takes no message off any queue. A client that registered no
|
|
174
182
|
consumer at all is a publisher and stays healthy; the check's own log line says
|
|
175
|
-
which of the two it is.
|
|
183
|
+
which of the two it is. `consumers.active` / `consumers.inactive` are this
|
|
184
|
+
client's own facts — a consumer is attached while the consumer channel is alive
|
|
185
|
+
and its entry holds the broker's tag (the definition `getConsumerState()` reads)
|
|
186
|
+
— so a dead QUEUE channel or an unanswered queue probe is reported as what it is,
|
|
187
|
+
a channel or probe issue, never as "no consumer" (d.1275). The probe adds what
|
|
188
|
+
only the broker knows: `queues.consumers` maps each tracked queue to the broker's
|
|
189
|
+
`consumerCount` (every consumer on the queue, not only this client's), or to
|
|
190
|
+
`'unknown'` where the probe did not run or got no answer — never `0` for "not
|
|
191
|
+
asked". A failing check emits `health:critical` and calls
|
|
192
|
+
`healthCriticalCallback`; it decides nothing about the process (there is no
|
|
193
|
+
`health:shutdown`, see § Configuration).
|
|
176
194
|
- `assertQueue(queue, options?)` / `checkQueue(queue)` / `purgeQueue(queue)` /
|
|
177
195
|
`deleteQueue(queue, options?)` - Queue operations, on the queue channel.
|
|
178
196
|
`assertQueue()` reads exactly four options — `durable`, `arguments`,
|
|
@@ -458,8 +476,21 @@ Permanently lost means all of this, together:
|
|
|
458
476
|
built on them;
|
|
459
477
|
- no further reconnect attempts are made, by a use or otherwise — the client is
|
|
460
478
|
dead until the process restarts, or until an explicit `connect()`;
|
|
461
|
-
- `publish()`/`consume()`
|
|
462
|
-
|
|
479
|
+
- `publish()`/`consume()` — and every other operation on the broker:
|
|
480
|
+
`assertQueue`, `checkQueue`, `purgeQueue`, `deleteQueue`, `assertExchange`,
|
|
481
|
+
`bindQueue`, `cancelConsumer`, `ack`, `nack` — refuse immediately instead of
|
|
482
|
+
blocking on a recovery that is not coming, with the verdict on the error
|
|
483
|
+
(`code: 'MQ_CONNECTION_FATAL'`, `reason`, the verdict as `cause`); through
|
|
484
|
+
`BaseClient` too, which after the verdict lets the lost client answer for
|
|
485
|
+
itself instead of advising `connect()`. The state readers (`isConnected()`,
|
|
486
|
+
`isConnectionFatal()`, `getConsumerState()`, `getChannelState()`,
|
|
487
|
+
`performHealthCheck()`, the metrics) do not throw: they describe the state. A `consume()`
|
|
488
|
+
that was waiting when the verdict fell, or that is asked of the lost client
|
|
489
|
+
afterwards, is refused with that verdict readable on the error (`code`,
|
|
490
|
+
`reason`) and the verdict itself in `cause`. A `publish()` in the same position
|
|
491
|
+
is refused with a `PublishError` that carries the same `code` and `reason` and
|
|
492
|
+
`buffered: false`: nothing is held, because no `reconnected` will ever come to
|
|
493
|
+
replay it (§ *The publish buffer*) — the caller holds the only copy.
|
|
463
494
|
|
|
464
495
|
**The library never ends the process, and never listens for a signal.** Ending
|
|
465
496
|
it is the lifecycle owner's decision: a service typically logs and exits
|
|
@@ -590,14 +621,27 @@ is permanent (d.1134).
|
|
|
590
621
|
**One call can leave the buffer: `publish(queue, message, { bufferOnFailure: false })`.**
|
|
591
622
|
It is for a caller that keeps the message durably elsewhere and must not have a second
|
|
592
623
|
copy replayed behind its back — the orchestrator writing a `publish_pending` record
|
|
593
|
-
(`docs/governance/confirmations/step-publish-failure.md` 001). On each of the
|
|
624
|
+
(`docs/governance/confirmations/step-publish-failure.md` 001). On each of the four
|
|
594
625
|
transient paths (retries exhausted, `publishRetryEnabled: false`, the reconnect a retry
|
|
595
|
-
waited for failed
|
|
596
|
-
|
|
597
|
-
|
|
626
|
+
waited for failed, and a send channel the attempt could not get because the connection stayed
|
|
627
|
+
down — the recovery it waited for ended in standby or fatal, or none may start: the client is
|
|
628
|
+
permanently lost, closed by `disconnect()`, or has `reconnectEnabled: false`; B1, the owner's
|
|
629
|
+
decision of 2026-10-06) the error `publish()` throws carries `buffered` (boolean): `true` only
|
|
630
|
+
when the buffer holds the message for the next reconnect or `unblocked`, `false` when
|
|
631
|
+
nothing will replay it. A permanent refusal never reaches the buffer and carries no `buffered`. A
|
|
598
632
|
value that is not a boolean is refused before any attempt (`ValidationError`). Retry
|
|
599
633
|
stays the library's either way — the option decides only what happens after it.
|
|
600
634
|
|
|
635
|
+
The fourth path is its own class on the transport, `RecoveryEndedPublishError` (a
|
|
636
|
+
`TransientPublishError`, exported under `publishErrors`): `PublishLayer` holds the message at
|
|
637
|
+
once instead of retrying, because the next use of the client is what starts the next recovery
|
|
638
|
+
cycle. It holds it **only where a `reconnected` can still come** — the class says so in
|
|
639
|
+
`replayable`: after a spent cycle (standby) the message is held; once the client is permanently
|
|
640
|
+
lost, closed by `disconnect()`, or has `reconnectEnabled: false`, nothing will ever replay it, so
|
|
641
|
+
nothing is held and the error says `buffered: false` with the reason. When the recovery ended
|
|
642
|
+
fatally the `PublishError` carries the verdict on top — `code: 'MQ_CONNECTION_FATAL'`, `reason` —
|
|
643
|
+
and the verdict itself at the end of `cause`; a spent cycle carries no code.
|
|
644
|
+
|
|
601
645
|
**Every failed publish says whether its frame left the client: `error.sent`** (boolean, on
|
|
602
646
|
the `PublishError` from `publish()` and on its cause; conf
|
|
603
647
|
`docs/governance/confirmations/mq-publish-buffer.md` 001). `true` when any attempt wrote the
|
|
@@ -611,9 +655,21 @@ happens when the outage ends:
|
|
|
611
655
|
| a reconnect (`reconnected`) | re-sent | re-sent — the old socket and what the broker had not read are gone |
|
|
612
656
|
| the broker unblocking the connection (`connection.unblocked`, end of a memory or disk alarm) | re-sent | dropped with a `warn` — the broker delivers the frames it held by itself |
|
|
613
657
|
|
|
614
|
-
`connection.blocked` is logged as a `warn` with the broker's reason.
|
|
615
|
-
|
|
616
|
-
|
|
658
|
+
`connection.blocked` is logged as a `warn` with the broker's reason. **Nothing else flushes
|
|
659
|
+
the buffer**: an outage of the publisher CHANNEL alone, on a connection that stays up and is
|
|
660
|
+
not blocked, ends with neither event, so a message held during it is not replayed when the
|
|
661
|
+
next publish recreates the channel. Whether a channel recovery should flush too is an open
|
|
662
|
+
decision (a sent entry re-sent then may be a second copy), and the error `publish()` throws
|
|
663
|
+
says the gap in as many words: buffered for up to `publishBufferTtlMs`, replayed only after a
|
|
664
|
+
reconnect or `unblocked`, reported as LOST at expiry.
|
|
665
|
+
|
|
666
|
+
A held message that outlives `publishBufferTtlMs` is dropped **at its TTL, on the buffer's own
|
|
667
|
+
timer** — not whenever the buffer is next touched (until d.1275 the expiry ran only inside the
|
|
668
|
+
next `add()`/`flush()`/`getBufferState()`, and was measured on the dev scheduler up to 88
|
|
669
|
+
minutes late; d.1275). The timer runs on the client's timer rail, so `disconnect()` ends it. The
|
|
670
|
+
drop is logged as an `error` naming its queue or exchange, the count, the age and `sent` —
|
|
671
|
+
`sent=false` reads `LOST after <ms>` and is a message lost for good; `sent=true` reads `expired
|
|
672
|
+
after <ms>`, because the broker may have delivered it on its own.
|
|
617
673
|
|
|
618
674
|
**There is no persistent buffer, and `persistentBufferEnabled` /
|
|
619
675
|
`persistentRedisClient` are refused by name.** They declared one until
|
|
@@ -658,11 +714,15 @@ error had never once run (fixed 2026-09-08, `errors.ConnectionError` now carries
|
|
|
658
714
|
| `CONSUMER_QUEUE_MISSING` | the queue does not exist | start the service that owns the queue — a consumer never creates one |
|
|
659
715
|
| `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 |
|
|
660
716
|
| `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 |
|
|
717
|
+
| `MQ_CONNECTION_FATAL` (with `reason: 'cycles-spent' \| 'broker-refused'`, and `isConnectionFatal()` now `true`) | the CONNECTION is permanently lost — the recovery this `consume()` waited for ended the client, or the client was already lost; `error.cause` is the very verdict `onFatal` received (§ *permanently lost*) | the lifecycle owner stops the process; the restart policy boots a new one against a verified broker |
|
|
661
718
|
|
|
662
719
|
The third one is the SAME verdict a refused connection carries (§ *permanently lost*),
|
|
663
720
|
because it asks the lifecycle owner for the same thing: stop retrying. It refuses the
|
|
664
721
|
QUEUE, not the connection — the client stays connected and `isConnectionFatal()` stays
|
|
665
|
-
`false`.
|
|
722
|
+
`false`. The fourth is that connection verdict itself, carried onto the `consume()` that met
|
|
723
|
+
it (one rail, `_carryVerdict()`): the code is on the error, not only in its `cause`, so
|
|
724
|
+
`BaseClient` lets it through unchanged as it does every refusal with a verdict. A recovery
|
|
725
|
+
cycle that merely ran out (standby) is no verdict and carries no code. `@onlineapps/service-wrapper` reads `code` to end FÁZE 0.7 permanently; before
|
|
666
726
|
2026-09-16 a 406 on a queue carried no code at all, so the service kept booting against a
|
|
667
727
|
queue it will never be allowed to declare.
|
|
668
728
|
|
|
@@ -758,6 +818,7 @@ topology.telemetryLogsQueue = 'mine'; // TypeError — the object is froz
|
|
|
758
818
|
|
|
759
819
|
| Key | Name on the wire | Who publishes / who drains it |
|
|
760
820
|
|---|---|---|
|
|
821
|
+
| `infrastructureHealthChecksQueue` | `infrastructure.health.checks` | Every infrastructure service publishes its health checks there (`@onlineapps/infrastructure-tools` `healthPublisher.js`); the registry drains it (`api/infra/api_services_registry/src/listeners/infrastructureHealth.listener.js`). Composed here since d.1307; no reader cites it yet — `@onlineapps/service-common` still carries the literal (`src/defaults.js`), and which side switches is the owner's decision (d.1306). |
|
|
761
822
|
| `infrastructureHealthEventsExchange` | `infrastructure.health.events` | Fanout. The registry publishes (`api/infra/api_services_registry/src/services/infrastructureEventPublisher.js`); the two queues below are bound to it. |
|
|
762
823
|
| `monitoringInfrastructureHealthEventsQueue` | `monitoring.infrastructure.health.events` | The monitoring consumer's queue on that fanout (`api/infra/api_monitoring/src/consumer/topology.js`, `MONITORING_INFRA_EVENT_QUEUE`). |
|
|
763
824
|
| `deliveryHealthEventsQueue` | `delivery.health.events` | The delivery endpoint binds it to the fanout and consumes it for WS push (`api/infra/api_delivery_endpoint/src/index.js`, `bindQueueToFanout(config.healthEventsQueue, …)`). |
|
|
@@ -766,6 +827,7 @@ topology.telemetryLogsQueue = 'mine'; // TypeError — the object is froz
|
|
|
766
827
|
| `telemetryMetricsQueue` | `telemetry.metrics.queue` | The same exchange, the same consumer — metric samples. |
|
|
767
828
|
| `telemetryTracesQueue` | `telemetry.traces.queue` | The same exchange, the same consumer — spans. |
|
|
768
829
|
| `telemetryExchange` | `telemetry.exchange` | Topic exchange the three queues above are bound to; every telemetry producer publishes to it, and the monitoring consumer asserts it (d.708b). |
|
|
830
|
+
| `workflowObserverCopyQueues` | `['monitoring.workflow', 'delivery.workflow.events', 'scheduler.workflow.events']` (frozen array) | The observers' copies of a run: the queues bound to `monitoring.workflow.fanout` for a reader that watches the run and is not part of it (the monitoring consumer, the delivery endpoint's WS feed, the scheduler's end-of-run watch). A dead letter from one of them is a lost copy of the run's telemetry, not a lost run (INFRA 559). Declared once in `queueConfig` (`WORKFLOW_OBSERVER_COPIES`) — membership cannot be read off an entry, because the binding is each consumer's own act at boot — and composed by `queueName()` (d.1336). |
|
|
769
831
|
|
|
770
832
|
- **Frozen on purpose.** An importer must not be able to rewrite platform topology: a
|
|
771
833
|
name overridden on one side of a fanout loses the events silently, which is why batch
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/mq-client-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
|
|
5
5
|
"oa": {
|
|
6
6
|
"category": "connector"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"author": "OnlineApps",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@onlineapps/logger-contract": "2.
|
|
26
|
+
"@onlineapps/logger-contract": "2.1.0",
|
|
27
27
|
"@onlineapps/runtime-config": "1.2.0",
|
|
28
28
|
"ajv": "^8.12.0",
|
|
29
29
|
"amqplib": "0.10.9"
|