@onlineapps/mq-client-core 2.0.1 → 3.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 +153 -0
- package/README.md +491 -8
- package/package.json +12 -8
- package/src/BaseClient.js +953 -80
- package/src/buffer/InMemoryBuffer.js +50 -9
- package/src/buffer/MessageBuffer.js +20 -52
- package/src/config/composeConfig.js +60 -0
- package/src/config/configSchema.js +398 -17
- package/src/config/defaultConfig.js +169 -15
- package/src/config/deliveryPolicy.js +165 -0
- package/src/config/queueConfig.js +738 -64
- package/src/config.js +29 -0
- package/src/defaults.js +43 -0
- package/src/index.js +91 -2
- package/src/layers/PublishLayer.js +83 -37
- package/src/monitoring/PublishMonitor.js +11 -4
- package/src/monitoring-publish.js +81 -54
- package/src/transports/rabbitmqClient.js +2698 -738
- package/src/transports/transportFactory.js +9 -2
- package/src/utils/errorHandler.js +83 -4
- package/src/utils/nearestKey.js +101 -0
- package/src/utils/publishErrors.js +95 -10
- package/src/utils/redactCredentials.js +106 -0
- package/src/utils/serializer.js +12 -2
- package/src/workers/RecoveryWorker.js +58 -81
- package/src/buffer/RedisBuffer.js +0 -57
|
@@ -1,49 +1,57 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { QueueNotFoundError } = require('../utils/publishErrors');
|
|
4
|
+
const { ValidationError } = require('../utils/errorHandler');
|
|
5
|
+
const { assertLogger } = require('@onlineapps/logger-contract');
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
|
-
* RecoveryWorker
|
|
7
|
-
*
|
|
8
|
-
* Scope:
|
|
9
|
-
* - infrastructure: connection recovery, channel recreation, consumer re-registration (NENÍ queue creation)
|
|
10
|
-
* - business: totéž + zakládání front, ale POUZE těch, které nemají vlastníka
|
|
11
|
-
* (viz Queue Creation níže). Scope říká, ČÍ ten klient je — ne co smí založit.
|
|
8
|
+
* RecoveryWorker — odpovídá na problémy publish cesty (connection recovery,
|
|
9
|
+
* hlášení chybějící fronty).
|
|
12
10
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
11
|
+
* Zakládání front — NEZAKLÁDÁ ŽÁDNOU. Fronta vzniká výhradně z deklarace:
|
|
12
|
+
* infrastrukturní ji zakládá vlastnící infra služba při bootu, business ji
|
|
13
|
+
* zakládá vlastnící služba přes `setupServiceQueues()` nad šablonou z
|
|
14
|
+
* `queueConfig` po registraci, a jméno, které `queueConfig` neklasifikuje ani
|
|
15
|
+
* jako jedno, nezakládá nikdo. Rozhodnutí vlastníka
|
|
16
|
+
* (`docs/governance/confirmations/mq-consumer-contract.md` 006) říká obojí
|
|
17
|
+
* větou: *„queues come into being only from declarations"* a *„A future family
|
|
18
|
+
* of queues that a service must create ad hoc gets a `queueConfig` template
|
|
19
|
+
* first, never a generic create-anything method."*
|
|
20
|
+
*
|
|
21
|
+
* Do d.419 tady byly DRUHÉ dveře k těm, které publish cesta v témže kroku
|
|
22
|
+
* zavírala: `createQueue()` a delegující `queueCreationCallback` zakládaly
|
|
23
|
+
* frontu bez vlastníka s `durable=true, arguments={}` — bez TTL a bez cesty pro
|
|
24
|
+
* nedoručitelné zprávy. Taková fronta je od d.259 nekonzumovatelná
|
|
25
|
+
* (`consume()` odmítne frontu, které konfigurace žádnou cestu nedeklaruje), takže
|
|
26
|
+
* ty dveře vyráběly fronty, do kterých lze publikovat a nelze z nich číst.
|
|
27
|
+
* Čtyři otázky (`change-discipline.md` § Removing something removes its
|
|
28
|
+
* declaration) jsou zodpovězené v CHANGELOGu dávky.
|
|
29
|
+
*
|
|
30
|
+
* `scope` tedy už o zakládání front nerozhoduje; říká jen, čí ten klient je.
|
|
22
31
|
*/
|
|
23
32
|
class RecoveryWorker {
|
|
24
33
|
/**
|
|
25
34
|
* @param {Object} options
|
|
26
35
|
* @param {Object} options.client - RabbitMQClient instance
|
|
27
36
|
* @param {string} [options.scope='infrastructure'] - 'infrastructure' nebo 'business'
|
|
28
|
-
* @param {
|
|
29
|
-
* Should delegate to QueueManager (e.g., setupServiceQueues or ensureQueue)
|
|
30
|
-
* @param {Object} [options.logger] - Logger
|
|
37
|
+
* @param {Object} options.logger - Logger s info/warn/error/debug (povinny)
|
|
31
38
|
*/
|
|
32
39
|
constructor(options = {}) {
|
|
33
40
|
if (!options.client) {
|
|
34
|
-
throw new
|
|
41
|
+
throw new ValidationError(
|
|
42
|
+
'[RecoveryWorker] options.client is required - Expected: the RabbitMQClient whose publish failures the worker answers. '
|
|
43
|
+
+ 'Fix: construct it as new RecoveryWorker({ client, logger }).'
|
|
44
|
+
);
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
this._client = options.client;
|
|
38
48
|
this._scope = options.scope || 'infrastructure';
|
|
39
|
-
this._logger =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Queue creation callback - delegates to QueueManager if provided
|
|
46
|
-
this._queueCreationCallback = options.queueCreationCallback || null;
|
|
49
|
+
this._logger = assertLogger(
|
|
50
|
+
'RecoveryWorker',
|
|
51
|
+
options.logger,
|
|
52
|
+
'the worker reports every recovery decision it makes about a queue that does not exist',
|
|
53
|
+
'pass options.logger (the transport forwards the one your service built)'
|
|
54
|
+
);
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
|
@@ -54,11 +62,16 @@ class RecoveryWorker {
|
|
|
54
62
|
async handleTransientError(error, context) {
|
|
55
63
|
// Transient errors jsou už bufferované v PublishLayer
|
|
56
64
|
// Recovery worker jen loguje a může triggerovat další akce
|
|
57
|
-
this._logger
|
|
65
|
+
this._logger.warn(`[RecoveryWorker] Transient error handled (buffered): ${error.message}`, context);
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
/**
|
|
61
|
-
*
|
|
69
|
+
* Report a queue that does not exist, and rethrow.
|
|
70
|
+
*
|
|
71
|
+
* Every class of name ends the same way — the worker creates nothing — and the
|
|
72
|
+
* three branches differ only in which rule the reader has to act on: who owns
|
|
73
|
+
* the queue, and how it is meant to come into being.
|
|
74
|
+
*
|
|
62
75
|
* @param {QueueNotFoundError} error
|
|
63
76
|
* @param {Object} context - { queue, buffer, options }
|
|
64
77
|
*/
|
|
@@ -69,7 +82,7 @@ class RecoveryWorker {
|
|
|
69
82
|
|
|
70
83
|
// Infrastructure queues must exist - cannot create
|
|
71
84
|
if (error.isInfrastructure) {
|
|
72
|
-
this._logger
|
|
85
|
+
this._logger.error(`[RecoveryWorker] Cannot create infrastructure queue '${error.queueName}' - must exist before publishing`);
|
|
73
86
|
throw error;
|
|
74
87
|
}
|
|
75
88
|
|
|
@@ -89,61 +102,25 @@ class RecoveryWorker {
|
|
|
89
102
|
// never meant "may create business queues", and ConnectorMQClient.js:74 sets
|
|
90
103
|
// it for every biz service, so that reading made the rule vacuous.
|
|
91
104
|
if (error.kind === 'business') {
|
|
92
|
-
this._logger
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Everything else - create if scope allows
|
|
97
|
-
if (this._queueCreationEnabled) {
|
|
98
|
-
// Check filter if provided
|
|
99
|
-
if (this._queueCreationFilter && !this._queueCreationFilter(error.queueName)) {
|
|
100
|
-
this._logger?.warn?.(`[RecoveryWorker] Queue creation filtered out for '${error.queueName}'`);
|
|
101
|
-
throw error;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
// Use queue creation callback if provided (delegates to QueueManager)
|
|
106
|
-
// Otherwise fall back to direct queue creation (backward compatibility)
|
|
107
|
-
if (this._queueCreationCallback) {
|
|
108
|
-
this._logger?.info?.(`[RecoveryWorker] Delegating queue creation to callback for '${error.queueName}'`);
|
|
109
|
-
await this._queueCreationCallback(error.queueName, context.options);
|
|
110
|
-
this._logger?.info?.(`[RecoveryWorker] ✓ Queue '${error.queueName}' created via callback`);
|
|
111
|
-
} else {
|
|
112
|
-
// Fallback: direct queue creation (for backward compatibility)
|
|
113
|
-
await this.createQueue(error.queueName, context.options);
|
|
114
|
-
this._logger?.info?.(`[RecoveryWorker] ✓ Created queue '${error.queueName}' (direct)`);
|
|
115
|
-
}
|
|
116
|
-
} catch (createErr) {
|
|
117
|
-
this._logger?.error?.(`[RecoveryWorker] Failed to create queue '${error.queueName}': ${createErr.message}`);
|
|
118
|
-
throw error;
|
|
119
|
-
}
|
|
120
|
-
} else {
|
|
121
|
-
this._logger?.error?.(`[RecoveryWorker] Cannot create queue '${error.queueName}' - scope is '${this._scope}' (queue creation disabled)`);
|
|
105
|
+
this._logger.error(`[RecoveryWorker] Cannot create business queue '${error.queueName}' - it is created by its owning service via setupServiceQueues() after registration`);
|
|
122
106
|
throw error;
|
|
123
107
|
}
|
|
124
|
-
}
|
|
125
108
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
throw new Error(`Cannot create queue ${queueName}: queue channel is not available (connection may be closed)`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const queueOptions = options.queueOptions || { durable: this._client._config.durable };
|
|
144
|
-
await this._client._queueChannel.assertQueue(queueName, queueOptions);
|
|
109
|
+
// Everything else — a name `queueConfig` classifies as neither. Until d.419
|
|
110
|
+
// this was the one branch that DID create, and it is the same "one door
|
|
111
|
+
// refuses while the other opens" the business branch above records, one class
|
|
112
|
+
// of name further out: the publish path refused the name and this handler,
|
|
113
|
+
// answering the very error that refusal raised, declared it anyway. There is
|
|
114
|
+
// no owner to start and no template to call, so the report names the only
|
|
115
|
+
// thing the reader can act on — the missing declaration.
|
|
116
|
+
this._logger.error(
|
|
117
|
+
`[RecoveryWorker] Cannot create queue '${error.queueName}' - no section of queueConfig declares it, `
|
|
118
|
+
+ 'and a queue is only ever brought into being from a declaration. Fix: add a template for this '
|
|
119
|
+
+ 'name to queueConfig and let its owner declare it (confirmation mq-consumer-contract 003), or '
|
|
120
|
+
+ 'publish to a queue that already has one.'
|
|
121
|
+
);
|
|
122
|
+
throw error;
|
|
145
123
|
}
|
|
146
124
|
}
|
|
147
125
|
|
|
148
126
|
module.exports = RecoveryWorker;
|
|
149
|
-
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Placeholder Redis buffer.
|
|
5
|
-
*
|
|
6
|
-
* Záměrně JE minimalistický: v tomto projektu zatím nemáme přímou Redis závislost v mq-client-core.
|
|
7
|
-
* Třída je připravená na budoucí rozšíření – aktuálně pouze no-op / deleguje na in-memory fallback.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
class RedisBuffer {
|
|
11
|
-
/**
|
|
12
|
-
* @param {Object} options
|
|
13
|
-
* @param {Object} [options.redisClient] - Volitelný Redis klient (s metodami set/get/lpush/brpop atd.)
|
|
14
|
-
* @param {Function} [options.logger] - Logger (console-like)
|
|
15
|
-
*/
|
|
16
|
-
constructor(options = {}) {
|
|
17
|
-
this._redis = options.redisClient || null;
|
|
18
|
-
this._logger = options.logger || console;
|
|
19
|
-
|
|
20
|
-
if (!this._redis) {
|
|
21
|
-
this._logger?.info?.(
|
|
22
|
-
'[RedisBuffer] No redisClient provided - RedisBuffer is effectively disabled (will not persist messages)'
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Přidá zprávu do persistentního bufferu.
|
|
29
|
-
* Aktuální implementace je no-op, pokud není k dispozici redisClient.
|
|
30
|
-
*/
|
|
31
|
-
async add(/* queue, buffer, options, priority */) {
|
|
32
|
-
if (!this._redis) {
|
|
33
|
-
// No-op; fallback na InMemoryBuffer zajišťuje MessageBuffer
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
// Budoucí rozšíření: implementace zápisu do Redis seznamu/streamu.
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Flushne všechny zprávy z Redis bufferu přes poskytnutý flushFn.
|
|
41
|
-
* Aktuální implementace je no-op, pokud není k dispozici redisClient.
|
|
42
|
-
*
|
|
43
|
-
* @param {Function} flushFn - async (queue, buffer, options) => void
|
|
44
|
-
* @returns {Promise<number>} - počet flushnutých zpráv
|
|
45
|
-
*/
|
|
46
|
-
async flush(flushFn) {
|
|
47
|
-
if (!this._redis) {
|
|
48
|
-
return 0;
|
|
49
|
-
}
|
|
50
|
-
// Budoucí rozšíření: čtení z Redis (např. list/stream) a volání flushFn.
|
|
51
|
-
return 0;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
module.exports = RedisBuffer;
|
|
56
|
-
|
|
57
|
-
|