@onlineapps/mq-client-core 1.0.36 → 1.0.38
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/package.json +1 -1
- package/src/buffer/InMemoryBuffer.js +118 -0
- package/src/buffer/MessageBuffer.js +107 -0
- package/src/buffer/RedisBuffer.js +57 -0
- package/src/index.js +12 -0
- package/src/layers/PublishLayer.js +242 -0
- package/src/monitoring/PublishMonitor.js +138 -0
- package/src/transports/rabbitmqClient.js +910 -183
- package/src/utils/publishErrors.js +105 -0
- package/src/workers/RecoveryWorker.js +99 -0
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Jednoduchý in-memory buffer pro zprávy, které čekají na bezpečné odeslání.
|
|
5
|
+
* - Omezený počtem položek (maxSize)
|
|
6
|
+
* - TTL pro jednotlivé položky (ttlMs)
|
|
7
|
+
*
|
|
8
|
+
* Primárně určený pro transient scénáře (health checky apod.).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class InMemoryBuffer {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {number} [options.maxSize=100] - Max počet zpráv v bufferu
|
|
15
|
+
* @param {number} [options.ttlMs=300000] - TTL jedné zprávy v ms (default 5 minut)
|
|
16
|
+
* @param {Function} [options.logger] - Volitelný logger (console-like)
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this._maxSize = options.maxSize || 100;
|
|
20
|
+
this._ttlMs = options.ttlMs || 5 * 60 * 1000;
|
|
21
|
+
this._logger = options.logger || console;
|
|
22
|
+
|
|
23
|
+
/** @type {Array<{queue:string, buffer:Buffer, options:Object, createdAt:number, priority:string}>} */
|
|
24
|
+
this._items = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Přidá zprávu do bufferu.
|
|
29
|
+
* Staré/expirující zprávy jsou průběžně čištěny, při překročení kapacity odhazujeme nejstarší.
|
|
30
|
+
*/
|
|
31
|
+
async add(queue, buffer, options = {}, priority = 'normal') {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
this._cleanupExpired(now);
|
|
34
|
+
|
|
35
|
+
if (this._items.length >= this._maxSize) {
|
|
36
|
+
const dropped = this._items.shift();
|
|
37
|
+
this._logger?.warn?.(
|
|
38
|
+
`[InMemoryBuffer] Buffer full (${this._maxSize}), dropping oldest message for queue '${dropped.queue}'`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this._items.push({
|
|
43
|
+
queue,
|
|
44
|
+
buffer,
|
|
45
|
+
options,
|
|
46
|
+
createdAt: now,
|
|
47
|
+
priority,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Flushne všechny aktuálně platné zprávy do poskytnuté publish funkce (FIFO, priority-aware).
|
|
53
|
+
* @param {Function} flushFn - async (queue, buffer, options) => void
|
|
54
|
+
* @returns {Promise<number>} - počet úspěšně flushnutých zpráv
|
|
55
|
+
*/
|
|
56
|
+
async flush(flushFn) {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
this._cleanupExpired(now);
|
|
59
|
+
|
|
60
|
+
if (this._items.length === 0) {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Zkopírujeme a vyprázdníme interní pole, abychom se vyhnuli reentrancy problémům
|
|
65
|
+
const items = this._items.slice();
|
|
66
|
+
this._items = [];
|
|
67
|
+
|
|
68
|
+
// Kritické zprávy (priority === 'critical') házíme dopředu
|
|
69
|
+
items.sort((a, b) => {
|
|
70
|
+
if (a.priority === b.priority) return a.createdAt - b.createdAt;
|
|
71
|
+
if (a.priority === 'critical') return -1;
|
|
72
|
+
if (b.priority === 'critical') return 1;
|
|
73
|
+
return a.createdAt - b.createdAt;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let flushed = 0;
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
try {
|
|
79
|
+
await flushFn(item.queue, item.buffer, item.options || {});
|
|
80
|
+
flushed++;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
// Pokud flush selže, vrátíme zprávu zpět do bufferu (na konec fronty),
|
|
83
|
+
// aby ji mohl zpracovat další pokus / worker.
|
|
84
|
+
this._logger?.warn?.(
|
|
85
|
+
`[InMemoryBuffer] Failed to flush message for queue '${item.queue}': ${err.message}`
|
|
86
|
+
);
|
|
87
|
+
await this.add(item.queue, item.buffer, item.options, item.priority);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return flushed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Aktuální velikost bufferu.
|
|
96
|
+
*/
|
|
97
|
+
size() {
|
|
98
|
+
this._cleanupExpired(Date.now());
|
|
99
|
+
return this._items.length;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_cleanupExpired(now) {
|
|
103
|
+
const before = this._items.length;
|
|
104
|
+
this._items = this._items.filter(
|
|
105
|
+
(item) => now - item.createdAt <= this._ttlMs
|
|
106
|
+
);
|
|
107
|
+
const removed = before - this._items.length;
|
|
108
|
+
if (removed > 0) {
|
|
109
|
+
this._logger?.info?.(
|
|
110
|
+
`[InMemoryBuffer] Removed ${removed} expired buffered messages`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = InMemoryBuffer;
|
|
117
|
+
|
|
118
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const InMemoryBuffer = require('./InMemoryBuffer');
|
|
4
|
+
const RedisBuffer = require('./RedisBuffer');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MessageBuffer – sjednocený interface nad in-memory a (potenciálně) persistentním bufferem.
|
|
8
|
+
*
|
|
9
|
+
* - InMemoryBuffer: pro transient zprávy (např. health checky)
|
|
10
|
+
* - RedisBuffer: pro kritické zprávy (workflow.completed apod.) – aktuálně jen připravený hook
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class MessageBuffer {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {Object} [options.inMemory] - In-memory buffer options
|
|
17
|
+
* @param {Object} [options.persistent] - Persistent buffer options (Redis)
|
|
18
|
+
* @param {boolean} [options.persistent.enabled=false] - Zapnutí persistentního bufferu
|
|
19
|
+
* @param {Object} [options.logger] - Logger (console-like)
|
|
20
|
+
*/
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this._logger = options.logger || console;
|
|
23
|
+
|
|
24
|
+
this._inMemory = new InMemoryBuffer({
|
|
25
|
+
maxSize: options.inMemory?.maxSize,
|
|
26
|
+
ttlMs: options.inMemory?.ttlMs,
|
|
27
|
+
logger: this._logger,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const persistentEnabled = !!options.persistent?.enabled;
|
|
31
|
+
this._persistent = persistentEnabled
|
|
32
|
+
? new RedisBuffer({
|
|
33
|
+
redisClient: options.persistent?.redisClient,
|
|
34
|
+
logger: this._logger,
|
|
35
|
+
})
|
|
36
|
+
: null;
|
|
37
|
+
|
|
38
|
+
if (!persistentEnabled) {
|
|
39
|
+
this._logger?.info?.(
|
|
40
|
+
'[MessageBuffer] Persistent buffer is disabled (persistent.enabled=false)'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Přidá zprávu do bufferu.
|
|
47
|
+
* @param {string} queue
|
|
48
|
+
* @param {Buffer} buffer
|
|
49
|
+
* @param {Object} options
|
|
50
|
+
* @param {Object} [meta]
|
|
51
|
+
* @param {string} [meta.priority='normal'] - 'normal' | 'critical'
|
|
52
|
+
* @param {boolean} [meta.persistent=false] - zda preferovat persistentní buffer
|
|
53
|
+
*/
|
|
54
|
+
async add(queue, buffer, options = {}, meta = {}) {
|
|
55
|
+
const priority = meta.priority || 'normal';
|
|
56
|
+
const wantPersistent = !!meta.persistent;
|
|
57
|
+
|
|
58
|
+
if (wantPersistent && this._persistent) {
|
|
59
|
+
try {
|
|
60
|
+
await this._persistent.add(queue, buffer, options, priority);
|
|
61
|
+
return;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
this._logger?.warn?.(
|
|
64
|
+
`[MessageBuffer] Failed to add message to persistent buffer, falling back to in-memory: ${err.message}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await this._inMemory.add(queue, buffer, options, priority);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Flushne všechny buffered zprávy přes poskytnutou publish funkci.
|
|
74
|
+
* @param {Function} flushFn - async (queue, buffer, options) => void
|
|
75
|
+
* @returns {Promise<{inMemory:number,persistent:number}>}
|
|
76
|
+
*/
|
|
77
|
+
async flush(flushFn) {
|
|
78
|
+
let persistentFlushed = 0;
|
|
79
|
+
if (this._persistent) {
|
|
80
|
+
try {
|
|
81
|
+
persistentFlushed = await this._persistent.flush(flushFn);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
this._logger?.error?.(
|
|
84
|
+
`[MessageBuffer] Failed to flush persistent buffer: ${err.message}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const inMemoryFlushed = await this._inMemory.flush(flushFn);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
inMemory: inMemoryFlushed,
|
|
93
|
+
persistent: persistentFlushed,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Aktuální velikost in-memory bufferu.
|
|
99
|
+
*/
|
|
100
|
+
size() {
|
|
101
|
+
return this._inMemory.size();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = MessageBuffer;
|
|
106
|
+
|
|
107
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
|
package/src/index.js
CHANGED
|
@@ -19,6 +19,12 @@ const {
|
|
|
19
19
|
ConsumeError,
|
|
20
20
|
SerializationError,
|
|
21
21
|
} = require('./utils/errorHandler');
|
|
22
|
+
const {
|
|
23
|
+
TransientPublishError,
|
|
24
|
+
PermanentPublishError,
|
|
25
|
+
QueueNotFoundError,
|
|
26
|
+
classifyPublishError,
|
|
27
|
+
} = require('./utils/publishErrors');
|
|
22
28
|
|
|
23
29
|
// Export BaseClient as default (constructor), with additional named exports
|
|
24
30
|
// NOTE: When destructuring, use: const { BaseClient } = require('@onlineapps/mq-client-core');
|
|
@@ -34,4 +40,10 @@ module.exports.errors = {
|
|
|
34
40
|
ConsumeError,
|
|
35
41
|
SerializationError,
|
|
36
42
|
};
|
|
43
|
+
module.exports.publishErrors = {
|
|
44
|
+
TransientPublishError,
|
|
45
|
+
PermanentPublishError,
|
|
46
|
+
QueueNotFoundError,
|
|
47
|
+
classifyPublishError,
|
|
48
|
+
};
|
|
37
49
|
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
TransientPublishError,
|
|
5
|
+
PermanentPublishError,
|
|
6
|
+
QueueNotFoundError,
|
|
7
|
+
} = require('../utils/publishErrors');
|
|
8
|
+
const MessageBuffer = require('../buffer/MessageBuffer');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PublishLayer - čistá vrstva pro publish s retry + buffer
|
|
12
|
+
*
|
|
13
|
+
* Odděluje:
|
|
14
|
+
* - Retry logiku (exponential backoff)
|
|
15
|
+
* - Bufferování při transient chybách
|
|
16
|
+
* - Delegaci na RabbitMQClient._publishOnce() (čisté publish bez retry)
|
|
17
|
+
*/
|
|
18
|
+
class PublishLayer {
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} options
|
|
21
|
+
* @param {Object} options.client - Instance RabbitMQClient
|
|
22
|
+
* @param {Object} [options.logger] - Logger
|
|
23
|
+
* @param {Object} [options.bufferConfig] - Konfigurace pro MessageBuffer
|
|
24
|
+
* @param {boolean} [options.retryEnabled=true] - Zapnout retry
|
|
25
|
+
* @param {number} [options.maxRetries=3] - Max počet pokusů
|
|
26
|
+
* @param {number} [options.retryBaseDelay=100] - Base delay v ms
|
|
27
|
+
* @param {number} [options.retryMaxDelay=5000] - Max delay v ms
|
|
28
|
+
* @param {number} [options.retryBackoffMultiplier=2] - Backoff multiplikátor
|
|
29
|
+
*/
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
if (!options.client) {
|
|
32
|
+
throw new Error('PublishLayer requires a client instance');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this._client = options.client;
|
|
36
|
+
this._logger = options.logger || console;
|
|
37
|
+
|
|
38
|
+
// Retry konfigurace - bere z client config nebo options
|
|
39
|
+
this._retryEnabled = options.retryEnabled !== undefined
|
|
40
|
+
? options.retryEnabled
|
|
41
|
+
: (this._client._publishRetryEnabled !== false);
|
|
42
|
+
this._maxRetries = options.maxRetries || this._client._publishMaxRetries || 3;
|
|
43
|
+
this._retryBaseDelay = options.retryBaseDelay || this._client._publishRetryBaseDelay || 100;
|
|
44
|
+
this._retryMaxDelay = options.retryMaxDelay || this._client._publishRetryMaxDelay || 5000;
|
|
45
|
+
this._retryBackoffMultiplier = options.retryBackoffMultiplier || this._client._publishRetryBackoffMultiplier || 2;
|
|
46
|
+
|
|
47
|
+
// Buffer
|
|
48
|
+
this._buffer = new MessageBuffer({
|
|
49
|
+
inMemory: options.bufferConfig?.inMemory,
|
|
50
|
+
persistent: options.bufferConfig?.persistent,
|
|
51
|
+
logger: this._logger,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Publish s retry + buffer logikou
|
|
57
|
+
* @param {string} queue
|
|
58
|
+
* @param {Buffer} buffer
|
|
59
|
+
* @param {Object} [options]
|
|
60
|
+
*/
|
|
61
|
+
async publish(queue, buffer, options = {}) {
|
|
62
|
+
const priority = options.priority || 'normal';
|
|
63
|
+
const usePersistentBuffer =
|
|
64
|
+
priority === 'critical' && !!this._client._config?.persistentBufferEnabled;
|
|
65
|
+
|
|
66
|
+
if (this._retryEnabled) {
|
|
67
|
+
return await this._publishWithRetry(queue, buffer, options, priority, usePersistentBuffer);
|
|
68
|
+
} else {
|
|
69
|
+
return await this._publishOnce(queue, buffer, options, priority, usePersistentBuffer);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Single publish attempt - deleguje na RabbitMQClient._publishOnce()
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
async _publishOnce(queue, buffer, options, priority, usePersistentBuffer) {
|
|
78
|
+
try {
|
|
79
|
+
await this._client._publishOnce(queue, buffer, options);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return await this._handlePublishError(err, queue, buffer, options, priority, usePersistentBuffer);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Publish s retry logikou
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
async _publishWithRetry(queue, buffer, options, priority, usePersistentBuffer) {
|
|
90
|
+
let lastError = null;
|
|
91
|
+
let attempt = 0;
|
|
92
|
+
|
|
93
|
+
while (attempt < this._maxRetries) {
|
|
94
|
+
attempt++;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Track attempt (i při prvním pokusu pro monitoring)
|
|
98
|
+
this._client.emit('publish:retry', {
|
|
99
|
+
queue,
|
|
100
|
+
attempt,
|
|
101
|
+
maxRetries: this._maxRetries,
|
|
102
|
+
lastError: lastError?.message
|
|
103
|
+
});
|
|
104
|
+
if (attempt > 1) {
|
|
105
|
+
this._logger?.log?.(`[PublishLayer] Retry attempt ${attempt}/${this._maxRetries} for queue "${queue}"`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await this._client._publishOnce(queue, buffer, options);
|
|
109
|
+
|
|
110
|
+
// Track success (always emit, even for first attempt)
|
|
111
|
+
this._client.emit('publish:success', {
|
|
112
|
+
queue,
|
|
113
|
+
attempt,
|
|
114
|
+
totalAttempts: attempt
|
|
115
|
+
});
|
|
116
|
+
if (attempt > 1) {
|
|
117
|
+
this._logger?.log?.(`[PublishLayer] ✓ Published successfully after ${attempt} attempts for queue "${queue}"`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return; // Success
|
|
121
|
+
|
|
122
|
+
} catch (err) {
|
|
123
|
+
lastError = err;
|
|
124
|
+
|
|
125
|
+
// Non-retryable errors - fail immediately
|
|
126
|
+
if (err instanceof PermanentPublishError || err instanceof QueueNotFoundError) {
|
|
127
|
+
this._client.emit('publish:failed', {
|
|
128
|
+
queue,
|
|
129
|
+
attempt,
|
|
130
|
+
error: err.message,
|
|
131
|
+
retryable: false
|
|
132
|
+
});
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Transient errors - check if we should retry or buffer
|
|
137
|
+
if (err instanceof TransientPublishError) {
|
|
138
|
+
// If we have more attempts, retry
|
|
139
|
+
if (attempt < this._maxRetries) {
|
|
140
|
+
// Wait for reconnection if in progress
|
|
141
|
+
if (this._client._reconnecting) {
|
|
142
|
+
this._logger?.log?.(`[PublishLayer] Connection reconnecting, waiting before retry ${attempt + 1}/${this._maxRetries}...`);
|
|
143
|
+
try {
|
|
144
|
+
await this._client._waitForReconnection();
|
|
145
|
+
this._logger?.log?.(`[PublishLayer] ✓ Reconnection completed, proceeding with retry ${attempt + 1}/${this._maxRetries}`);
|
|
146
|
+
continue; // Retry immediately after reconnect
|
|
147
|
+
} catch (reconnectErr) {
|
|
148
|
+
// Reconnection failed - buffer and fail
|
|
149
|
+
await this._bufferMessage(queue, buffer, options, priority, usePersistentBuffer, err);
|
|
150
|
+
this._client.emit('publish:failed', {
|
|
151
|
+
queue,
|
|
152
|
+
attempt,
|
|
153
|
+
error: `Reconnection failed: ${reconnectErr.message}`,
|
|
154
|
+
retryable: false,
|
|
155
|
+
reconnectFailed: true
|
|
156
|
+
});
|
|
157
|
+
throw new Error(`Publish failed: reconnection failed after ${attempt} attempts for queue "${queue}": ${reconnectErr.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Exponential backoff
|
|
162
|
+
const delay = Math.min(
|
|
163
|
+
this._retryBaseDelay * Math.pow(this._retryBackoffMultiplier, attempt - 1),
|
|
164
|
+
this._retryMaxDelay
|
|
165
|
+
);
|
|
166
|
+
this._logger?.warn?.(`[PublishLayer] Retryable error for queue "${queue}" (attempt ${attempt}/${this._maxRetries}), waiting ${delay}ms before retry: ${err.message}`);
|
|
167
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Max retries exceeded - buffer and fail
|
|
172
|
+
await this._bufferMessage(queue, buffer, options, priority, usePersistentBuffer, err);
|
|
173
|
+
this._client.emit('publish:failed', {
|
|
174
|
+
queue,
|
|
175
|
+
attempt,
|
|
176
|
+
error: err.message,
|
|
177
|
+
retryable: true,
|
|
178
|
+
maxRetriesExceeded: true
|
|
179
|
+
});
|
|
180
|
+
throw new Error(`Publish failed after ${attempt} attempts for queue "${queue}": ${err.message}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Unknown error - fail immediately
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
throw lastError || new Error(`Publish failed for queue "${queue}" after ${attempt} attempts`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Handle publish error - buffer transient errors
|
|
193
|
+
* @private
|
|
194
|
+
*/
|
|
195
|
+
async _handlePublishError(err, queue, buffer, options, priority, usePersistentBuffer) {
|
|
196
|
+
if (err instanceof QueueNotFoundError) {
|
|
197
|
+
this._logger?.error?.(`[PublishLayer] QueueNotFoundError for '${queue}' (infra=${err.isInfrastructure}): ${err.message}`);
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (err instanceof TransientPublishError) {
|
|
202
|
+
await this._bufferMessage(queue, buffer, options, priority, usePersistentBuffer, err);
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Buffer message při transient error
|
|
211
|
+
* @private
|
|
212
|
+
*/
|
|
213
|
+
async _bufferMessage(queue, buffer, options, priority, usePersistentBuffer, err) {
|
|
214
|
+
try {
|
|
215
|
+
await this._buffer.add(queue, buffer, options, {
|
|
216
|
+
priority,
|
|
217
|
+
persistent: usePersistentBuffer,
|
|
218
|
+
});
|
|
219
|
+
this._logger?.warn?.(`[PublishLayer] Buffered message for queue '${queue}' due to transient error: ${err.message}`);
|
|
220
|
+
this._client.emit('publish:buffered', { queue });
|
|
221
|
+
} catch (bufferErr) {
|
|
222
|
+
this._logger?.error?.(`[PublishLayer] Failed to buffer message for queue '${queue}': ${bufferErr.message}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Flush buffered messages po reconnectu
|
|
228
|
+
*/
|
|
229
|
+
async flushBuffered(flushOptions = {}) {
|
|
230
|
+
const result = await this._buffer.flush(async (queue, buf, opts) => {
|
|
231
|
+
const mergedOptions = Object.assign({}, opts, flushOptions);
|
|
232
|
+
await this._client._publishOnce(queue, buf, mergedOptions);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
this._logger?.info?.(`[PublishLayer] Flushed buffered messages (inMemory=${result.inMemory}, persistent=${result.persistent})`);
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = PublishLayer;
|
|
241
|
+
|
|
242
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PublishMonitor - tracking a metriky pro publish operace
|
|
5
|
+
*/
|
|
6
|
+
class PublishMonitor {
|
|
7
|
+
/**
|
|
8
|
+
* @param {Object} options
|
|
9
|
+
* @param {Object} [options.logger] - Logger
|
|
10
|
+
*/
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this._logger = options.logger || console;
|
|
13
|
+
|
|
14
|
+
// Metrics
|
|
15
|
+
this._metrics = {
|
|
16
|
+
attempts: 0,
|
|
17
|
+
successes: 0,
|
|
18
|
+
failures: 0,
|
|
19
|
+
retries: 0,
|
|
20
|
+
buffered: 0,
|
|
21
|
+
flushed: 0,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Per-queue metrics
|
|
25
|
+
this._queueMetrics = new Map(); // queue -> { attempts, successes, failures, retries }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Track publish attempt
|
|
30
|
+
*/
|
|
31
|
+
trackAttempt(queue) {
|
|
32
|
+
this._metrics.attempts++;
|
|
33
|
+
this._updateQueueMetrics(queue, 'attempts');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Track publish success
|
|
38
|
+
*/
|
|
39
|
+
trackSuccess(queue, attempt = 1) {
|
|
40
|
+
this._metrics.successes++;
|
|
41
|
+
if (attempt > 1) {
|
|
42
|
+
this._metrics.retries += (attempt - 1);
|
|
43
|
+
}
|
|
44
|
+
this._updateQueueMetrics(queue, 'successes', attempt);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Track publish failure
|
|
49
|
+
*/
|
|
50
|
+
trackFailure(queue, retryable = false) {
|
|
51
|
+
this._metrics.failures++;
|
|
52
|
+
this._updateQueueMetrics(queue, 'failures', 0, retryable);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Track buffered message
|
|
57
|
+
*/
|
|
58
|
+
trackBuffered(queue) {
|
|
59
|
+
this._metrics.buffered++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Track flushed messages
|
|
64
|
+
*/
|
|
65
|
+
trackFlushed(count) {
|
|
66
|
+
this._metrics.flushed += count;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get metrics
|
|
71
|
+
*/
|
|
72
|
+
getMetrics() {
|
|
73
|
+
return {
|
|
74
|
+
...this._metrics,
|
|
75
|
+
queues: Array.from(this._queueMetrics.entries()).map(([queue, metrics]) => ({
|
|
76
|
+
queue,
|
|
77
|
+
...metrics
|
|
78
|
+
}))
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get Prometheus metrics format
|
|
84
|
+
*/
|
|
85
|
+
getPrometheusMetrics() {
|
|
86
|
+
const lines = [];
|
|
87
|
+
lines.push(`# HELP mq_publish_attempts Total publish attempts`);
|
|
88
|
+
lines.push(`# TYPE mq_publish_attempts counter`);
|
|
89
|
+
lines.push(`mq_publish_attempts ${this._metrics.attempts}`);
|
|
90
|
+
|
|
91
|
+
lines.push(`# HELP mq_publish_successes Total successful publishes`);
|
|
92
|
+
lines.push(`# TYPE mq_publish_successes counter`);
|
|
93
|
+
lines.push(`mq_publish_successes ${this._metrics.successes}`);
|
|
94
|
+
|
|
95
|
+
lines.push(`# HELP mq_publish_failures Total failed publishes`);
|
|
96
|
+
lines.push(`# TYPE mq_publish_failures counter`);
|
|
97
|
+
lines.push(`mq_publish_failures ${this._metrics.failures}`);
|
|
98
|
+
|
|
99
|
+
lines.push(`# HELP mq_publish_retries Total retry attempts`);
|
|
100
|
+
lines.push(`# TYPE mq_publish_retries counter`);
|
|
101
|
+
lines.push(`mq_publish_retries ${this._metrics.retries}`);
|
|
102
|
+
|
|
103
|
+
lines.push(`# HELP mq_publish_buffered Total buffered messages`);
|
|
104
|
+
lines.push(`# TYPE mq_publish_buffered counter`);
|
|
105
|
+
lines.push(`mq_publish_buffered ${this._metrics.buffered}`);
|
|
106
|
+
|
|
107
|
+
lines.push(`# HELP mq_publish_flushed Total flushed messages`);
|
|
108
|
+
lines.push(`# TYPE mq_publish_flushed counter`);
|
|
109
|
+
lines.push(`mq_publish_flushed ${this._metrics.flushed}`);
|
|
110
|
+
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Update queue metrics
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
_updateQueueMetrics(queue, type, attempt = 1, retryable = false) {
|
|
119
|
+
if (!this._queueMetrics.has(queue)) {
|
|
120
|
+
this._queueMetrics.set(queue, {
|
|
121
|
+
attempts: 0,
|
|
122
|
+
successes: 0,
|
|
123
|
+
failures: 0,
|
|
124
|
+
retries: 0,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const metrics = this._queueMetrics.get(queue);
|
|
129
|
+
metrics[type]++;
|
|
130
|
+
|
|
131
|
+
if (type === 'successes' && attempt > 1) {
|
|
132
|
+
metrics.retries += (attempt - 1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = PublishMonitor;
|
|
138
|
+
|