@onlineapps/mq-client-core 4.0.1 → 4.2.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 +220 -0
- package/README.md +174 -0
- package/package.json +1 -1
- package/src/config/queueConfig.js +272 -21
- package/src/config/recurrencesSchema.js +121 -0
- package/src/index.js +47 -0
- package/src/monitoring-initiated.js +187 -0
- package/src/scheduler-request.js +481 -0
- package/src/transports/rabbitmqClient.js +71 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,226 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [4.2.0] — 2026-09-25
|
|
8
|
+
|
|
9
|
+
### Added — služba deklaruje své rekurence a mluví s plánovačem jednou kolejí (d.833, d.840)
|
|
10
|
+
|
|
11
|
+
`docs/architecture/scheduler.md` §7 dává klientské knihovně tvar jedné rekurence
|
|
12
|
+
a wrapperu úkol při každém bootu ten stav znovu ohlásit. Odkud služba rekurence bere,
|
|
13
|
+
je její věc — odpovídá jedním hookem (`scheduler: { recurrences: async () => [...] }`),
|
|
14
|
+
platformní konstantním seznamem, tenantové odvozené z vlastních dat (conf `scheduler` 011).
|
|
15
|
+
Knihovna nese tvar a cestu ke zprávě:
|
|
16
|
+
`scheduler.validateRecurrences(document, source)`,
|
|
17
|
+
`scheduler.buildRecurringRequest({ declaration, tenantId, workspaceId, requestedBy })`,
|
|
18
|
+
`scheduler.publishSchedulerRequest(mqClient, request)` a jméno fronty
|
|
19
|
+
`scheduler.requestQueue` (složené z deklarace v `queueConfig`, ne opsané).
|
|
20
|
+
|
|
21
|
+
Záznam rekurence má u knihovny dvě podoby, obě jsou jejím vstupem:
|
|
22
|
+
|
|
23
|
+
- **záznam deklarace** pro `validateRecurrences()` a `buildRecurringRequest({ declaration, … })`
|
|
24
|
+
ve snake_case, jak ho drží schéma `src/config/recurrencesSchema.js`:
|
|
25
|
+
`{ id, cookbook_ref, cron, timezone, input?, catch_up? }` (první čtyři povinné,
|
|
26
|
+
`additionalProperties: false`); tenancy v něm není — stavitel ji bere zvlášť jako
|
|
27
|
+
`tenantId` a `workspaceId`;
|
|
28
|
+
- **záznam portu** `createSchedulerPublisher({ mqClient, requestedBy }).registerRecurring(…)`
|
|
29
|
+
v camelCase: `{ id, cookbookRef, cron, timezone, tenantId, workspaceId, input?, catchUp? }`.
|
|
30
|
+
Port ho převede na záznam deklarace a tenancy předá staviteli.
|
|
31
|
+
|
|
32
|
+
`id` cestuje jako `idempotency_key`, takže re-registrace je upsert a každý boot smí
|
|
33
|
+
deklarovaný stav zopakovat. Dvě stejná `id` v jedné odpovědi jsou odmítnuta — tiše by se
|
|
34
|
+
přepisovala. `source` u `validateRecurrences` je povinný: každá hláška ho cituje, takže
|
|
35
|
+
výchozí hodnota by psala jméno jednoho volajícího k chybě druhého.
|
|
36
|
+
|
|
37
|
+
**Tenant a workspace dodává VOLAJÍCÍ u každé rekurence — nikdy prostředí, nikdy výchozí
|
|
38
|
+
hodnota.** Obojí je povinné a ≥ 1 (vlastník zamítl `0/0` jako platformní scope —
|
|
39
|
+
konfirmace `scheduler` 002); chybí-li, stavitel hází jménem klíče. Rekurence služba odvozuje
|
|
40
|
+
ze svých dat (conf `scheduler` 010) a hlásí je jedinou kolejí (011), takže tenancy nese
|
|
41
|
+
každý záznam: boot hook wrapperu (FÁZE 0.7b) ji u každého vráceného záznamu VYŽADUJE, protože
|
|
42
|
+
při bootu žádná obálka invokace není; port `ctx.scheduler` v handleru ji naopak doplní
|
|
43
|
+
z obálky invokace a od volajícího ji odmítne. `catch_up` je klíč záznamu portu a požadavku,
|
|
44
|
+
ne deklarace, kterou si služba drží ve svých datech (conf `scheduler` 012 a 018).
|
|
45
|
+
|
|
46
|
+
**Fixed — hlášky o chybějící tenancy posílaly čtenáře na špatné místo (d.864).** Stavitel
|
|
47
|
+
rekurence i události odmítal chybějící `tenantId` s Fix „pass the tenant the environment
|
|
48
|
+
declares for this service" a chybějící `workspaceId` s Fix „pass the workspace the cookbook
|
|
49
|
+
declares (defaults.workspace_id)". Ani jedno není zdroj, ze kterého hodnota na téhle cestě
|
|
50
|
+
jde: Fix teď říká, že se obě hodnoty podávají s každým požadavkem — rekurence je nese
|
|
51
|
+
v záznamu, který služba vrací ze svých dat, a `ctx.scheduler` je doplní z obálky invokace;
|
|
52
|
+
odjinud se nečtou. Chování se nemění, mění se jen to, kam hláška posílá. Totéž u odmítnutí
|
|
53
|
+
záznamu v `validateRecurrences()`: poznámka „tenant a workspace sem nepatří" citovala conf
|
|
54
|
+
`tier1-runner-workspace` 001, která mluví o jiném běhu; teď cituje conf `scheduler` 010 a 011
|
|
55
|
+
(d.864b).
|
|
56
|
+
|
|
57
|
+
**Prázdná odpověď je odpověď**, ne chyba: služba bez rozvrhů hook nemá, nebo vrátí prázdný
|
|
58
|
+
seznam, a wrapper svůj krok přeskočí. Odpověď, která NEOBSTOJÍ, hází — nezaregistrovaný
|
|
59
|
+
rozvrh je práce, která se nikdy nestane, a nic jiného na to neupozorní. Ze stejného důvodu
|
|
60
|
+
je odeslání **fail-fast**, na rozdíl od sousedních pomocníků monitoringu.
|
|
61
|
+
|
|
62
|
+
Zpráva se staví přesně podle kontraktu plánovače
|
|
63
|
+
(`api/infra/api_scheduler/contracts/scheduler.request.schema.json`, `additionalProperties:
|
|
64
|
+
false`) a sada to proti TOMU kontraktu ověřuje Ajvem, ne proti kopii schématu.
|
|
65
|
+
|
|
66
|
+
**d.840 — zbylé dva druhy a jedno odeslání.** Vedle `buildRecurringRequest()` přibyly
|
|
67
|
+
`scheduler.buildEventRequest({ event: { service, name }, idempotencyKey, tenantId,
|
|
68
|
+
workspaceId, requestedBy, input? })` a `scheduler.buildCancelRequest({ idempotencyKey,
|
|
69
|
+
requestedBy })`, a všechny tři druhy odesílá JEDNA funkce
|
|
70
|
+
`scheduler.publishSchedulerRequest(mqClient, request)` — druh je pole zprávy, ne druhá
|
|
71
|
+
funkce. (Nevydané `publishRecurringRequest` tím zaniká; čtenáře mimo balíček nemělo,
|
|
72
|
+
takže přechodový alias nevzniká — pravidlo 11.)
|
|
73
|
+
|
|
74
|
+
Událost NEJMENUJE recept: plánovač spustí každý recept, jehož `triggers` tu událost
|
|
75
|
+
deklarují (conf `scheduler` 004), takže producent, který by recept jmenoval, by mapoval
|
|
76
|
+
práci na něco, co nemusí vlastnit (conf `ingest-poll-trigger-rail` 001). `cancel` nenese
|
|
77
|
+
tenanta ani workspace — klíč jmenuje položku, která si kontext nese od registrace, a
|
|
78
|
+
zrušení neexistujícího klíče je výsledek (`cancel-nothing`), ne chyba.
|
|
79
|
+
|
|
80
|
+
**Klíč je vždy volajícího.** `idempotency_key` vyžaduje kořen kontraktu u všech druhů a
|
|
81
|
+
plánovač podle něj jednorázovou položku rozhodne JEDNOU (druhá událost s týmž klíčem =
|
|
82
|
+
`duplicate`), takže klíč vyrobený knihovnou by z každého opakování jednoho faktu udělal
|
|
83
|
+
druhý běh. Žádný stavitel ho proto nevymýšlí.
|
|
84
|
+
|
|
85
|
+
**Port `scheduler.createSchedulerPublisher({ mqClient, requestedBy })`** vrací tři slovesa
|
|
86
|
+
(`registerRecurring`, `publishEvent`, `cancel`) se jménem služby už rozhodnutým. Klient
|
|
87
|
+
i jméno fronty se ověřují PŘI KONSTRUKCI — služba, která ohlašuje fakty, má spadnout při
|
|
88
|
+
bootu, ne u prvního dokladu — a každé odmítnutí jmenuje slot a Fix, aby vada wiringu
|
|
89
|
+
neodpověděla místo operace. Tenancy zůstává na každém volání výslovná, protože port drží
|
|
90
|
+
dva různí volající: služba pod wrapperem (doplní ji z obálky běhu) a operátorský skript,
|
|
91
|
+
který wrapper vůbec nebootuje a pracuje řádek po řádku (měření BIZ-ingest 23. 9.).
|
|
92
|
+
|
|
93
|
+
Při té příležitosti se rozšířil tvar `id` v záznamu rekurence: schéma ho dosud omezovalo
|
|
94
|
+
na malá písmena a pomlčky, což je přísnější než konzument (jakýkoli řetězec 1–255 znaků) a
|
|
95
|
+
odmítalo by to konvenci prvního volajícího `<source_uuid>:<cookbook_ref>`. Nově se odmítá
|
|
96
|
+
jen bílé místo, které by z klíče udělalo větu.
|
|
97
|
+
### Changed — port `registerRecurring()` klíč, který požadavek nedeklaruje, odmítne (d.864b)
|
|
98
|
+
|
|
99
|
+
`createSchedulerPublisher(…).registerRecurring()` bral jen pojmenované klíče a cokoli
|
|
100
|
+
navíc tiše zahodil — takže překlep (`catch_up` místo `catchUp`) prošel a rozvrh se
|
|
101
|
+
zaregistroval bez toho, co volající chtěl. Port je teď stejně přísný jako záznam
|
|
102
|
+
deklarace, na který se převádí (`additionalProperties: false`): klíč mimo
|
|
103
|
+
`{ id, cookbookRef, cron, timezone, tenantId, workspaceId, input?, catchUp? }` je odmítnutí,
|
|
104
|
+
které ho jmenuje. Port je ve vydání nový, takže to žádného volajícího vydané verze nerozbije;
|
|
105
|
+
změřeno, že dnešní volající (boot hook a `ctx.scheduler` wrapperu, `declaredRecurrences`
|
|
106
|
+
ingestu) klíč navíc neposílají.
|
|
107
|
+
|
|
108
|
+
### Added — výměna `dlx` je v `queueConfig` deklarovaná i s typem a trvanlivostí (d.902)
|
|
109
|
+
|
|
110
|
+
`deadLetterExchange()` vracel jen jméno `dlx`; typ `direct` a `durable: true` žily jen
|
|
111
|
+
u zakladatele (`@onlineapps/conn-infra-mq` `QueueManager.setupServiceQueues()`), takže
|
|
112
|
+
CI nástroj INFRA `scripts/ci/declare-broker-exchanges.mjs` je nesl jako opsanou druhou
|
|
113
|
+
kopii (TODO INFRA 349). Nová sekce `deadLetter` deklaruje `dlx` stejným tvarem jako každá
|
|
114
|
+
jiná výměna (`type: 'exchange'`, `exchangeType: 'direct'`, `durable: true`), a protože
|
|
115
|
+
jméno na drátě prefix nemá, `SECTION_PREFIXES.deadLetter` je `''` — deklarovaný případ,
|
|
116
|
+
který `queueName()` čte výslovně (`queueName('deadLetter', 'dlx') === 'dlx'`); sekce bez
|
|
117
|
+
deklarovaného prefixu je dál odmítnuta toutéž hláškou. `deadLetterExchange()` jméno skládá
|
|
118
|
+
z téže položky. Seznam front (`infrastructurePrefixes()`, vstupy `initInfrastructureQueues`)
|
|
119
|
+
se nemění — změřeno před/po; počet deklarovaných položek 36 → 37, výměn 6 → 7, front 30.
|
|
120
|
+
|
|
121
|
+
### Fixed — handshake, který doběhne po `connectTimeout`, se zavře; nezůstane spojení bez vlastníka (INFRA 359, d.917)
|
|
122
|
+
|
|
123
|
+
`connect()` i `_reconnectWithBackoff()` (`src/transports/rabbitmqClient.js`) závodily
|
|
124
|
+
`amqp.connect()` s časovačem `connectTimeout`. Když vyhrál časovač, volající dostal chybu
|
|
125
|
+
timeoutu, ale handshake běžel dál a mohl doběhnout do úplného AMQP spojení, na které
|
|
126
|
+
žádné pole klienta neukazovalo — `disconnect()` ho tedy nezavřel, spojení viselo na
|
|
127
|
+
brokeru a proces neskončil (INFRA 359, změřeno mimo jest: mlčící TCP server a proxy
|
|
128
|
+
zdržující handshake o 11 s proti `connectTimeout` 10 000 ms). Obě místa teď závodí
|
|
129
|
+
jedním pomocníkem `_raceHandshake()`: opuštěný handshake se po doběhnutí zavře (varování
|
|
130
|
+
v logu), handshake, který po timeoutu selže, nic neotevřel. Kontrakt se nemění —
|
|
131
|
+
`connect()` odmítne timeoutem stejnou chybou jako dřív a včas doběhlé spojení zůstává
|
|
132
|
+
spojením klienta.
|
|
133
|
+
|
|
134
|
+
### Changed — TTL telemetrie 60 s → 300 s (conf telemetry-queue-ttl 001, INFRA 356)
|
|
135
|
+
|
|
136
|
+
`x-message-ttl` tří front `telemetry.logs.queue`, `telemetry.metrics.queue` a
|
|
137
|
+
`telemetry.traces.queue` (`src/config/queueConfig.js`, sekce `telemetry`) je 300000 místo
|
|
138
|
+
60000 — rozhodnutí vlastníka, varianta B (conf `telemetry-queue-ttl` 001). Proč: konzument
|
|
139
|
+
telemetrie stojí v okně deploye (stop před migracemi, `up -d` až po nich); při prvním
|
|
140
|
+
produkčním deployi `f03fad7c` bez něj byl 159 s a každá zpráva starší než 60 s expirovala
|
|
141
|
+
do `telemetry.dlq` (1414, všechny `x-death.reason = expired`), takže brána 9b dala NO-GO na
|
|
142
|
+
`dlqMaxMessages` 100 za ztrátu, kterou způsobil sám deploy. 300 s pokryje okno deploye.
|
|
143
|
+
Nemění se: `x-max-length` 100000 (paměťový strop), `telemetry.dlq` (`x-max-length` 50000,
|
|
144
|
+
bez TTL), dead-letter cesta ani práh brány 9b; fronty `delivery.*` a další s TTL 60 s
|
|
145
|
+
zůstávají. Provozní důsledek: fronta existující na brokeru s `x-message-ttl` 60000 dá při
|
|
146
|
+
bootu konzumenta na 4.2.0 406 `PRECONDITION_FAILED` (`initInfrastructureQueues` hází,
|
|
147
|
+
nikdy nemaže) — smazání a znovuvytvoření tří front v okně deploye je krok deploye INFRA
|
|
148
|
+
(conf `mq-consumer-contract` bod 4 a conf `telemetry-queue-ttl` 001 Conditions/limits).
|
|
149
|
+
|
|
150
|
+
## [4.1.0] — 2026-09-23
|
|
151
|
+
|
|
152
|
+
### Added — plánovač se dozví KONEC běhu z vlastní fronty (d.818)
|
|
153
|
+
|
|
154
|
+
`queueConfig.scheduler` nese třetí položku, `workflow.events`. Je to fronta,
|
|
155
|
+
kterou si plánovač sám naváže na existující výměnu `monitoring.workflow.fanout`
|
|
156
|
+
a čte si z ní terminální událost běhu (`completed` / `failed` / `discarded`) —
|
|
157
|
+
místo aby se ptal pozorovatele přes HTTP nebo četl databázi, která není jeho
|
|
158
|
+
(rozhodnutí vlastníka, `api/docs/governance/confirmations/scheduler.md` 007).
|
|
159
|
+
Žádný producent se nemění; od knihovny je jen deklarace.
|
|
160
|
+
|
|
161
|
+
TTL je **vyslovená, ne zděděná**: `x-message-ttl: 300000`, stejně jako u
|
|
162
|
+
sourozence `monitoring.workflow` na téže výměně. Fanout neumí filtrovat, takže
|
|
163
|
+
fronta dostane všechny workflow události platformy a plánovač většinu zahodí; co
|
|
164
|
+
stojí výpadek delší než TTL, je zapsané v 007 (události se ztratí a běh uzavře
|
|
165
|
+
per-schedule timeout, který zůstává pojistkou). Dál `x-max-length: 10000` a
|
|
166
|
+
dead-letter přes VÝCHOZÍ výměnu do `scheduler.dlq` — bez deklarované cesty by
|
|
167
|
+
`consume()` frontu vůbec nepřipojil (conf `mq-consumer-contract` 002).
|
|
168
|
+
|
|
169
|
+
Import: `queueConfig.queueName('scheduler', 'workflow.events')` složí jméno,
|
|
170
|
+
`queueConfig.declarationOptions('scheduler.workflow.events')` vrátí argumenty,
|
|
171
|
+
kterými ji služba asserteuje — druhá kopie těch čísel u služby by se s touhle
|
|
172
|
+
potkala jako 406 v den, kdy se rozejdou.
|
|
173
|
+
|
|
174
|
+
### Added — `workflow_initiated` má jednu kolej: stavitel události vedle jejího přenosu (d.819)
|
|
175
|
+
|
|
176
|
+
`monitoring.buildWorkflowInitiatedEvent()` a `monitoring.publishWorkflowInitiated()`
|
|
177
|
+
— jediný stavitel události, ze které se monitoring dozví, že běh existuje (čte
|
|
178
|
+
z ní tenanta, štítky i spouštěč a teprve jí označí běh za `active`). Dosud ji
|
|
179
|
+
stavěly dvě služby, každá po svém; rozhodnutí vlastníka
|
|
180
|
+
(`api/docs/governance/confirmations/scheduler.md` 009) dalo kolej sem, do
|
|
181
|
+
knihovny, která už vlastní přenosovou půlku téhož drátu.
|
|
182
|
+
|
|
183
|
+
Vstup: `{ workflowId, redactedCookbook, serviceName, timestamp }`. Modul
|
|
184
|
+
**neredaguje** — co je v receptu citlivé, ví jedině spec služby v registru, a
|
|
185
|
+
číst ho by knihovně přidalo závislost na specech operací; volající předává
|
|
186
|
+
observability kopii, kterou si sám postavil. A **nic nevymýšlí**: každý klíč
|
|
187
|
+
`_system` musí existovat (`hasOwnProperty`, ne pravdivost), takže
|
|
188
|
+
`tenant_uuid: null` projde jako výslovná hodnota (conf
|
|
189
|
+
`run-envelope-tenant-uuid` 001) a chybějící klíč je hlášená chyba, ne tiché
|
|
190
|
+
`null`.
|
|
191
|
+
|
|
192
|
+
Publikace jde stávající `publishToMonitoringWorkflow`, tedy fail-safe: ztracená
|
|
193
|
+
kopie se hlásí, běh se kvůli ní neodmítá. Jediná výjimka je chybějící logger —
|
|
194
|
+
bez něj by slib „každé selhání se ohlásí" neplatil.
|
|
195
|
+
|
|
196
|
+
Import: `const { monitoring } = require('@onlineapps/mq-client-core');`
|
|
197
|
+
|
|
198
|
+
### Added — jméno `monitoring.resource` má vlastníka v jádru (d.619)
|
|
199
|
+
|
|
200
|
+
Topic exchange, po které jezdí události `resource.changed.*`, byla poslední
|
|
201
|
+
stranou téhle linky bez vlastníka jména: publikující strana ji nesla jako
|
|
202
|
+
konstantu modulu (`api/infra/api_monitoring/src/consumer/watchers/
|
|
203
|
+
resourceChangedPublisher.js`, `EXCHANGE_NAME`), bindující strana jako **povinný
|
|
204
|
+
env klíč** (`MONITORING_RESOURCE_EXCHANGE` v delivery-endpointu), zatímco fronta
|
|
205
|
+
na ni navázaná (`delivery.resource.events`) má vlastníka tady od d.270/d.278.
|
|
206
|
+
Jméno, na kterém se musí shodnout tři strany jedné linky, má mít jednoho
|
|
207
|
+
vlastníka — jinak jich má tolik, kolik je míst, která ho píší, a překlep nebo
|
|
208
|
+
override na jedné straně ztrácí události tiše (týž důvod jako u fanout výměny
|
|
209
|
+
`infrastructure.health.events`, d.270).
|
|
210
|
+
|
|
211
|
+
Nově: `queueConfig.monitoring.resource` deklaruje `type: 'exchange'`,
|
|
212
|
+
`exchangeType: 'topic'`, `durable: true` — tvar, který publikující strana
|
|
213
|
+
asserteuje a který vrací i běžící broker — a `topology.monitoringResourceExchange`
|
|
214
|
+
to jméno publikuje, složené z té deklarace (`queueConfig.queueName()`), ne
|
|
215
|
+
přepsané. Import: `const { topology } = require('@onlineapps/mq-client-core');`
|
|
216
|
+
|
|
217
|
+
Doklad proti realitě, ne proti sobě: integrační tier jméno ověřuje na živém
|
|
218
|
+
brokeru (`checkExchange`) a deklarovaný typ `assertExchange`em, s kontrolním
|
|
219
|
+
případem, že tatáž výměna asserteovaná jako `fanout` končí 406 — typ je tedy
|
|
220
|
+
změřený, ne předpokládaný.
|
|
221
|
+
|
|
222
|
+
Beze změny na drátě: žádné jméno se nepřejmenovává, výměna se nezakládá (
|
|
223
|
+
deklarace není pokyn k vytvoření — zakládá ji dál vlastník prefixu). Pro INFRA
|
|
224
|
+
z toho plyne, že povinný klíč `MONITORING_RESOURCE_EXCHANGE` může po re-pinu
|
|
225
|
+
zmizet stejně jako pět jmen před ním.
|
|
226
|
+
|
|
7
227
|
## [4.0.1] — 2026-09-17
|
|
8
228
|
|
|
9
229
|
### Fixed — zrušení konzumenta brokerem přestalo být tiché (d.605)
|
package/README.md
CHANGED
|
@@ -619,8 +619,16 @@ queueConfig.isBusinessQueue('emailer.workflow'); // true
|
|
|
619
619
|
queueConfig.getInfrastructureQueueConfig('workflow.init'); // { durable, arguments }
|
|
620
620
|
queueConfig.getDeadLetterRoute('workflow.init'); // { exchange: '', routingKey: 'workflow.failed' }
|
|
621
621
|
queueConfig.isDeadLetterTarget('workflow.failed'); // true — the end of that chain
|
|
622
|
+
queueConfig.deadLetterExchange(); // 'dlx' — composed from queueConfig.deadLetter
|
|
622
623
|
```
|
|
623
624
|
|
|
625
|
+
- The platform's dead-letter exchange is declared like every other exchange here —
|
|
626
|
+
`queueConfig.deadLetter.dlx` is `{ type: 'exchange', exchangeType: 'direct', durable: true }`,
|
|
627
|
+
the shape its founder (`@onlineapps/conn-infra-mq` `setupServiceQueues()`) asserts. Its wire
|
|
628
|
+
name carries no prefix: the `deadLetter` section declares an empty one, so
|
|
629
|
+
`queueName('deadLetter', 'dlx')` is `'dlx'`, and a reader walking the `type: 'exchange'`
|
|
630
|
+
entries finds it with the rest.
|
|
631
|
+
|
|
624
632
|
- `workflow.failed` carries no `x-message-ttl`. It is read by an operator, not by a
|
|
625
633
|
service, and it has no dead-letter exchange, so a TTL there does not move a message
|
|
626
634
|
anywhere — it deletes it. See
|
|
@@ -673,6 +681,172 @@ topology.telemetryLogsQueue = 'mine'; // TypeError — the object is froz
|
|
|
673
681
|
- A service that still carries one of these names as a private literal is carrying the
|
|
674
682
|
spelling of a fact declared here; importing it changes the wire by nothing at all.
|
|
675
683
|
|
|
684
|
+
### monitoring — `workflow_initiated`, the event a run is known by
|
|
685
|
+
|
|
686
|
+
Monitoring learns that a run exists from ONE event and from nowhere else: it reads the run's
|
|
687
|
+
tenancy, its labels and its trigger out of `workflow_initiated` and marks the run active on it.
|
|
688
|
+
A service that starts runs and does not file it starts runs that are invisible for their whole
|
|
689
|
+
life. So the event has one builder, here, beside the transport half that sends it
|
|
690
|
+
([`scheduler` 009](/docs/governance/confirmations/scheduler.md)).
|
|
691
|
+
|
|
692
|
+
```javascript
|
|
693
|
+
const { monitoring } = require('@onlineapps/mq-client-core');
|
|
694
|
+
|
|
695
|
+
await monitoring.publishWorkflowInitiated({
|
|
696
|
+
mqClient,
|
|
697
|
+
logger,
|
|
698
|
+
workflowId,
|
|
699
|
+
redactedCookbook, // already redacted by the caller
|
|
700
|
+
serviceName: 'api_gateway', // who files the event
|
|
701
|
+
timestamp: startedAt, // the run's instant, not a second reading of the clock
|
|
702
|
+
context: { correlationId } // optional, for the log lines
|
|
703
|
+
});
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
`monitoring.buildWorkflowInitiatedEvent({ workflowId, redactedCookbook, serviceName, timestamp })`
|
|
707
|
+
composes the body alone, so a suite can state it without a broker. It carries exactly these ten
|
|
708
|
+
keys: `event_type` (`initiated`), `workflow_id`, `service_name`, `step_index` (`-1`), `step_id`
|
|
709
|
+
(`workflow_start`), `cookbook`, `status` (`initiated`), `tenant_uuid`, `tenant_id`, `timestamp`.
|
|
710
|
+
|
|
711
|
+
What it does NOT do, and both halves are rules rather than omissions:
|
|
712
|
+
|
|
713
|
+
- **It does not redact.** What is sensitive in a cookbook is known only to the service spec in
|
|
714
|
+
the registry, and reading that spec would give this package a dependency on the operation
|
|
715
|
+
specs. The caller hands over the observability copy it built; a path that cannot redact does
|
|
716
|
+
not file the event at all, because an empty set of sensitive fields is silent consent to an
|
|
717
|
+
unredacted copy.
|
|
718
|
+
- **It invents no value.** `service_name` and the instant are the caller's, and every key it
|
|
719
|
+
reads out of `_system` must EXIST — `hasOwnProperty`, not truthiness. `tenant_uuid: null`
|
|
720
|
+
therefore passes through as the explicit value it is (a webhook run proves itself by
|
|
721
|
+
signature and has no personal identity), while a MISSING `_system.tenant_id` or
|
|
722
|
+
`_system.tenant_uuid` is a reported error. A `|| null` here would make the two cases
|
|
723
|
+
identical and hide a broken envelope.
|
|
724
|
+
|
|
725
|
+
`publishWorkflowInitiated()` is **fail-safe**: it never throws. It is observability about a run
|
|
726
|
+
that has already happened, so refusing the run because the observer could not be told would
|
|
727
|
+
trade a real effect for a record of it — a lost copy is reported through the logger and the call
|
|
728
|
+
answers `false`. The one exception is a missing logger, which is the transport half's rule:
|
|
729
|
+
without it the promise "every failure is reported" cannot hold.
|
|
730
|
+
|
|
731
|
+
**The two builders of this package fail differently, on purpose.** This one answers `false` and
|
|
732
|
+
reports; `scheduler.publishSchedulerRequest()` below throws. A lost monitoring copy is a missing
|
|
733
|
+
record of work that happened; a lost registration is work that will never happen, and nothing
|
|
734
|
+
rings for it.
|
|
735
|
+
|
|
736
|
+
### scheduler — what a service says to the platform's owner of time
|
|
737
|
+
|
|
738
|
+
A service that runs work on a schedule ANSWERS with the recurrences it owns, through one hook
|
|
739
|
+
the wrapper asks at boot, and the wrapper re-asserts each of them as a `scheduler.request` of
|
|
740
|
+
kind `recurring` ([scheduler.md §7](/docs/architecture/scheduler.md)). Re-registration is an
|
|
741
|
+
upsert — a recurring request's `idempotency_key` IS the schedule id — so a boot that re-asserts
|
|
742
|
+
the declared state costs nothing and drifts nowhere.
|
|
743
|
+
|
|
744
|
+
ONE RAIL FOR BOTH KINDS (confirmation `scheduler` 011): the hook answers a constant list for
|
|
745
|
+
platform recurrences and a list derived from the service's own data for tenant ones. This
|
|
746
|
+
package owns the SHAPE of a record and the message built from it — never where the record comes
|
|
747
|
+
from, which is the service's business.
|
|
748
|
+
|
|
749
|
+
```javascript
|
|
750
|
+
// In the service, the hook the wrapper asks at boot (FÁZE 0.7b). Every record carries its own
|
|
751
|
+
// tenancy, derived from the service's data — at boot there is no invocation envelope to take
|
|
752
|
+
// it from (confirmations `scheduler` 002, 010):
|
|
753
|
+
new ServiceWrapper({
|
|
754
|
+
scheduler: {
|
|
755
|
+
recurrences: async () => ([
|
|
756
|
+
{ id: '31bf8bac:servant-poll', cookbookRef: 'servant-poll', cron: '0 * * * *',
|
|
757
|
+
timezone: 'Europe/Prague', tenantId: 101, workspaceId: 1, input: { source_uuid: '31bf8bac' } }
|
|
758
|
+
])
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// What the wrapper does with the answer — the port this package builds:
|
|
763
|
+
const { scheduler } = require('@onlineapps/mq-client-core');
|
|
764
|
+
const port = scheduler.createSchedulerPublisher({ mqClient, requestedBy: serviceName });
|
|
765
|
+
for (const record of answered) await port.registerRecurring(record);
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
The port record is camelCase — `{ id, cookbookRef, cron, timezone, tenantId, workspaceId,
|
|
769
|
+
input?, catchUp? }` — and the port turns it into the declaration record below, which is what
|
|
770
|
+
`validateRecurrences()` and `buildRecurringRequest({ declaration, tenantId, workspaceId,
|
|
771
|
+
requestedBy })` take. One declaration record, and nothing else:
|
|
772
|
+
|
|
773
|
+
| Key | Required | Meaning |
|
|
774
|
+
|---|---|---|
|
|
775
|
+
| `id` | ✅ | The schedule's stable id; it travels as `idempotency_key`, so two records of one id are refused |
|
|
776
|
+
| `cookbook_ref` | ✅ | The cookbook to run. Its `triggers` must name this service and the kind `recurring`, or the scheduler refuses the request |
|
|
777
|
+
| `cron` | ✅ | Five fields, the same expression the scheduler's contract takes |
|
|
778
|
+
| `timezone` | ✅ | An explicit IANA zone; there is no implicit one |
|
|
779
|
+
| `input` | – | Handed to the cookbook unchanged — a scheduled run that writes says so in the record, while the same cookbook run by hand can be given a different input |
|
|
780
|
+
| `catch_up` | – | Absent means false, and the scheduler materialises that default |
|
|
781
|
+
|
|
782
|
+
- **Tenancy is the caller's, on every recurrence — never the environment, never a default.**
|
|
783
|
+
Both ids are mandatory and ≥ 1 (the owner rejected `0/0` as a platform scope, confirmation
|
|
784
|
+
`scheduler` 002), and a missing one is refused by name. The declaration record above does not
|
|
785
|
+
carry them; the builder takes them beside it, and the port record carries them per entry.
|
|
786
|
+
Who fills them depends on where the call comes from: the boot hook's records must carry
|
|
787
|
+
them (no envelope exists at boot), while `ctx.scheduler` inside a handler fills them from the
|
|
788
|
+
invocation envelope and refuses them from the caller.
|
|
789
|
+
- **An empty answer is an answer.** A service with no recurrences has no hook, or answers an
|
|
790
|
+
empty list, and the wrapper skips its step. An answer that does NOT hold throws — a
|
|
791
|
+
recurrence nobody registered is work that never happens, and nothing else rings for it.
|
|
792
|
+
- **`validateRecurrences(document, source)` names its source**, and the name is required: every
|
|
793
|
+
refusal quotes it, so a default here would put one caller's name on another caller's mistake.
|
|
794
|
+
- **Sending is fail-fast**, unlike the monitoring helpers beside it. A lost monitoring copy
|
|
795
|
+
is a missing record of work that happened; a lost registration is work that will not happen.
|
|
796
|
+
- `scheduler.requestQueue` is the queue name, composed from the declaration in `queueConfig`
|
|
797
|
+
— a caller never types `scheduler.request`.
|
|
798
|
+
|
|
799
|
+
Two more things a service says on that queue, and one send for all of them:
|
|
800
|
+
|
|
801
|
+
```javascript
|
|
802
|
+
// THAT something happened. It names no cookbook: the scheduler starts every recipe whose
|
|
803
|
+
// `triggers` declare this event, so a producer naming a recipe would map work to something it
|
|
804
|
+
// may not own.
|
|
805
|
+
await scheduler.publishSchedulerRequest(mqClient, scheduler.buildEventRequest({
|
|
806
|
+
event: { service: 'biz-ingest', name: 'job-persisted' },
|
|
807
|
+
idempotencyKey: `biz-ingest:job-persisted:${jobId}`, // the identity of the fact
|
|
808
|
+
tenantId, workspaceId, requestedBy: serviceName,
|
|
809
|
+
input: { source_uuid } // optional, handed to every cookbook it starts
|
|
810
|
+
}));
|
|
811
|
+
|
|
812
|
+
// An item back, by the key it was registered with.
|
|
813
|
+
await scheduler.publishSchedulerRequest(mqClient, scheduler.buildCancelRequest({
|
|
814
|
+
idempotencyKey: 'servant-poll',
|
|
815
|
+
requestedBy: serviceName
|
|
816
|
+
}));
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
- **The key is always the caller's.** `idempotency_key` is required for every kind, and the
|
|
820
|
+
scheduler decides a one-shot item ONCE by it — a second `event` carrying a known key is
|
|
821
|
+
answered `duplicate`. A key this package invented per call would turn every retry of one fact
|
|
822
|
+
into a second run, so no builder makes one up.
|
|
823
|
+
- **`cancel` carries no tenancy**, and needs none: the key names an item that already holds the
|
|
824
|
+
context it was registered with. Cancelling a key that names nothing is a result rather than an
|
|
825
|
+
error — the scheduler answers `cancel-nothing` — so the builder judges nothing about it.
|
|
826
|
+
- **One send, `publishSchedulerRequest()`**, for every kind: the kind is a field of the message,
|
|
827
|
+
never a second function. It is fail-fast like the registration above, and it refuses a message
|
|
828
|
+
none of these builders produced.
|
|
829
|
+
|
|
830
|
+
A service that speaks to the scheduler more than once holds a port instead of repeating its own
|
|
831
|
+
name and the queue:
|
|
832
|
+
|
|
833
|
+
```javascript
|
|
834
|
+
const port = scheduler.createSchedulerPublisher({ mqClient, requestedBy: serviceName });
|
|
835
|
+
|
|
836
|
+
await port.registerRecurring({ id, cookbookRef, cron, timezone, tenantId, workspaceId, input, catchUp });
|
|
837
|
+
await port.publishEvent({ name: 'job-persisted', idempotencyKey: jobUuid, tenantId, workspaceId, input });
|
|
838
|
+
await port.cancel({ id });
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
- **The client and the queue are decided at construction**, not at the first call: a service that
|
|
842
|
+
announces facts has to fall over at boot rather than at its first document. On a pin whose
|
|
843
|
+
`queueConfig` declares no `scheduler` section this module refuses when it is required, earlier
|
|
844
|
+
still. Every refusal names the slot it is about and how to fix it, so a defect of the wiring can
|
|
845
|
+
never be read as the operation's own error.
|
|
846
|
+
- **Tenancy stays explicit on every call**, because two kinds of caller hold this port: a service
|
|
847
|
+
under the wrapper, which fills it from the envelope of the run, and an operator script that does
|
|
848
|
+
not boot the wrapper at all and works row by row.
|
|
849
|
+
|
|
676
850
|
### redactUrl — a connection URL that is safe to log
|
|
677
851
|
|
|
678
852
|
`RABBITMQ_URL` carries the broker account, so anything that renders it verbatim
|