@onlineapps/mq-client-core 4.2.0 → 5.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 +623 -0
- package/README.md +99 -11
- package/package.json +1 -1
- package/src/BaseClient.js +105 -29
- package/src/config/configSchema.js +21 -4
- package/src/config/defaultConfig.js +14 -4
- package/src/config/deliveryPolicy.js +86 -6
- package/src/config/queueConfig.js +68 -39
- package/src/config.js +5 -1
- package/src/defaults.js +7 -1
- package/src/index.js +41 -4
- package/src/layers/PublishLayer.js +113 -5
- package/src/monitoring-publish.js +89 -40
- package/src/scheduler-request.js +32 -11
- package/src/transports/rabbitmqClient.js +1237 -316
- package/src/utils/decodeMessage.js +123 -0
- package/src/utils/publishErrors.js +256 -17
- package/jest.config.js +0 -15
- package/jest.integration.config.js +0 -16
- package/src/workers/RecoveryWorker.js +0 -130
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,606 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [5.0.0] — 2026-09-27
|
|
8
|
+
|
|
9
|
+
### Removed — BREAKING — fronta `workflow.control` (d.1061)
|
|
10
|
+
|
|
11
|
+
Sekce `workflow` v `src/config/queueConfig.js` už nedeklaruje položku `control` (TTL 30 s,
|
|
12
|
+
max 10 000, DLX → `workflow.failed`). Rozhodnutí vlastníka:
|
|
13
|
+
`docs/governance/confirmations/cookbook-execution-owner.md` 002.
|
|
14
|
+
|
|
15
|
+
Co to znamená pro volajícího — hodnotami, ne větou:
|
|
16
|
+
|
|
17
|
+
- `queueConfig.getInfrastructureQueueConfig('workflow.control')` a
|
|
18
|
+
`queueConfig.getDeadLetterRoute('workflow.control')` hází
|
|
19
|
+
`[queueConfig] Infrastructure queue config not found: workflow.control …`;
|
|
20
|
+
- `queueConfig.queueName('workflow', 'control')` hází
|
|
21
|
+
`[queueConfig] Cannot compose a name: queueConfig declares no "control" under "workflow" …`,
|
|
22
|
+
takže publikace na to jméno se odmítne dřív, než cokoli odejde;
|
|
23
|
+
- konzument, který frontu dosud kontroloval při startu, ji v deklaraci nenajde.
|
|
24
|
+
|
|
25
|
+
Co se **nemění**: `workflow.init` (deklarace, jméno, trasa do `workflow.failed`) a
|
|
26
|
+
`workflow.failed` jako konec řetězu, který `workflow.init` jmenuje. Počty deklarací klesly
|
|
27
|
+
o jednu frontu (36 položek / 29 front / 7 exchange); křížový součin prefix × šablona se
|
|
28
|
+
nemění, prefix `workflow` zůstává.
|
|
29
|
+
|
|
30
|
+
Odebrání deklarace frontu z běžícího brokeru nesmaže (fronta je durable) — to je krok
|
|
31
|
+
nasazení, ne knihovny.
|
|
32
|
+
|
|
33
|
+
### Tests — `workflow.control` není deklarovaná (d.1061)
|
|
34
|
+
|
|
35
|
+
Nová sada `tests/unit/workflow-control-queue-not-declared.test.js` tvrdí nepřítomnost
|
|
36
|
+
hodnotami (žádný klíč `control`, tři odmítnutí se jménem fronty) a kontrolní případy
|
|
37
|
+
`workflow.init` a `workflow.failed`. Případy, které `workflow.control` používaly jako
|
|
38
|
+
deklarovanou frontu (`queueConfig-workflow-failed`, `dead-letter-topology`,
|
|
39
|
+
`consume-dead-letter-route-required`, `consume-terminal-queue`), ji už nejmenují; počty
|
|
40
|
+
deklarací v `queue-name-collision` a `d712-registry-changes-exchange-type` jsou srovnané.
|
|
41
|
+
|
|
42
|
+
### Documentation — README: příklad publikace na `workflow.control` nahrazen `workflow.completed` (d.1061)
|
|
43
|
+
|
|
44
|
+
### Fixed — `getChannelState()` hlásí i telemetrický kanál (d.1007)
|
|
45
|
+
|
|
46
|
+
`getChannelState()` hlásil `publisher`, `queue` a `consumer`, ale telemetrický kanál z d.724
|
|
47
|
+
(`_telemetryChannel`, vlastní události klienta) vynechával, přestože klíče téhle zprávy jsou
|
|
48
|
+
dokumentované jako seznam kanálů klienta. Teď nese čtvrtý klíč `telemetry` ve stejném tvaru
|
|
49
|
+
`{ exists, closed, ready }`. Kanál se otevírá prvním telemetrickým publishem; do té doby čte
|
|
50
|
+
`{ exists: false, closed: true, ready: false }`, stejně jako každý dosud neotevřený kanál.
|
|
51
|
+
|
|
52
|
+
Aditivní klíč. Čtenář, který tvrdí přesný tvar tří klíčů (`toEqual`), se musí srovnat: v balíčku
|
|
53
|
+
`connection-liveness-single-source` (unit i IT) je srovnaný v téže změně, kořenové
|
|
54
|
+
`api/tests/integration/channel-recovery.test.js` se srovná s re-pinem kořene na tuto verzi.
|
|
55
|
+
|
|
56
|
+
### Added — `deliveryPolicy.validateConsumeOptions`: pravidla voleb `consume()` mají jednoho exportovaného vlastníka (d.1005)
|
|
57
|
+
|
|
58
|
+
Čtyři pravidla voleb `consume()` dosud žila inline v transportu (`rabbitmqClient.js` `consume()`):
|
|
59
|
+
- `requeueOnError` je boolean;
|
|
60
|
+
- `requeueOnError: false` znamená jeden pokus a jiné `maxAttempts` s ním je rozpor;
|
|
61
|
+
- rozpočet je celé číslo ≥ 1;
|
|
62
|
+
- `classify` je funkce.
|
|
63
|
+
|
|
64
|
+
Dvojník konzumenta (service-wrapper `MockMQConnector`) je proto mohl jen opsat, tedy druhá kolej
|
|
65
|
+
(nález d.998c). Teď bydlí v `src/config/deliveryPolicy.js` `validateConsumeOptions(options,
|
|
66
|
+
resolveDefaultMaxAttempts)`, transport ji volá a balíček ji exportuje jako
|
|
67
|
+
`deliveryPolicy.validateConsumeOptions`. Vrací `{ maxAttempts, classify }`, nebo hází `ValidationError`
|
|
68
|
+
se stejnými texty jako dosud `consume()`: `[RabbitMQClient] Invalid requeueOnError …`,
|
|
69
|
+
`… Contradictory delivery policy …`, `… Invalid delivery budget …`, `… Invalid error classifier …`.
|
|
70
|
+
|
|
71
|
+
Výchozí rozpočet se předává funkcí a volá se jen tehdy, když ho volby nechávají otevřený. Jeho
|
|
72
|
+
čtení sahá do konfigurace (env `RABBITMQ_MAX_DELIVERY_ATTEMPTS`), která může sama odmítnout, a
|
|
73
|
+
explicitní `maxAttempts` ani `requeueOnError: false` ho nikdy nepotřebovaly. Chování `consume()` se
|
|
74
|
+
nemění. Pro dvojníky konzumentů: volat `deliveryPolicy.validateConsumeOptions(options, () =>
|
|
75
|
+
<rozpočet dvojníka>)` místo vlastní kopie pravidel.
|
|
76
|
+
|
|
77
|
+
### Removed — BREAKING — `RecoveryWorker` a posluchač kanálu `error`, který ho krmil (d.971)
|
|
78
|
+
|
|
79
|
+
Pryč je `src/workers/RecoveryWorker.js` (`handleQueueNotFound()`, `handleTransientError()`)
|
|
80
|
+
a v `src/transports/rabbitmqClient.js` jeho instance `_recoveryWorker` i vlastní posluchač
|
|
81
|
+
transportu `this.on('error', …)` („Recovery worker - handle QueueNotFoundError via error
|
|
82
|
+
event“). Čerstvě postavený transport na svém kanálu `error` neposlouchá nic.
|
|
83
|
+
|
|
84
|
+
BREAKING pro každého, kdo si worker bral hlubokou cestou
|
|
85
|
+
`require('@onlineapps/mq-client-core/src/workers/RecoveryWorker')` — z `src/index.js` se
|
|
86
|
+
nikdy neexportoval; ve workspace to dělaly jen dvě sady `conn-infra-mq`, přepsané v téže
|
|
87
|
+
vlně na veřejný kontrakt.
|
|
88
|
+
|
|
89
|
+
Co zmizí z logu: při publikaci do chybějící fronty už nevznikne řádek
|
|
90
|
+
`[RecoveryWorker] Cannot create infrastructure queue '<q>' …`,
|
|
91
|
+
`[RecoveryWorker] Cannot create business queue '<q>' …` ani
|
|
92
|
+
`[RecoveryWorker] Cannot create queue '<q>' - no section of queueConfig declares it …`.
|
|
93
|
+
Po d.811b (výše) už ho stejně nic nespouštělo.
|
|
94
|
+
|
|
95
|
+
Kde volající verdikt dostane (beze změny): `publish()` odmítne `PublishError`
|
|
96
|
+
(`error.queue` = cílová fronta) s `cause` = `QueueNotFoundError` — `queueName`,
|
|
97
|
+
`isInfrastructure`, `kind` (`infrastructure` / `business` / `unowned`) a pro každou třídu
|
|
98
|
+
věta s vlastníkem fronty a `Fix:`. Nic se nedeklaruje.
|
|
99
|
+
|
|
100
|
+
Čtyři otázky (`change-discipline.md` § Removing): (1) vznikl v `3588b148` jako zakladač
|
|
101
|
+
chybějící fronty pro business scope; (2) nesla ho hlavička workeru § Queue Creation, kterou
|
|
102
|
+
nahradila conf `mq-consumer-contract` 006 („queues come into being only from
|
|
103
|
+
declarations“) — od d.419 nic nezakládal, jen logoval a znovu házel do posluchače, který
|
|
104
|
+
chybu spolkl; (3) funkci mu vzal d.419, vstup d.811b; (4) náhrada je konceptuálnější —
|
|
105
|
+
jeden verdikt jednou kolejí, k volajícímu, místo druhé kopie téže věty do logu.
|
|
106
|
+
Sada `tests/unit/recovery-worker-removed.test.js` (kontrolní případ: `publish()` do
|
|
107
|
+
chybějící business fronty dál odmítá `PublishError` s `QueueNotFoundError` v `cause`).
|
|
108
|
+
|
|
109
|
+
### Changed — BREAKING — verdikt o odmítnuté publikaci jde jen odmítnutím `publish()`, ne kanálem `error` (d.811b)
|
|
110
|
+
|
|
111
|
+
`RabbitMQClient._publishOnce()` končil každé selhání pokusu dvěma tvrzeními jednoho
|
|
112
|
+
verdiktu: `this.emit('error', classifiedErr)` a `throw classifiedErr`. Emit je pryč,
|
|
113
|
+
zůstává `throw classifyPublishError(err, queue)` (`src/transports/rabbitmqClient.js`,
|
|
114
|
+
větev `catch` v `_publishOnce()`). Kanál `error` zůstává tomu, k čemu ho používá zbytek
|
|
115
|
+
klienta — spojení a kanálům.
|
|
116
|
+
|
|
117
|
+
Co konzument vidí jinak:
|
|
118
|
+
|
|
119
|
+
- **`BaseClient.onError(callback)`** (jediná veřejná cesta ke kanálu `error` transportu,
|
|
120
|
+
`BaseClient.connect()` na něj věší `_handleError`) už nedostává `TransientPublishError`
|
|
121
|
+
(`retryable: true`), `PermanentPublishError` (`retryable: false`) ani `QueueNotFoundError`
|
|
122
|
+
(`kind`, `queueName`, `isInfrastructure`). Dřív je dostával za KAŽDÝ neúspěšný pokus,
|
|
123
|
+
tedy i za transientní pokusy, které `PublishLayer` následně zopakoval nebo zabufferoval.
|
|
124
|
+
Ostatní emity na `error` se nemění: spojení (`CONNECTION_CLOSED_UNEXPECTEDLY`), konec
|
|
125
|
+
cyklu obnovy spojení, chyby kanálu vydavatele (406 dál ne — tu odpovídá odesílateli
|
|
126
|
+
publikace) i konzumenta, odmítnutí `consume()`, selhání `ack`/`nack` a odmítnuté zavření
|
|
127
|
+
kanálu či spojení v `disconnect()`.
|
|
128
|
+
- **`publish()` se nemění:** odmítá stejně jako dřív — `PublishError` z `BaseClient.publish()`
|
|
129
|
+
s klasifikovanou chybou v `error.cause`. Čísla o publikacích hlásí dál
|
|
130
|
+
`getPublishMetrics()` / `getPrometheusMetrics()` (plní je události `publish:retry`,
|
|
131
|
+
`publish:success`, `publish:failed`, `publish:buffered`, beze změny).
|
|
132
|
+
- **`RecoveryWorker.handleQueueNotFound()` už na cestě publikace neběží.** Transport ho volal
|
|
133
|
+
z vlastního posluchače kanálu `error` (`_setupPublishMonitoring()`, „Recovery worker -
|
|
134
|
+
handle QueueNotFoundError via error event") a žádné jiné místo v `src/**` ho nevolá; žádný
|
|
135
|
+
zbývající emit na `error` `QueueNotFoundError` nenese. Při publikaci do chybějící fronty
|
|
136
|
+
proto z logu mizí řádek `[RecoveryWorker] Cannot create infrastructure queue '<q>' …` /
|
|
137
|
+
`Cannot create business queue '<q>' …` / `Cannot create queue '<q>' - no section of
|
|
138
|
+
queueConfig declares it …`. Fronta se nezakládá ani dál (nezakládal ji ani ten řádek) a
|
|
139
|
+
volající dostane odmítnutí jako dosud; ubyla jen druhá, logová zpráva o téže události.
|
|
140
|
+
- Filtr v `_waitForReconnection()` (`if (error.retryable !== undefined) return;`) zůstává:
|
|
141
|
+
knihovna do kanálu klasifikovanou chybu už nedává, ale kanál je veřejný a kdo drží
|
|
142
|
+
transport, emitovat na něj může.
|
|
143
|
+
|
|
144
|
+
Dopad ve workspace (změřeno 2026-09-26 nad `api/infra/*`, `api/shared/**` mimo tento balíček
|
|
145
|
+
a `api_biz/*/src`): v produkčním kódu nevolá `onError()` nikdo a nikdo neposlouchá `error` na
|
|
146
|
+
MQ klientovi ani na jeho transportu — pět míst, která sahají na `mqClient._transport`
|
|
147
|
+
(gateway, dispatcher, scheduler, wrapper, `conn-infra-mq` `QueueManager`), berou z něj
|
|
148
|
+
`queueChannel`, `connection` nebo `onChannelClose`, ne kanál `error`. Integrační sady v
|
|
149
|
+
`api/tests/integration` a `api/infra/api_gateway/tests` registrují `onError(() => {})`, který
|
|
150
|
+
nic netvrdí. Proto BREAKING podle kontraktu `onError()`, bez čtenáře, kterého by to zlomilo.
|
|
151
|
+
|
|
152
|
+
### Changed — BREAKING — `serviceName` je povinný, knihovna si ho nevyrábí (d.770)
|
|
153
|
+
|
|
154
|
+
`src/defaults.js` už nenese `serviceName: 'oa-service'` a `src/config.js` deklaruje klíč
|
|
155
|
+
jako `required: true`. Jméno má dvě nohy, obě volajícího: explicitní `serviceName`
|
|
156
|
+
v konfiguraci klienta, nebo ENV `SERVICE_NAME`. Třetí není. Chybí-li obě,
|
|
157
|
+
`new BaseClient(…)` se odmítne postavit hláškou, jejíž znění vlastní
|
|
158
|
+
`@onlineapps/runtime-config` (`[RuntimeConfig] Missing environment variable - SERVICE_NAME
|
|
159
|
+
is required for config key "serviceName". Fix: …`). Dřív klient postavený bez klíče
|
|
160
|
+
publikoval události monitoringu a pojmenovával fronty pod službou `oa-service`, která
|
|
161
|
+
neexistuje — v `workflow_traces.service_name` pak stálo jméno nikoho.
|
|
162
|
+
|
|
163
|
+
Druhý čtenář téhož faktu je srovnaný: `BaseClient` rozřešené jméno (ať přišlo kteroukoli
|
|
164
|
+
nohou) zapisuje zpět do `config.serviceName` a transport z něj skládá jméno spojení
|
|
165
|
+
`oa-client:<serviceName>:<pid>`, když volající nedal `connectionName` ani `clientName`.
|
|
166
|
+
Dřív ho transport bral z runtime konfigurace, takže explicitní `serviceName` volajícího se
|
|
167
|
+
tam vůbec nečetl. Transport postavený bez jména se odmítne `ValidationError` místo spojení
|
|
168
|
+
pojmenovaného `oa-client:undefined`.
|
|
169
|
+
|
|
170
|
+
Migrace: předejte `serviceName` v konfiguraci klienta, nebo nastavte `SERVICE_NAME`
|
|
171
|
+
v prostředí služby. Změřeno v dávce: service-wrapper, conn-orch-registry,
|
|
172
|
+
api_delivery_dispatcher a api_meta_reader jméno předávají; api_delivery_endpoint stojí na
|
|
173
|
+
noze `SERVICE_NAME`.
|
|
174
|
+
|
|
175
|
+
### Changed — BREAKING — zprávu, kterou broker pozdě odmítl, vrstva publishe už neposílá znovu (d.801c)
|
|
176
|
+
|
|
177
|
+
`PublishLayer` po dobu jednoho publishe poslouchá událost `publish:refused-late` (d.801b,
|
|
178
|
+
níže) a před každým dalším pokusem se ptá, zda pro tuto frontu nedorazil pozdní
|
|
179
|
+
PERMANENTNÍ verdikt. Pokud ano, smyčka končí: `warn` `[PublishLayer] The broker refused
|
|
180
|
+
this message while attempt <n> was in flight, so attempt <n+1> of <max> is not made …`,
|
|
181
|
+
událost `publish:failed` s `retryable: false` a `refusedLate: true`, a volající dostane ten
|
|
182
|
+
verdikt — `PermanentPublishError` s brokerovým rámcem v `cause`. Žádný další pokus, nic do
|
|
183
|
+
bufferu. Posluchač se odhlašuje v `finally`; nevzniká žádný nový časovač ani evidence. Číslo
|
|
184
|
+
pokusu jde transportu v options jako `attempt` (do AMQP vlastností se nedostane).
|
|
185
|
+
|
|
186
|
+
Co se mění pro konzumenta: zpráva, kterou broker odmítl až poté, co `brokerAnswerTimeout`
|
|
187
|
+
odpověděl volajícímu, se dřív opakovala `publishMaxRetries`krát a skončila v bufferu,
|
|
188
|
+
odkud ji `InMemoryBuffer.flush()` nabízel při každém reconnectu do vypršení TTL. Nově
|
|
189
|
+
skončí jedním pokusem a trvalou chybou. Dopad popsaný v dávce: api_delivery_dispatcher
|
|
190
|
+
takovou zásilku přestane opakovat a pošle ji deklarovanou dead-letter cestou; RPC odpověď
|
|
191
|
+
service-wrapperu změní třídu z transientní na permanentní; kdo počítá pokusy z
|
|
192
|
+
`publish:retry`, uvidí u takové zprávy jeden místo N. Zpráva, u níž potvrzení nedorazí
|
|
193
|
+
vůbec a žádný rámec není, se opakuje beze změny.
|
|
194
|
+
|
|
195
|
+
### Changed — BREAKING — očíslované odmítnutí odeslání je trvalý verdikt o TÉTO zprávě (d.685a, d.693, d.696, d.723)
|
|
196
|
+
|
|
197
|
+
Broker, který odmítne `basic.publish`, zavře kanál očíslovaným rámcem (`classId 60`,
|
|
198
|
+
`methodId 40`). amqplib ho předá události `error` kanálu, kde ho handler publisher kanálu
|
|
199
|
+
zapíše do `channel._closeError`; callback publishe dostane jen holé `'channel closed'`.
|
|
200
|
+
`_publishOnce()` ten rámec nově čte na obou nohách, frontové i výměnové:
|
|
201
|
+
|
|
202
|
+
- **Číselný kód je odpověď** (d.685a, d.693): 403 ACCESS-REFUSED, 406 PRECONDITION-FAILED
|
|
203
|
+
i 404 NOT-FOUND za publish přes výměnu, kterou nikdo nedeklaroval, končí
|
|
204
|
+
`PermanentPublishError` s rámcem v `cause` — jeden pokus, nic do bufferu. Dřív
|
|
205
|
+
`TransientPublishError`: `publishMaxRetries` pokusů, pak buffer a nabídka při každém
|
|
206
|
+
reconnectu do TTL. Věty má jeden producent, `refusedSendMessage(kind, target, code)`
|
|
207
|
+
v `utils/publishErrors.js`; u 404 nad výměnou říká, že ji nikdo nedeklaroval, a `Fix:`
|
|
208
|
+
posílá do `queueConfig`. Chybějící FRONTA se nemění — dál `QueueNotFoundError`
|
|
209
|
+
z předkontroly.
|
|
210
|
+
- **Rámec patří jedné publikaci** (d.723): nese-li kanál v okamžiku rámce víc
|
|
211
|
+
nepotvrzených publikací, trvalý verdikt dostane jen ta, jejíž výměnu či frontu rámec
|
|
212
|
+
jmenuje; ostatní dostanou `TransientPublishError` s rámcem v `cause`
|
|
213
|
+
(`collateralCloseMessage()`), takže je vrstva zopakuje a mezitím bufferuje. Jméno, které
|
|
214
|
+
se v rámci nenajde, znamená „nepřisouzeno" — přechodný verdikt, tedy nanejvýš duplicitní
|
|
215
|
+
zpráva, nikdy zahozená.
|
|
216
|
+
- **Důvod se zapisuje dřív, než handler větví** (d.696): i rámec `406` „unknown delivery
|
|
217
|
+
tag" handler publisher kanálu zapíše do `channel._closeError`, takže publish na kanálu
|
|
218
|
+
zabitém tímto rámcem je trvalý stejně jako každé jiné očíslované odmítnutí. Na kanál
|
|
219
|
+
`error` klienta rámec dál nejde.
|
|
220
|
+
- Smrt kanálu BEZ číselného kódu (výpadek spojení, soketový `code` jako `'ECONNRESET'`)
|
|
221
|
+
zůstává přechodná: o osudu zprávy se nic neví.
|
|
222
|
+
|
|
223
|
+
Migrace: kdo spoléhal na to, že vrstva odmítnutou zprávu zopakuje nebo zabufferuje,
|
|
224
|
+
dostane ji nově hned jako odmítnutí `publish()`. Oprava je u příčiny, kterou rámec
|
|
225
|
+
v `error.cause` jmenuje — právo účtu ve vhostu, `user_id` zprávy, deklarace výměny.
|
|
226
|
+
|
|
227
|
+
### Changed — BREAKING — `publishMaxRetries: 0` se odmítá jménem (d.684, d.685b)
|
|
228
|
+
|
|
229
|
+
Klíč počítá POKUSY včetně prvního. `0` proto nebylo „bez opakování", ale publish, ke
|
|
230
|
+
kterému nedojde: smyčka `_publishWithRetry()` neproběhla ani jednou, zpráva se brokeru
|
|
231
|
+
nenabídla, cestou ven se nezabufferovala a závěrečná hláška svalovala vinu na
|
|
232
|
+
`PublishLayer`. Tři komentáře, které `0` vykládaly jako „do not retry", jsou opravené.
|
|
233
|
+
|
|
234
|
+
Nově: `configSchema.js` deklaruje `minimum: 1`, takže `new BaseClient({ …,
|
|
235
|
+
publishMaxRetries: 0 })` padne už při stavbě klienta (`/publishMaxRetries: must be >= 1`
|
|
236
|
+
s `Fix:`); konstruktor `PublishLayer` hodnotu pod 1 odmítá i tam, kam schéma nedosáhne
|
|
237
|
+
(transport postavený přes `transportFactory`, vlastní `options.maxRetries` volajícího),
|
|
238
|
+
`ValidationError` `[PublishLayer] publishMaxRetries must be at least 1 - …`.
|
|
239
|
+
|
|
240
|
+
Migrace: jeden pokus bez opakování je `publishMaxRetries: 1`, opuštění retry koleje
|
|
241
|
+
`publishRetryEnabled: false`. Změřeno v dávce d.685b: jediný zápis klíče mimo balíček je
|
|
242
|
+
`api/tests/integration/message-buffer.test.js` s hodnotou 2.
|
|
243
|
+
|
|
244
|
+
### Changed — vytíženost prefetch okna se hlásí na přechodu, oběma směry (d.736a)
|
|
245
|
+
|
|
246
|
+
`_checkPrefetchUtilization()` dřív mluvil pokaždé, když podmínku našel platnou (produkce
|
|
247
|
+
18. 9.: tři řádky za 68 ms o jedné stojící podmínce), a návrat pod práh neřekl nikdy. Nově
|
|
248
|
+
padá verdikt jen na přechodu: nad práh jeden `warn` `Prefetch utilization high: …` a s ním
|
|
249
|
+
jednou událost `prefetch:high-utilization` a jednou `prefetchAlertCallback`; pod práh
|
|
250
|
+
jeden `info` `Prefetch utilization back below threshold: …`; mezi tím ticho, ať se čítač
|
|
251
|
+
nad prahem hýbe jakkoli. Stav drží `tracking.utilizationHigh` a verdikt se bere i při
|
|
252
|
+
snížení čítače. Kdo počítal volání callbacku nebo události, dostane jedno za epizodu místo
|
|
253
|
+
jednoho za každou kontrolu nad prahem. Práh se nemění, hystereze ani druhý callback nevznikají.
|
|
254
|
+
|
|
255
|
+
### Changed — fronta instance registru vyprší 60 s po posledním konzumentovi (`x-expires`, d.1036)
|
|
256
|
+
|
|
257
|
+
Šablona `registry.{instance_id}` (`src/config/queueConfig.js`) nese `'x-expires': 60000`.
|
|
258
|
+
Každý start kopie služby pojmenuje novou frontu `<svc>.registry.<instance_id>` (UUID na proces,
|
|
259
|
+
pokud `SERVICE_INSTANCE_ID` neurčí jinak) a nic starou nemaže — `deregister()`, shutdown
|
|
260
|
+
wrapperu ani registr, který frontu před odpovědí jen ověří (měřeno d.1032). Bez životnosti by
|
|
261
|
+
po každém restartu, pádu i čistém stopu zůstala trvalá fronta bez konzumenta, kterou monitoring
|
|
262
|
+
hlásí jako `no_owner`. Živá kopie má konzumenta, takže jí fronta nevyprší; po odchodu kopie ji
|
|
263
|
+
broker smaže do 60 s — pod oknem sirotka monitoringu (`min_orphan_minutes`, seed 5), takže
|
|
264
|
+
epizodu neotevře. Sdílená `<svc>.registry` životnost nemá a nemění se. Rozhodnutí vlastníka:
|
|
265
|
+
otázka 14, varianta A (`registry-instance-identity` 003).
|
|
266
|
+
|
|
267
|
+
Není to BREAKING vůči vydané 4.2.0: ta šablonu `registry.{instance_id}` nemá vůbec, takže
|
|
268
|
+
žádný vydaný klient frontu tohoto tvaru nezaložil (konzumaci by odmítl bez dead-letter cesty).
|
|
269
|
+
Změřeno na brokeru (`tests/integration/registry-instance-queue-expires.integration.test.js`):
|
|
270
|
+
fronta tohoto tvaru, kterou dřív kdokoli založil BEZ `x-expires`, se teď při `consume()` odmítne
|
|
271
|
+
`406 PRECONDITION-FAILED` („inequivalent arg 'x-expires'") — týká se jen front z nevydaného
|
|
272
|
+
buildu (na dev brokeru jich je 0, d.1032 bod 5); oprava = frontu smazat, klient ji při
|
|
273
|
+
konzumaci založí znovu.
|
|
274
|
+
|
|
275
|
+
### Added — fronta jedné instance registru `{service}.registry.{instance_id}` je deklarovaná (d.817b)
|
|
276
|
+
|
|
277
|
+
Odpovědi registru na registraci jdou instanci na `<svc>.registry.<instanceId>` (conf
|
|
278
|
+
`registry-instance-identity` 001); sdílená `<svc>.registry` nese jen podněty registru. Pro
|
|
279
|
+
takové jméno dosud `queueConfig.getDeadLetterRoute()` i `parseBusinessQueue()` vracely `null`
|
|
280
|
+
a `consume()` frontu bez deklarované dead-letter cesty odmítá (conf `mq-consumer-contract`
|
|
281
|
+
002).
|
|
282
|
+
|
|
283
|
+
Nová šablona v sekci `business` (`src/config/queueConfig.js`):
|
|
284
|
+
|
|
285
|
+
'registry.{instance_id}': {
|
|
286
|
+
durable: true,
|
|
287
|
+
arguments: {
|
|
288
|
+
'x-dead-letter-exchange': DEAD_LETTER_EXCHANGE, // 'dlx'
|
|
289
|
+
'x-dead-letter-routing-key': '{service}.dlq'
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
tedy stejná dead-letter cesta jako u sdílené `{service}.registry`:
|
|
294
|
+
`getDeadLetterRoute('biz-hello.registry.7f3a9c21')` = `{ exchange: 'dlx', routingKey:
|
|
295
|
+
'biz-hello.dlq' }`. Životnost fronty doplnila d.1036 (`x-expires`, viz Changed níž).
|
|
296
|
+
|
|
297
|
+
Rozhodčí pravidlo `matchBusinessTemplate()`: `<svc>.registry.events` sedí na doslovnou
|
|
298
|
+
šablonu `registry.events` i na zástupnou `registry.{instance_id}`. Doslovná šablona nově
|
|
299
|
+
vyhrává nad zástupnou; dvě shody šablon TÉHOŽ druhu jsou dál vada a jméno se odmítá
|
|
300
|
+
`ValidationError` („matches more than one template"). Stávající šablony, jejich argumenty ani
|
|
301
|
+
cesty se nemění (`registry.events` si drží TTL i strop, sdílená `registry` je beze změny,
|
|
302
|
+
`registry.register` zůstává platformní); jméno, na které nesedí žádná šablona, je dál
|
|
303
|
+
odmítnuto.
|
|
304
|
+
|
|
305
|
+
### Added — `scheduler.SCHEDULER_REQUEST_INVALID`: odmítnutí builderu se pozná podle kódu, ne podle textu (d.943)
|
|
306
|
+
|
|
307
|
+
Boot fáze 0.7b wrapperu má podle conf `scheduler` 020 izolovat záznam, který builder odmítne
|
|
308
|
+
(selhaný zdroj), a dál zastavit boot, když se požadavek nepodaří ODESLAT (selhání služby). Obě
|
|
309
|
+
cesty dosud házely holé `Error`, takže je šlo rozlišit jen podle znění hlášky. Nově každé
|
|
310
|
+
odmítnutí builderu nese `error.code = 'SCHEDULER_REQUEST_INVALID'` (konstanta pod jmenným
|
|
311
|
+
prostorem `scheduler`, jediný vlastník). Kód nesou: kontrola klíčů v `registerRecurring`,
|
|
312
|
+
`validateRecurrences` (chybějící `source`, dokument není objekt, dokument nedrží tvar, dvakrát
|
|
313
|
+
stejné `id`), `requireIdempotencyKey`, `requireRequestedBy`, `requireTenancy` (tenant i
|
|
314
|
+
workspace) a `buildEventRequest` (chybí `event`, `event.service`/`event.name` mimo vzor) —
|
|
315
|
+
tedy i `buildRecurringRequest` a `buildCancelRequest`, které je volají. NEnesou ho:
|
|
316
|
+
`publishSchedulerRequest` (chybí klient, klient není připojen, cizí druh požadavku, publikace
|
|
317
|
+
odmítnutá brokerem) a stavba portu v `createSchedulerPublisher`. Znění hlášek beze změny.
|
|
318
|
+
Sada `tests/unit/scheduler-request-invalid-code.test.js` (kontrolní případy: selhaná publikace
|
|
319
|
+
a nepřipojený klient kód nemají).
|
|
320
|
+
|
|
321
|
+
### Added — událost `message_dlq` jmenuje operaci, která umřela (d.641)
|
|
322
|
+
|
|
323
|
+
Mrtvý dopis dosud říkal, ve KTERÉ frontě zemřel (`queue`, `attempts`,
|
|
324
|
+
`classification`, `error`), ale ne CO umřelo. Operátor tak u běhu s víc kroky
|
|
325
|
+
neměl z události jak poznat, která operace to byla; jméno bylo přitom celou dobu
|
|
326
|
+
v obálce, kterou `_emitDeadLetterEvent()` už rozbaluje kvůli `workflow_id`.
|
|
327
|
+
|
|
328
|
+
Nově událost nese `operation` — **jen když ho obálka nesla**, v jediném
|
|
329
|
+
pravopisu invokační obálky (`docs/biz/10-invocation/mq-envelope.md` § Inbound
|
|
330
|
+
envelope). Workflow zpráva (`docs/biz/70-contracts/workflow-message.md` §3.2)
|
|
331
|
+
`operation` nikdy nenese, takže u ní klíč v události prostě chybí; žádný
|
|
332
|
+
`unknown`, žádný `null` — stejně jako se zachází s chybějícím `workflow_id`.
|
|
333
|
+
Hodnota, která není neprázdný řetězec (`''`, číslo, objekt), se bere jako
|
|
334
|
+
nepřítomnost.
|
|
335
|
+
|
|
336
|
+
Konzument existuje už dnes a nový klíč čte beze změny: monitoring bere
|
|
337
|
+
`readReported(data.operation)` (`api/infra/api_monitoring/src/consumer/
|
|
338
|
+
apiflow-extension.js`) a posílá ji do řádku v Loki jako `operation` — u události
|
|
339
|
+
`message_dlq` tam do téhle dávky chodilo `null`. Identita běhu zůstává **jeden
|
|
340
|
+
klíč**, `workflow_id`: `correlation_id` na `monitoring.workflow` nejezdí
|
|
341
|
+
(`docs/biz/70-contracts/monitoring-events.md` § Rules) a tahle dávka ho tam
|
|
342
|
+
nezavádí.
|
|
343
|
+
|
|
344
|
+
Doklad proti realitě: integrační tier posílá přes živý broker obálku s
|
|
345
|
+
`operation`, nechá ji dojít do `<service>.dlq` a čte událost z vlastní kopie
|
|
346
|
+
fanoutu `monitoring.workflow.fanout`; kontrolní případ je tatáž cesta s
|
|
347
|
+
workflow zprávou bez `operation` — klíč v události chybí.
|
|
348
|
+
|
|
349
|
+
### Added — `decodeMessage()`: jedna veřejná kolej z doručení na zprávu (d.789)
|
|
350
|
+
|
|
351
|
+
Nový export z kořene balíčku (`src/utils/decodeMessage.js`). `consume()` předává handleru
|
|
352
|
+
syrovou zásilku amqplib, takže si překlad psal každý konzument sám — dávka d.788 změřila
|
|
353
|
+
čtyři kopie (dispatcher, plánovač, service-wrapper, registr), které se neshodnou právě
|
|
354
|
+
v tom, co se stane s vadnou zprávou. Dekódování samo dělá `utils/serializer.js`
|
|
355
|
+
`deserialize()` odjakživa; chyběly veřejné dveře.
|
|
356
|
+
|
|
357
|
+
`decodeMessage(raw)` vrací `{ body, headers, properties, fields }` — dekódované tělo
|
|
358
|
+
a všechno ostatní tak, jak přišlo. Odmítá `ValidationError`em, jménem toho, co přišlo:
|
|
359
|
+
zásilku, která není objekt; `content`, který není `Buffer` ani řetězec; prázdné tělo;
|
|
360
|
+
deklarovaný `contentType` jiný než `application/json` (typ se kontroluje, jen když ho
|
|
361
|
+
zpráva deklaruje — obálka platformy ho nevyžaduje). Tělo, které není JSON, propouští
|
|
362
|
+
`SerializationError` z `deserialize()` beze změny (`payload`, `cause`). Pole `retryable`
|
|
363
|
+
nenese: na cestě konzumace ho nic nečte, osud zprávy rozhoduje `classify(error)`
|
|
364
|
+
volajícího podle třídy chyby.
|
|
365
|
+
|
|
366
|
+
Aditivní: `consume()` se nemění a v balíčku funkci nic nevolá. Převod čtyř konzumentů
|
|
367
|
+
mění osud vadné zprávy a jde vlastní dávkou.
|
|
368
|
+
|
|
369
|
+
### Added — `topology.telemetryExchange` (d.708b)
|
|
370
|
+
|
|
371
|
+
Zmrazený export `topology` nese `telemetryExchange: 'telemetry.exchange'`, složený
|
|
372
|
+
`queueConfig.queueName('telemetry', 'exchange')` ze šablony, kterou výměna má
|
|
373
|
+
v `queueConfig` (d.708) — stejným tvarem jako tři `telemetry*Queue` nad ním. Monitoring
|
|
374
|
+
konzument řeší každé platformní jméno, které sám nevlastní, přes `topology`, a
|
|
375
|
+
`src/config/queueConfig` je interní cesta, ne kontrakt; deklarované, ale nevyexportované
|
|
376
|
+
jméno si proto dosud musel držet jako literál. Přejmenování v `queueConfig` export posune
|
|
377
|
+
s sebou, chybějící záznam selže hned.
|
|
378
|
+
|
|
379
|
+
### Added — `BaseClient.publishTelemetry()`: vlastní události klienta mají vlastní kanál (d.724)
|
|
380
|
+
|
|
381
|
+
Kanál umírá na první odmítnutí, které potká, a bere s sebou každou nepotvrzenou publikaci.
|
|
382
|
+
Klient přitom vlastní události (`message_dlq` na `monitoring.workflow.fanout`) posílal po
|
|
383
|
+
tomtéž ConfirmChannelu, na kterém čekaly zprávy volajícího — ve vhostu bez toho fanoutu
|
|
384
|
+
tak 404 kvůli události shodil obchodní zprávu (změřeno d.711: tři ze sedmi běhů
|
|
385
|
+
nesouvisející sady).
|
|
386
|
+
|
|
387
|
+
- `publishTelemetry(queue, message, options?)` publikuje stejně jako `publish()`, ale po
|
|
388
|
+
druhém CONFIRM kanálu s rolí `telemetry`. Ten vzniká až s první takovou publikací (u
|
|
389
|
+
klienta, který žádnou neposílá, nikdy) a `disconnect()` ho zavírá spolu s ostatními.
|
|
390
|
+
`getChannelState()` ho hlásí jako čtvrtý klíč `telemetry` (d.1007, výše).
|
|
391
|
+
- Kanál se vybírá výslovně, nikdy podle jména fronty.
|
|
392
|
+
- `publishToMonitoringResilient()` (a `publishToMonitoringWorkflow()` /
|
|
393
|
+
`publishToMonitoringServices()`, které ho volají) publikuje přes
|
|
394
|
+
`mqClient.publishTelemetry()`. Klienta, který tu metodu nemá, ohlásí `warn`
|
|
395
|
+
(`Monitoring publish skipped: the client has no publishTelemetry() …`) a vrátí `false`,
|
|
396
|
+
místo aby publikoval po obchodním kanálu. `BaseClient` i jeho podtřídy (např.
|
|
397
|
+
`ConnectorMQClient` z `@onlineapps/conn-infra-mq`) ji dědí.
|
|
398
|
+
|
|
399
|
+
### Added — pozdní odmítnutí od brokeru se ohlásí: `warn` a událost `publish:refused-late` (d.801b)
|
|
400
|
+
|
|
401
|
+
Když `brokerAnswerTimeout` odpoví volajícímu dřív než broker, callback publishe běží dál
|
|
402
|
+
a sestaví správný verdikt — a dřív ho předal už vyřízené promise, kde zmizel. Nově se
|
|
403
|
+
takový verdikt ohlásí: `warn` v tvaru `[Context] Problem - Expected/Fix` (cíl, kód brokeru,
|
|
404
|
+
třída verdiktu; `Fix:` říká, že vrstva publishe zprávu brala jako nepotvrzenou, a nabízí
|
|
405
|
+
zvednout strop nad změřenou latenci) a událost `publish:refused-late` s `queue`,
|
|
406
|
+
`exchange`, `target`, `kind`, `code`, `verdict`, `error` (verdikt jako objekt), `attempt`
|
|
407
|
+
a `budgetMs`. Strop ani výsledek promise se tím nemění; podle události jedná `PublishLayer`
|
|
408
|
+
(d.801c, výše).
|
|
409
|
+
|
|
410
|
+
### Fixed — `_ensureConsumerChannel()` hlásí změřené počty a obnova s nepřipojeným konzumentem není „✓" (d.1003d)
|
|
411
|
+
|
|
412
|
+
- **Počty**: nad kanálem, který už žil (i po obnově, která ho vytvořila), vracela metoda
|
|
413
|
+
`{ reRegistered: <počet sledovaných>, failed: 0 }` — konzument, jehož re-registraci broker
|
|
414
|
+
odmítl (zůstává sledovaný bez tagu, d.262 a d.386b), se počítal jako re-registrovaný.
|
|
415
|
+
Nově se počítá podle `_isConsumerAttached()`: `reRegistered` = připojení, `failed` =
|
|
416
|
+
sledovaní bez připojení. Tvar výsledku beze změny; v produkčním kódu ho nikdo nečte
|
|
417
|
+
(`consume()` a handler zavření kanálu ho zahazují), čtou ho testy.
|
|
418
|
+
- **Souhrn obnovy**: `✓ Consumer channel recreated (re-registered: N, failed: M)` stál na
|
|
419
|
+
INFO se značkou ✓ i při `failed > 0`. Nově je to `warn` bez ✓, který jmenuje fronty bez
|
|
420
|
+
konzumenta; úspěšná obnova má dál ✓ na INFO. Odmítnutí jednotlivé fronty zůstává řádkem
|
|
421
|
+
`error` jako dosud.
|
|
422
|
+
- **Souhrn obnovy spojení** (d.1003e): reconnect logoval `✓ Consumer channel recreated` na INFO
|
|
423
|
+
bez počtů i při `failed > 0`. Totéž pravidlo jako výš — při `failed > 0` `warn` bez ✓ se jmény
|
|
424
|
+
front, jinak ✓ na INFO nově i s počty.
|
|
425
|
+
|
|
426
|
+
### Fixed — handler zavření frontového kanálu trvalé selhání obnovy nespolkne (d.1003c)
|
|
427
|
+
|
|
428
|
+
Handler `'close'` frontového kanálu logoval selhání okamžité obnovy vždy jen jako `warn`,
|
|
429
|
+
přechodné i trvalé. Nově čte totéž pravidlo jako send a consumer kanál
|
|
430
|
+
(`_connectionIsGoingAway()` nad `cause` chyby z `_ensureQueueChannel()`): spojení, které
|
|
431
|
+
odchází, zůstává `warn`; cokoli jiného jde řádkem `error` do logu a na kanál `error` klienta.
|
|
432
|
+
Líná obnova se nemění — kanál zůstane prázdný a příští operace nad frontou
|
|
433
|
+
(`_requireQueueChannel()`) ho vytvoří jako dosud. Nová událost ani nový tvar chyby nevznikl.
|
|
434
|
+
|
|
435
|
+
### Fixed — handler zavření obnovuje kanál té role, která zemřela, a consumer kanál už trvalé selhání obnovy nespolkne (d.1003b)
|
|
436
|
+
|
|
437
|
+
- **Role send kanálu**: handler zavření visí na publisher i telemetry kanálu, ale obnovu
|
|
438
|
+
volal bez role — po smrti telemetry kanálu „obnovil" živý publisher (nic) a telemetry
|
|
439
|
+
zůstal prázdný až do příští události. Handler teď nese roli kanálu, na kterém visí, a
|
|
440
|
+
obnoví tu. Smrt publisher kanálu se chová jako dosud.
|
|
441
|
+
- **Consumer kanál**: jeho handler zavření polykal každé selhání obnovy jako `warn`.
|
|
442
|
+
Přechodné (spojení odchází) zůstává `warn`; jakékoli jiné jde na kanál `error` klienta
|
|
443
|
+
a řádkem `error` do logu — konzumenti už tiše nepřestanou dostávat zprávy.
|
|
444
|
+
- **Jedno pravidlo** „odmítnuté otevření kanálu je přechodné" (`IllegalOperationError`,
|
|
445
|
+
nebo spojení, které už ohlásilo `'close'`) má jedno tělo, `_connectionIsGoingAway()`,
|
|
446
|
+
a čtou ho obnova send kanálu i handler consumer kanálu. Nová událost ani nový tvar chyby
|
|
447
|
+
nevznikl.
|
|
448
|
+
|
|
449
|
+
### Fixed — obnova send kanálu: spojení, které spadlo uprostřed otevírání, je přechodné; trvalé selhání obnovy už handler zavření nespolkne (d.1003)
|
|
450
|
+
|
|
451
|
+
- **`_ensurePublisherChannel()`**: spadne-li spojení UPROSTŘED `createConfirmChannel()`,
|
|
452
|
+
amqplib odmítne čekající RPC holým `Error('Channel ended, no reply will be forthcoming')` —
|
|
453
|
+
bez třídy i `code` — a obnova z něj dělala `PermanentPublishError`, takže PublishLayer
|
|
454
|
+
zprávu zahodil místo aby ji podržel do reconnectu. Nově rozhoduje i STAV: odmítnutí, které
|
|
455
|
+
dorazí, když `_connectionAlive` už říká, že spojení je pryč, je `TransientPublishError`.
|
|
456
|
+
Změřeno nad živým brokerem: zavření spojení brokerem během otevírání dalo tuto chybu 8× z 12,
|
|
457
|
+
pokaždé až po ohlášeném `'close'`. Táž věta nad živým spojením zůstává trvalá — text dál
|
|
458
|
+
nerozhoduje.
|
|
459
|
+
- **Handler zavření publisher kanálu**: selhání obnovy kanálu polykal celé jako `warn`.
|
|
460
|
+
Přechodné (`TransientPublishError`) zůstává `warn` — příští publish kanál po reconnectu
|
|
461
|
+
obnoví; jakékoli jiné jde na kanál `error` klienta (kolej nehlášeného selhání, jako
|
|
462
|
+
v `_reportRecoveryFailure()`) a řádkem `error` do logu. Nová událost ani nový tvar chyby
|
|
463
|
+
nevznikl.
|
|
464
|
+
|
|
465
|
+
### Fixed — `getBufferState()` hlásí změřené číslo, ne vymyšlenou nulu (d.645)
|
|
466
|
+
|
|
467
|
+
Buffer je jeden a `MessageBuffer.size()` JE `_inMemory.size()` (o dva řádky
|
|
468
|
+
vedle, `src/buffer/MessageBuffer.js`). Hlásící strana přesto brala druhé číslo
|
|
469
|
+
ze soukromého členu toho soukromého členu, za dvěma operátory, které dohromady
|
|
470
|
+
uměly jen zakrývat:
|
|
471
|
+
|
|
472
|
+
inMemory: this._publishLayer._buffer._inMemory?.size() || 0
|
|
473
|
+
|
|
474
|
+
`?.` nemůže zafungovat — konstruktor `MessageBuffer` staví `_inMemory` vždy a
|
|
475
|
+
řádek nad tím volá `_buffer.size()` bez ochrany, takže chybějící buffer spadne
|
|
476
|
+
dřív, než se k výrazu dojde. Co ta dvojice ale uměla vyrobit pro buffer, jehož
|
|
477
|
+
vnitřek není ten předpokládaný, je `inMemory: 0` vedle nenulového `size`:
|
|
478
|
+
„drženo, ale nikde" — stav, ve kterém žádný skutečný buffer být nemůže, ohlášený
|
|
479
|
+
bez hlásky a pro čtenáře nerozeznatelný od prázdného bufferu
|
|
480
|
+
(`architecture-principles.md` §3 No Fallbacks, §8 Explicit Over Implicit;
|
|
481
|
+
`automation-gates.md` §5 mlčení je vada).
|
|
482
|
+
|
|
483
|
+
Nově obě čísla pocházejí z jednoho čtení veřejného `size()`. Tvar odpovědi ani
|
|
484
|
+
hodnoty pro skutečný buffer se nemění — `{ size, inMemory }` dál, stejná čísla —
|
|
485
|
+
takže žádný čtenář se nepřepisuje.
|
|
486
|
+
|
|
487
|
+
Doklad: jednotkový tier RED→GREEN nad bufferem, který na `size()` odpoví 2 a
|
|
488
|
+
`_inMemory` nemá (dřív `{ size: 2, inMemory: 0 }`, nově `{ size: 2, inMemory: 2 }`),
|
|
489
|
+
s kontrolními případy nad skutečným bufferem (držená zpráva 1, prázdný 0).
|
|
490
|
+
Integrační tier drží zprávu ve skutečném `InMemoryBuffer` na klientu živého
|
|
491
|
+
brokeru a hlášené číslo měří proti tomu, co broker po flushi opravdu převzal; je
|
|
492
|
+
zelený na obou stranách téhle dávky a říká to ve své hlavičce — živý běh tenhle
|
|
493
|
+
rozdíl vyrobit neumí, protože skutečný `MessageBuffer` `_inMemory` vždycky má.
|
|
494
|
+
|
|
495
|
+
### Fixed — o chybě rozhoduje třída a kód, ne text hlášky (d.676, d.677, d.683, d.685c, d.696, d.698, d.711, d.1000)
|
|
496
|
+
|
|
497
|
+
Kontrakt `docs/biz/70-contracts/error-handling.md` §6: větvit podle `err.code`, nikdy podle
|
|
498
|
+
znění. Přeformulovaná věta dosud tiše měnila osud zprávy. Tyto větve nově rozhodují třídou
|
|
499
|
+
a kódem:
|
|
500
|
+
|
|
501
|
+
- **`classifyPublishError()`** (d.677, d.683): už otypovaná chyba projde beze změny;
|
|
502
|
+
`code === 404` → `QueueNotFoundError` se jménem od volajícího; `IllegalOperationError`
|
|
503
|
+
z amqplib, `ConnectionError` a `PublishError` → `TransientPublishError`; všechno ostatní
|
|
504
|
+
`PermanentPublishError`. Textová větev nezbyla ani jedna: holé chyby amqplib
|
|
505
|
+
(`'channel closed'`, `'Channel ended, no reply will be forthcoming'`) typuje místo, kde
|
|
506
|
+
přijdou — výměnová noha `_publishOnce()` odpoví na mrtvém kanálu vlastním `PublishError`
|
|
507
|
+
s původní chybou v `cause`, předkontrola fronty odliší očíslované odmítnutí (např. 405
|
|
508
|
+
RESOURCE_LOCKED — trvalé) od bezkódové chyby na mrtvém kanálu (přechodná). Mění se dva
|
|
509
|
+
tvary, které dřív rozhodovala náhoda: chyba se správnou třídou a jiným zněním dostane
|
|
510
|
+
verdikt podle třídy; cizí chyba, jejíž věta obsahuje některé z dřívějších klíčových slov,
|
|
511
|
+
je trvalá.
|
|
512
|
+
- **`isQueueUnavailableError()`** (d.676): prochází řetěz `cause` (nejvýš 8 článků,
|
|
513
|
+
smyčka na sebe sama se zastaví) a ptá se jen na `QueueNotFoundError`, `ConnectionError`,
|
|
514
|
+
`TransientPublishError`, `code === 404` a soketové kódy `ECONNREFUSED` / `ENOTFOUND` /
|
|
515
|
+
`ETIMEDOUT`. Mění se tři vstupy: fronta bez vlastníka (`kind: 'unowned'`) a publikace po
|
|
516
|
+
vyčerpání pokusů jsou nově `true`, chyba, která `404`/`NOT_FOUND` jen zmiňuje v textu, je
|
|
517
|
+
nově `false`.
|
|
518
|
+
- **Synchronní výjimka odeslání** v `_publishOnce()` (d.685c): o tom, zda počkat na
|
|
519
|
+
reconnect a zprávu poslat znovu, rozhoduje `_isChannelAlive()` kanálu, který odesílal,
|
|
520
|
+
ne slova `'Channel ended'` / `'Channel closed'`. Chyba, která ta slova jen obsahuje,
|
|
521
|
+
vyhozená na živém kanálu, se už znovu neposílá, ale vrací volajícímu.
|
|
522
|
+
- **Vypořádání a zavírání** (d.696, d.698): automatický ack v `consume()`, veřejné `ack()`
|
|
523
|
+
a `nack()` poznají zavřený či zavírající se kanál podle `instanceof IllegalOperationError`;
|
|
524
|
+
handler publisher kanálu pozná odmítnutí podle `code === AMQP_PRECONDITION_FAILED` (406,
|
|
525
|
+
nová konstanta v `utils/publishErrors.js` vedle `AMQP_NOT_FOUND`); `disconnect()` bere
|
|
526
|
+
odmítnuté zavření spojení za očekávané jen při `IllegalOperationError` BEZ zapsané
|
|
527
|
+
poruchy (`connection._closeError`, nově zapisované stejně jako u kanálů); obnova
|
|
528
|
+
frontového kanálu čte `err.cause instanceof IllegalOperationError`. Chyba, která větu
|
|
529
|
+
`'Connection closed (by client)'` jen cituje, už se nespolkne jako čekaná. Nedosažitelná
|
|
530
|
+
větev confirm callbacku („unknown delivery tag" / `PRECONDITION_FAILED` → čekat na
|
|
531
|
+
reconnect a publikovat znovu) je smazaná. `_isBrokerRefusal()` textovou půlku drží
|
|
532
|
+
a komentář říká proč: odmítnutý handshake (403 při špatném hesle) přijde jako holý
|
|
533
|
+
`Error` bez `code`.
|
|
534
|
+
- **Kód 404 má jednoho vlastníka** (d.711): `monitoring-publish.js` a
|
|
535
|
+
`transports/rabbitmqClient.js` čtou `AMQP_NOT_FOUND` z `utils/publishErrors.js` místo
|
|
536
|
+
vlastní konstanty a holé `404`. Chování beze změny.
|
|
537
|
+
- **Obnova send kanálu** (d.1000) — poslední textová větev publikační klasifikace:
|
|
538
|
+
`_ensurePublisherChannel()` dávala `TransientPublishError` podle slov `'Connection
|
|
539
|
+
closing'` / `'Connection closed'`, nově podle `instanceof IllegalOperationError`
|
|
540
|
+
(amqplib 0.10.9 tu třídu vydá z `createConfirmChannel()` nad zavírajícím i zavřeným
|
|
541
|
+
spojením, bez `code` — změřeno nad živým brokerem). Mění se dva tvary: jiná chyba, jejíž
|
|
542
|
+
věta ta slova jen obsahuje, je nově `PermanentPublishError`; `IllegalOperationError`
|
|
543
|
+
s jiným zněním (`'Channel closed'` nového kanálu, který zavřelo odcházející spojení) je
|
|
544
|
+
nově přechodná. Text v rozhodování zůstal jen v `_isBrokerRefusal()`, z důvodu, který
|
|
545
|
+
tam stojí (odmítnutý handshake nemá `code`).
|
|
546
|
+
|
|
547
|
+
### Fixed — odmítnutá konfigurace transportu není nedostupný broker (d.736b)
|
|
548
|
+
|
|
549
|
+
`BaseClient.connect()` stavěl transport uvnitř `try`, jehož `catch` balil všechno do
|
|
550
|
+
`ConnectionError` „a reachable broker … check the MQ url, credentials and network" —
|
|
551
|
+
i odmítnutí, která vydal konstruktor transportu, tedy vady konfigurace (cíl, který není
|
|
552
|
+
neprázdný řetězec, URL s vlastním `heartbeat`, `onFatal`, který není funkce, nepodporovaný
|
|
553
|
+
`type`). `ServiceWrapper._initializeMQ()` takovou chybu četl jako přechodnou a opakoval
|
|
554
|
+
`connect()` podle `wrapper.mq.connectionRetries` (~200 s čekání). Stavba transportu je nově
|
|
555
|
+
před `try` a odmítnutí projde tak, jak je — se svou třídou a hláškou, která jmenuje klíč
|
|
556
|
+
i opravu. `ConnectionError` zůstává tomu, co se stalo na spojení (ECONNREFUSED; odmítnutí
|
|
557
|
+
brokerem dál nese `MQ_CONNECTION_FATAL` / `broker-refused`).
|
|
558
|
+
|
|
559
|
+
### Fixed — čítač zpráv v letu je svázaný s doručením (d.680)
|
|
560
|
+
|
|
561
|
+
Produkce 20. 9. (dispatcher): `Prefetch utilization high: queue 'workflow.completed' has
|
|
562
|
+
43/5 messages in-flight (860%)` a číslo rostlo s každým workflow. Snížení čítače sedělo ve
|
|
563
|
+
větvích, které zprávu vypořádají přímo v `consume()`; handler, který si zprávu vypořádá
|
|
564
|
+
sám (`ack()`/`nack()`), konzument s `noAck` a předčasné návraty po smrti kanálu nechaly
|
|
565
|
+
zvýšení stát. Nově jedno zvýšení na začátku callbacku doručení a jedno snížení v jeho
|
|
566
|
+
`finally`. Veřejné API ani tvar události `prefetch:high-utilization` se nemění.
|
|
567
|
+
|
|
568
|
+
### Fixed — každý časovač klienta je v jedné evidenci `_activeTimers` (d.759e)
|
|
569
|
+
|
|
570
|
+
Opakující se časovače health a prefetch monitoringu vznikaly holým `setInterval`, mimo
|
|
571
|
+
evidenci, kterou `_clearAllTimers()` při teardownu smete a kterou sady tvrdí
|
|
572
|
+
(`_activeTimers.size`). Nově jdou přes `_setInterval()` / `_clearInterval()`;
|
|
573
|
+
`_clearAllTimers()` nejdřív zastaví oba monitory (aby jejich reference nezůstaly ukazovat
|
|
574
|
+
na mrtvý handle a příští start je neodbyl jako „already started"). `connect()` po
|
|
575
|
+
neúspěšném spojení ruší svůj časovač `_clearTimeout()`, takže v evidenci nezůstane handle,
|
|
576
|
+
který už nic nehlídá.
|
|
577
|
+
|
|
578
|
+
### Tests — komentáře v `src/**` k sadám publishe a prefetch (d.738, d.766)
|
|
579
|
+
|
|
580
|
+
`layers/PublishLayer.js` říká, proč spánek mezi pokusy publishe je holý `setTimeout`
|
|
581
|
+
a ne `_setTimeout()` transportu (čeká `publish()` volajícího, ne smyčka klienta, a
|
|
582
|
+
teardown ho ukončit nemá), se strážní sadou (d.766); `transports/rabbitmqClient.js`
|
|
583
|
+
popisuje, co po d.736a zbývá říct periodické kontrole prefetch (d.738, spolu s převodem tří
|
|
584
|
+
integračních sad na čekací kolej tieru). Chování beze změny.
|
|
585
|
+
|
|
586
|
+
### Documentation — komentáře v `src/**` bez čísel řádků a ručních verzí (d.646, d.668, d.981)
|
|
587
|
+
|
|
588
|
+
Hlavička `src/index.js` už neuvádí verzi ručně (d.646); komentáře `config/queueConfig.js`
|
|
589
|
+
už netvrdí, že `initInfrastructureQueues()` odvozuje seznam front ze sekcí (od d.668
|
|
590
|
+
vyžaduje `queues`); čísla řádků v komentářích `src/**` nahradily symboly (d.981). Chování
|
|
591
|
+
beze změny.
|
|
592
|
+
|
|
593
|
+
### Documentation — README: routing key telemetrie má tvar `telemetry.<signal>.<service_name>` (d.994b)
|
|
594
|
+
|
|
595
|
+
Řádek `telemetryLogsQueue` v tabulce `topology` tvrdil, že služby publikují na
|
|
596
|
+
`telemetry.exchange` „se svým jménem jako routing key". Změřeno: `monitoring-core` publikuje
|
|
597
|
+
logy pod `telemetry.logs.<serviceName>` (`src/logger.js`) a exportér pod
|
|
598
|
+
`telemetry.<type>.<service_name>` (`src/exporters/rabbitmq.js`), monitoring konzument váže
|
|
599
|
+
fronty vzorem `telemetry.<type>.*`. Pravdu měl komentář exportu v `src/index.js`; opravena je
|
|
600
|
+
věta v README.
|
|
601
|
+
|
|
602
|
+
### Changed — tarball už nenese `jest.config.js` ani `jest.integration.config.js` (d.1020)
|
|
603
|
+
|
|
604
|
+
- `.npmignore` je vylučuje. Spouštějí jen sady balíčku (`tests/` se nebalí) a po instalaci je nic nečte;
|
|
605
|
+
`npm pack --dry-run` ubral právě tyto dva soubory.
|
|
606
|
+
|
|
7
607
|
## [4.2.0] — 2026-09-25
|
|
8
608
|
|
|
9
609
|
### Added — služba deklaruje své rekurence a mluví s plánovačem jednou kolejí (d.833, d.840)
|
|
@@ -94,6 +694,7 @@ Při té příležitosti se rozšířil tvar `id` v záznamu rekurence: schéma
|
|
|
94
694
|
na malá písmena a pomlčky, což je přísnější než konzument (jakýkoli řetězec 1–255 znaků) a
|
|
95
695
|
odmítalo by to konvenci prvního volajícího `<source_uuid>:<cookbook_ref>`. Nově se odmítá
|
|
96
696
|
jen bílé místo, které by z klíče udělalo větu.
|
|
697
|
+
|
|
97
698
|
### Changed — port `registerRecurring()` klíč, který požadavek nedeklaruje, odmítne (d.864b)
|
|
98
699
|
|
|
99
700
|
`createSchedulerPublisher(…).registerRecurring()` bral jen pojmenované klíče a cokoli
|
|
@@ -224,6 +825,28 @@ deklarace není pokyn k vytvoření — zakládá ji dál vlastník prefixu). Pr
|
|
|
224
825
|
z toho plyne, že povinný klíč `MONITORING_RESOURCE_EXCHANGE` může po re-pinu
|
|
225
826
|
zmizet stejně jako pět jmen před ním.
|
|
226
827
|
|
|
828
|
+
### Added — výměna `telemetry.exchange` má šablonu v `queueConfig` (d.708) — doplněno 27. 9. 2026, změna vyšla bez záznamu
|
|
829
|
+
|
|
830
|
+
`telemetry.exchange` žila v `src/config/queueConfig.js` jen v komentáři, zatímco jméno mělo
|
|
831
|
+
tři vlastníky: konstantu v monitoring konzumentu (`infra/api_monitoring/src/consumer/topology.js`)
|
|
832
|
+
a výchozí hodnotu v obou publikátorech `@onlineapps/monitoring-core`. Sekce `telemetry` nese
|
|
833
|
+
nově záznam `exchange` = `{ type: 'exchange', exchangeType: 'topic', durable: true }`, bez
|
|
834
|
+
argumentů a bez dead-letter cesty; jméno se skládá `queueConfig.queueName('telemetry',
|
|
835
|
+
'exchange')`. Typ a trvanlivost jsou změřené na všech zakládajících stranách, které volaly
|
|
836
|
+
`assertExchange(<jméno>, 'topic', { durable: true })` — broker obě vlastnosti porovnává při
|
|
837
|
+
každé deklaraci, jiná hodnota by byla 406 PRECONDITION-FAILED. Klasifikace prefixu
|
|
838
|
+
`telemetry.` se nemění; producenty ani konzumenta tahle změna nepřepíná. Veřejný export
|
|
839
|
+
jména (`topology.telemetryExchange`) přišel až po vydání — viz `[Unreleased]`, d.708b.
|
|
840
|
+
|
|
841
|
+
### Changed — šablona výměny `registry.changes` nese typ `fanout` (d.712) — doplněno 27. 9. 2026, změna vyšla bez záznamu
|
|
842
|
+
|
|
843
|
+
`registry.changes` byla jediná výměna v `queueConfig` bez `exchangeType`, takže z ní
|
|
844
|
+
zakladatel deklarovat nemohl — typ je jedna ze dvou vlastností, které broker porovnává při
|
|
845
|
+
každém `assertExchange`. Šablona nově nese `exchangeType: 'fanout'`, hodnotu změřenou
|
|
846
|
+
u jediného zakladatele (`@onlineapps/conn-orch-registry`, `src/registryEventConsumer.js`:
|
|
847
|
+
`assertExchange(<jméno>, 'fanout', { durable: true })`, binding vzorem `''`). Argumenty ani
|
|
848
|
+
dead-letter cestu výměna nezískává; zakladatele změna nepřepíná.
|
|
849
|
+
|
|
227
850
|
## [4.0.1] — 2026-09-17
|
|
228
851
|
|
|
229
852
|
### Fixed — zrušení konzumenta brokerem přestalo být tiché (d.605)
|