@koala42/redis-highway 0.1.8 → 0.1.10
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/dist/batch-worker.d.ts +13 -2
- package/dist/batch-worker.js +86 -66
- package/dist/batch-worker.spec.js +2 -1
- package/dist/lua.d.ts +2 -2
- package/dist/lua.js +20 -24
- package/dist/producer.js +1 -9
- package/dist/queue.spec.js +43 -14
- package/dist/stream-message-entity.d.ts +2 -3
- package/dist/stream-message-entity.js +11 -4
- package/dist/worker.d.ts +5 -1
- package/dist/worker.js +63 -31
- package/package.json +1 -1
package/dist/batch-worker.d.ts
CHANGED
|
@@ -5,16 +5,27 @@ export declare abstract class BatchWorker<T extends Record<string, unknown>> {
|
|
|
5
5
|
protected streamName: string;
|
|
6
6
|
protected batchSize: number;
|
|
7
7
|
protected concurrency: number;
|
|
8
|
+
protected maxFetchSize: number;
|
|
8
9
|
protected maxRetries: number;
|
|
9
10
|
protected blockTimeMs: number;
|
|
11
|
+
protected maxFetchCount: number;
|
|
12
|
+
protected claimIntervalMs: number;
|
|
13
|
+
protected minIdleTimeMs: number;
|
|
10
14
|
private isRunning;
|
|
11
15
|
private activeCount;
|
|
12
|
-
private readonly events;
|
|
13
16
|
private keys;
|
|
17
|
+
private blockingRedis;
|
|
18
|
+
private readonly events;
|
|
14
19
|
private readonly consumerId;
|
|
15
|
-
constructor(redis: Redis, groupName: string, streamName: string, batchSize?: number,
|
|
20
|
+
constructor(redis: Redis, groupName: string, streamName: string, batchSize?: number, // How many jobs are passed to the process function (max)
|
|
21
|
+
concurrency?: number, // How many concurrent loops should run
|
|
22
|
+
maxFetchSize?: number, // How many jobs are fetched at once from redis stream
|
|
23
|
+
maxRetries?: number, blockTimeMs?: number, // How long should the blocking redis wait for logs from stream
|
|
24
|
+
maxFetchCount?: number, claimIntervalMs?: number, // Check for stuck jobs every minute
|
|
25
|
+
minIdleTimeMs?: number);
|
|
16
26
|
start(): Promise<void>;
|
|
17
27
|
stop(): Promise<void>;
|
|
28
|
+
private autoClaimLoop;
|
|
18
29
|
private fetchLoop;
|
|
19
30
|
/**
|
|
20
31
|
* Spawn worker for current processing
|
package/dist/batch-worker.js
CHANGED
|
@@ -7,14 +7,23 @@ const stream_message_entity_1 = require("./stream-message-entity");
|
|
|
7
7
|
const lua_1 = require("./lua");
|
|
8
8
|
const uuid_1 = require("uuid");
|
|
9
9
|
class BatchWorker {
|
|
10
|
-
constructor(redis, groupName, streamName, batchSize = 10,
|
|
10
|
+
constructor(redis, groupName, streamName, batchSize = 10, // How many jobs are passed to the process function (max)
|
|
11
|
+
concurrency = 1, // How many concurrent loops should run
|
|
12
|
+
maxFetchSize = 20, // How many jobs are fetched at once from redis stream
|
|
13
|
+
maxRetries = 3, blockTimeMs = 2000, // How long should the blocking redis wait for logs from stream
|
|
14
|
+
maxFetchCount = 5000, claimIntervalMs = 60000, // Check for stuck jobs every minute
|
|
15
|
+
minIdleTimeMs = 120000) {
|
|
11
16
|
this.redis = redis;
|
|
12
17
|
this.groupName = groupName;
|
|
13
18
|
this.streamName = streamName;
|
|
14
19
|
this.batchSize = batchSize;
|
|
15
20
|
this.concurrency = concurrency;
|
|
21
|
+
this.maxFetchSize = maxFetchSize;
|
|
16
22
|
this.maxRetries = maxRetries;
|
|
17
23
|
this.blockTimeMs = blockTimeMs;
|
|
24
|
+
this.maxFetchCount = maxFetchCount;
|
|
25
|
+
this.claimIntervalMs = claimIntervalMs;
|
|
26
|
+
this.minIdleTimeMs = minIdleTimeMs;
|
|
18
27
|
this.isRunning = false;
|
|
19
28
|
this.activeCount = 0;
|
|
20
29
|
this.events = new events_1.EventEmitter();
|
|
@@ -24,6 +33,7 @@ class BatchWorker {
|
|
|
24
33
|
}
|
|
25
34
|
this.events.setMaxListeners(100);
|
|
26
35
|
this.keys = new keys_1.KeyManager(streamName);
|
|
36
|
+
this.blockingRedis = this.redis.duplicate();
|
|
27
37
|
}
|
|
28
38
|
async start() {
|
|
29
39
|
if (this.isRunning) {
|
|
@@ -39,14 +49,62 @@ class BatchWorker {
|
|
|
39
49
|
}
|
|
40
50
|
}
|
|
41
51
|
this.fetchLoop();
|
|
52
|
+
this.autoClaimLoop();
|
|
42
53
|
}
|
|
43
54
|
async stop() {
|
|
44
55
|
this.isRunning = false;
|
|
45
56
|
this.events.emit('job_finished');
|
|
57
|
+
if (this.blockingRedis) {
|
|
58
|
+
try {
|
|
59
|
+
await this.blockingRedis.quit();
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
// whatever
|
|
63
|
+
}
|
|
64
|
+
}
|
|
46
65
|
while (this.activeCount > 0) {
|
|
47
66
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
48
67
|
}
|
|
49
68
|
}
|
|
69
|
+
async autoClaimLoop() {
|
|
70
|
+
while (this.isRunning) {
|
|
71
|
+
try {
|
|
72
|
+
await new Promise(resolve => setTimeout(resolve, this.claimIntervalMs));
|
|
73
|
+
if (!this.isRunning) {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
let cursor = '0-0';
|
|
77
|
+
let continueClaiming = true;
|
|
78
|
+
while (continueClaiming && this.isRunning) {
|
|
79
|
+
const result = await this.redis.xautoclaim(this.streamName, this.groupName, this.getConsumerName(), this.minIdleTimeMs, cursor, 'COUNT', this.batchSize);
|
|
80
|
+
if (!result || !result.length) {
|
|
81
|
+
continueClaiming = false;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
const [nextCursor, messages] = result;
|
|
85
|
+
cursor = nextCursor;
|
|
86
|
+
if (messages && messages.length > 0) {
|
|
87
|
+
console.log(`[${this.groupName}] Recovered ${messages.length} stuck messages`);
|
|
88
|
+
if (this.activeCount < this.concurrency) {
|
|
89
|
+
continueClaiming = false;
|
|
90
|
+
}
|
|
91
|
+
this.spawnWorker(messages);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
continueClaiming = false;
|
|
95
|
+
}
|
|
96
|
+
if (nextCursor === '0-0') {
|
|
97
|
+
continueClaiming = false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
if (this.isRunning) {
|
|
103
|
+
console.error(`[${this.groupName}] Auto claim err:`, e.message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
50
108
|
async fetchLoop() {
|
|
51
109
|
while (this.isRunning) {
|
|
52
110
|
const freeSlots = this.concurrency - this.activeCount;
|
|
@@ -54,9 +112,10 @@ class BatchWorker {
|
|
|
54
112
|
await new Promise((resolve) => this.events.once('job_finished', resolve));
|
|
55
113
|
continue;
|
|
56
114
|
}
|
|
57
|
-
const
|
|
115
|
+
const calculatedCount = freeSlots * this.batchSize;
|
|
116
|
+
const itemsCount = Math.min(calculatedCount, this.maxFetchCount);
|
|
58
117
|
try {
|
|
59
|
-
const results = await this.
|
|
118
|
+
const results = await this.blockingRedis.xreadgroup('GROUP', this.groupName, this.getConsumerName(), 'COUNT', itemsCount, 'BLOCK', this.blockTimeMs, 'STREAMS', this.streamName, '>');
|
|
60
119
|
if (!results) {
|
|
61
120
|
continue;
|
|
62
121
|
}
|
|
@@ -67,8 +126,10 @@ class BatchWorker {
|
|
|
67
126
|
}
|
|
68
127
|
}
|
|
69
128
|
catch (err) {
|
|
70
|
-
|
|
71
|
-
|
|
129
|
+
if (this.isRunning) { // Quicker grace shutdown
|
|
130
|
+
console.error(`[${this.groupName}] Fetch Error: `, err);
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
132
|
+
}
|
|
72
133
|
}
|
|
73
134
|
}
|
|
74
135
|
}
|
|
@@ -103,79 +164,43 @@ class BatchWorker {
|
|
|
103
164
|
}
|
|
104
165
|
await pipeline.exec();
|
|
105
166
|
}
|
|
106
|
-
// If no messsages need to be process, return. Fires job finished event for another loop to pickup next logs
|
|
107
167
|
if (!messages.length) {
|
|
108
168
|
return;
|
|
109
169
|
}
|
|
110
|
-
|
|
111
|
-
const pipeline = this.redis.pipeline();
|
|
112
|
-
for (const message of messages) {
|
|
113
|
-
pipeline.get(this.keys.getJobDataKey(message.messageUuid));
|
|
114
|
-
}
|
|
115
|
-
const response = await pipeline.exec();
|
|
116
|
-
// TODO: Add error handling
|
|
117
|
-
if (!response) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
// Parse job data into message entities (lol, titties)
|
|
121
|
-
messages.forEach((message, index) => {
|
|
122
|
-
const foundData = response[index] || null;
|
|
123
|
-
if (!foundData) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
const [error, data] = foundData;
|
|
127
|
-
if (error) {
|
|
128
|
-
console.error(`[${this.groupName}] Failed getting job data err: `, error);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (!data) {
|
|
132
|
-
console.error(`[${this.groupName}] Data not found for job`);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
message.data = JSON.parse(data);
|
|
136
|
-
});
|
|
137
|
-
const messagesData = [];
|
|
138
|
-
const messagesToFinalize = [];
|
|
139
|
-
messages.forEach((message) => {
|
|
140
|
-
messagesToFinalize.push(message);
|
|
141
|
-
if (message.data) {
|
|
142
|
-
messagesData.push(message.data);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
// TODO improve error handling
|
|
146
|
-
if (!messagesData.length) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
170
|
+
const messagesData = messages.map((msg) => msg.data);
|
|
149
171
|
try {
|
|
150
172
|
await this.process(messagesData);
|
|
151
|
-
await this.finalize(
|
|
173
|
+
await this.finalize(messages);
|
|
152
174
|
}
|
|
153
175
|
catch (err) {
|
|
154
|
-
console.error(`[${this.groupName}]
|
|
176
|
+
console.error(`[${this.groupName}] Processing failed`, err);
|
|
155
177
|
await this.handleFailure(messages, err.message);
|
|
156
178
|
}
|
|
157
179
|
}
|
|
158
180
|
async handleFailure(messages, errorMessage) {
|
|
159
181
|
const pipeline = this.redis.pipeline();
|
|
160
|
-
//
|
|
182
|
+
// ack
|
|
161
183
|
for (const message of messages) {
|
|
162
184
|
pipeline.xack(this.streamName, this.groupName, message.streamMessageId);
|
|
163
185
|
}
|
|
164
186
|
const messagesToDlq = [];
|
|
165
187
|
for (const message of messages) {
|
|
166
|
-
if (message.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
188
|
+
if (message.routes.includes(this.groupName)) {
|
|
189
|
+
if (message.retryCount < this.maxRetries && message.data) {
|
|
190
|
+
console.log(`[${this.groupName}] Retrying job ${message.messageUuid} attempt ${message.retryCount + 1}/${this.maxRetries}`);
|
|
191
|
+
const payloadString = JSON.stringify(message.data);
|
|
192
|
+
pipeline.xadd(this.streamName, '*', 'id', message.messageUuid, 'target', this.groupName, 'retryCount', message.retryCount + 1, 'data', payloadString);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
console.error(`[${this.groupName}] Job ${message.messageUuid} run out of retries. Moving to DLQ`);
|
|
196
|
+
messagesToDlq.push(message);
|
|
197
|
+
pipeline.xadd(this.keys.getDlqStreamKey(), '*', 'id', message.messageUuid, 'group', this.groupName, 'error', errorMessage, 'payload', message.data ? JSON.stringify(message.data) : 'MISSING', 'failedAt', Date.now());
|
|
198
|
+
}
|
|
173
199
|
}
|
|
174
200
|
else {
|
|
175
|
-
|
|
176
|
-
console.error(`[${this.groupName}] Job ${message.messageUuid} exhausted retries. Moving to DLQ.`);
|
|
201
|
+
console.error(`[${this.groupName}] Job ${message.messageUuid} failed but not routed to this group. Moving to DLQ.`);
|
|
177
202
|
messagesToDlq.push(message);
|
|
178
|
-
pipeline.xadd(this.keys.getDlqStreamKey(), '*', 'id', message.messageUuid, 'group', this.groupName, 'error', errorMessage
|
|
203
|
+
pipeline.xadd(this.keys.getDlqStreamKey(), '*', 'id', message.messageUuid, 'group', this.groupName, 'error', `Failed but not routed to ${this.groupName}: ${errorMessage}`, 'payload', JSON.stringify(message.data), 'failedAt', Date.now());
|
|
179
204
|
}
|
|
180
205
|
}
|
|
181
206
|
await pipeline.exec();
|
|
@@ -184,26 +209,21 @@ class BatchWorker {
|
|
|
184
209
|
}
|
|
185
210
|
}
|
|
186
211
|
async finalize(messages) {
|
|
187
|
-
if (messages.length === 0)
|
|
212
|
+
if (messages.length === 0) {
|
|
188
213
|
return;
|
|
214
|
+
}
|
|
189
215
|
const pipeline = this.redis.pipeline();
|
|
190
216
|
const timestamp = Date.now();
|
|
191
217
|
const throughputKey = this.keys.getThroughputKey(this.groupName, timestamp);
|
|
192
218
|
const totalKey = this.keys.getTotalKey(this.groupName);
|
|
193
|
-
// 1. Batch xacks
|
|
194
219
|
const ids = messages.map(m => m.streamMessageId);
|
|
195
220
|
pipeline.xack(this.streamName, this.groupName, ...ids);
|
|
196
|
-
// 2. Batch metrics
|
|
197
221
|
pipeline.incrby(throughputKey, ids.length);
|
|
198
222
|
pipeline.expire(throughputKey, 86400);
|
|
199
223
|
pipeline.incrby(totalKey, ids.length);
|
|
200
|
-
// Lua scripts to only check if data should be deleted
|
|
201
224
|
for (const msg of messages) {
|
|
202
225
|
const statusKey = this.keys.getJobStatusKey(msg.messageUuid);
|
|
203
|
-
|
|
204
|
-
pipeline.eval(lua_1.LUA_FINALIZE_COMPLEX, 3, statusKey, dataKey, this.streamName, // Keys
|
|
205
|
-
this.groupName, timestamp, msg.streamMessageId // args
|
|
206
|
-
);
|
|
226
|
+
pipeline.eval(lua_1.LUA_FINALIZE_COMPLEX, 2, statusKey, this.streamName, this.groupName, timestamp, msg.streamMessageId);
|
|
207
227
|
}
|
|
208
228
|
await pipeline.exec();
|
|
209
229
|
}
|
|
@@ -11,7 +11,8 @@ const uuid_1 = require("uuid");
|
|
|
11
11
|
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
|
|
12
12
|
class TestBatchWorker extends batch_worker_1.BatchWorker {
|
|
13
13
|
constructor(redis, groupName, streamName, batchSize = 10, concurrency = 1, maxRetries = 3, blockTimeMs = 100) {
|
|
14
|
-
|
|
14
|
+
// Fix argument order: batchSize, concurrency, maxFetchSize, maxRetries, blockTimeMs
|
|
15
|
+
super(redis, groupName, streamName, batchSize, concurrency, 20, maxRetries, blockTimeMs);
|
|
15
16
|
this.processedBatches = [];
|
|
16
17
|
this.shouldFail = false;
|
|
17
18
|
this.failCount = 0;
|
package/dist/lua.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const LUA_MARK_DONE = "\n-- KEYS[1] = status key status key for jog\n-- KEYS[2] =
|
|
2
|
-
export declare const LUA_FINALIZE_COMPLEX = "\n-- KEYS[1] = status key\n-- KEYS[2] =
|
|
1
|
+
export declare const LUA_MARK_DONE = "\n-- KEYS[1] = status key status key for jog\n-- KEYS[2] = stream key\n-- KEYS[3] = group name\n-- KEYS[4] = metrics key\n-- KEYS[5] = total metrics key(persistent)\n\n-- ARGV[1] = route name\n-- ARGV[2] = timestamp\n-- ARGV[3] = msgId - redis stream item ID\n\n-- 1 Ack the stream message\nredis.call('XACK', KEYS[2], KEYS[3], ARGV[3])\n\n-- 2 in status key mark the current route as done by saving timestamp\nredis.call('HSET', KEYS[1], ARGV[1], ARGV[2])\n\n-- 3 Increment throughput metric\nif KEYS[5] then\n redis.call('INCR', KEYS[4])\n redis.call('EXPIRE', KEYS[4], 86400)\nend\n\n-- 4 Increment Total Metric\nredis.call('INCR', KEYS[5])\n\n-- 5 Check for completed routes\nlocal current_fields = redis.call('HLEN', KEYS[1])\n\n-- 6 Get the target completed routes\nlocal target_str = redis.call('HGET', KEYS[1], '__target')\nlocal target = tonumber(target_str)\n\nif not target then\n return 0\nend\n\n-- 7 If completed routes is status hash length - 1 -> all were done and we can cleanup\nif current_fields >= (target + 1) then\n redis.call('DEL', KEYS[1]) -- Only delete status key\n redis.call('XDEL', KEYS[2], ARGV[3])\n return 1 -- Cleanup, DONE\nend\n\nreturn 0 -- Some routes are not done yet\n";
|
|
2
|
+
export declare const LUA_FINALIZE_COMPLEX = "\n-- KEYS[1] = status key\n-- KEYS[2] = stream key\n-- ARGV[1] = group name\n-- ARGV[2] = timestamp\n-- ARGV[3] = msgId\n\n-- 1. Update status\nredis.call('HSET', KEYS[1], ARGV[1], ARGV[2])\n\n-- 2. Check completions\nlocal current_fields = redis.call('HLEN', KEYS[1])\nlocal target_str = redis.call('HGET', KEYS[1], '__target')\nlocal target = tonumber(target_str)\n\nif not target then\n return 0\nend\n\n-- 3. Cleanup if done\nif current_fields >= (target + 1) then\n redis.call('DEL', KEYS[1])\n redis.call('XDEL', KEYS[2], ARGV[3])\n return 1\nend\n\nreturn 0\n";
|
package/dist/lua.js
CHANGED
|
@@ -3,37 +3,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.LUA_FINALIZE_COMPLEX = exports.LUA_MARK_DONE = void 0;
|
|
4
4
|
exports.LUA_MARK_DONE = `
|
|
5
5
|
-- KEYS[1] = status key status key for jog
|
|
6
|
-
-- KEYS[2] =
|
|
7
|
-
-- KEYS[3] =
|
|
8
|
-
-- KEYS[4] =
|
|
9
|
-
-- KEYS[5] = metrics key
|
|
10
|
-
-- KEYS[6] = total metrics key(persistent)
|
|
6
|
+
-- KEYS[2] = stream key
|
|
7
|
+
-- KEYS[3] = group name
|
|
8
|
+
-- KEYS[4] = metrics key
|
|
9
|
+
-- KEYS[5] = total metrics key(persistent)
|
|
11
10
|
|
|
12
11
|
-- ARGV[1] = route name
|
|
13
12
|
-- ARGV[2] = timestamp
|
|
14
13
|
-- ARGV[3] = msgId - redis stream item ID
|
|
15
14
|
|
|
16
|
-
-- 1
|
|
17
|
-
redis.call('XACK', KEYS[
|
|
15
|
+
-- 1 Ack the stream message
|
|
16
|
+
redis.call('XACK', KEYS[2], KEYS[3], ARGV[3])
|
|
18
17
|
|
|
19
|
-
-- 2
|
|
18
|
+
-- 2 in status key mark the current route as done by saving timestamp
|
|
20
19
|
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
|
|
21
20
|
|
|
22
|
-
-- 3
|
|
21
|
+
-- 3 Increment throughput metric
|
|
23
22
|
if KEYS[5] then
|
|
24
|
-
redis.call('INCR', KEYS[
|
|
25
|
-
redis.call('EXPIRE', KEYS[
|
|
23
|
+
redis.call('INCR', KEYS[4])
|
|
24
|
+
redis.call('EXPIRE', KEYS[4], 86400)
|
|
26
25
|
end
|
|
27
26
|
|
|
28
|
-
--
|
|
29
|
-
|
|
30
|
-
redis.call('INCR', KEYS[6])
|
|
31
|
-
end
|
|
27
|
+
-- 4 Increment Total Metric
|
|
28
|
+
redis.call('INCR', KEYS[5])
|
|
32
29
|
|
|
33
|
-
--
|
|
30
|
+
-- 5 Check for completed routes
|
|
34
31
|
local current_fields = redis.call('HLEN', KEYS[1])
|
|
35
32
|
|
|
36
|
-
--
|
|
33
|
+
-- 6 Get the target completed routes
|
|
37
34
|
local target_str = redis.call('HGET', KEYS[1], '__target')
|
|
38
35
|
local target = tonumber(target_str)
|
|
39
36
|
|
|
@@ -41,10 +38,10 @@ if not target then
|
|
|
41
38
|
return 0
|
|
42
39
|
end
|
|
43
40
|
|
|
44
|
-
--
|
|
41
|
+
-- 7 If completed routes is status hash length - 1 -> all were done and we can cleanup
|
|
45
42
|
if current_fields >= (target + 1) then
|
|
46
|
-
redis.call('DEL', KEYS[1]
|
|
47
|
-
redis.call('XDEL', KEYS[
|
|
43
|
+
redis.call('DEL', KEYS[1]) -- Only delete status key
|
|
44
|
+
redis.call('XDEL', KEYS[2], ARGV[3])
|
|
48
45
|
return 1 -- Cleanup, DONE
|
|
49
46
|
end
|
|
50
47
|
|
|
@@ -52,8 +49,7 @@ return 0 -- Some routes are not done yet
|
|
|
52
49
|
`;
|
|
53
50
|
exports.LUA_FINALIZE_COMPLEX = `
|
|
54
51
|
-- KEYS[1] = status key
|
|
55
|
-
-- KEYS[2] =
|
|
56
|
-
-- KEYS[3] = stream key
|
|
52
|
+
-- KEYS[2] = stream key
|
|
57
53
|
-- ARGV[1] = group name
|
|
58
54
|
-- ARGV[2] = timestamp
|
|
59
55
|
-- ARGV[3] = msgId
|
|
@@ -72,8 +68,8 @@ end
|
|
|
72
68
|
|
|
73
69
|
-- 3. Cleanup if done
|
|
74
70
|
if current_fields >= (target + 1) then
|
|
75
|
-
redis.call('DEL', KEYS[1]
|
|
76
|
-
redis.call('XDEL', KEYS[
|
|
71
|
+
redis.call('DEL', KEYS[1])
|
|
72
|
+
redis.call('XDEL', KEYS[2], ARGV[3])
|
|
77
73
|
return 1
|
|
78
74
|
end
|
|
79
75
|
|
package/dist/producer.js
CHANGED
|
@@ -21,15 +21,7 @@ class Producer {
|
|
|
21
21
|
const id = (0, uuid_1.v7)();
|
|
22
22
|
const ttl = opts?.ttl || null; // 24 hours in seconds
|
|
23
23
|
const pipeline = this.redis.pipeline();
|
|
24
|
-
const dataKey = this.keys.getJobDataKey(id);
|
|
25
24
|
const statusKey = this.keys.getJobStatusKey(id);
|
|
26
|
-
// Create job data
|
|
27
|
-
if (ttl) {
|
|
28
|
-
pipeline.set(dataKey, serializedPayload, 'EX', ttl);
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
pipeline.set(dataKey, serializedPayload);
|
|
32
|
-
}
|
|
33
25
|
// Initialize job metadata - status
|
|
34
26
|
// TODO: improve target groups use groups join by "," instead of groups length
|
|
35
27
|
pipeline.hset(statusKey, '__target', targetGroups.length);
|
|
@@ -37,7 +29,7 @@ class Producer {
|
|
|
37
29
|
pipeline.expire(statusKey, ttl);
|
|
38
30
|
}
|
|
39
31
|
// Push message to stream
|
|
40
|
-
pipeline.xadd(this.streamName, '*', 'id', id, 'target', targetGroups.join(','));
|
|
32
|
+
pipeline.xadd(this.streamName, '*', 'id', id, 'target', targetGroups.join(','), 'data', serializedPayload);
|
|
41
33
|
await pipeline.exec();
|
|
42
34
|
return id;
|
|
43
35
|
}
|
package/dist/queue.spec.js
CHANGED
|
@@ -11,8 +11,8 @@ const metrics_1 = require("./metrics");
|
|
|
11
11
|
const uuid_1 = require("uuid");
|
|
12
12
|
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
|
|
13
13
|
class TestWorker extends worker_1.Worker {
|
|
14
|
-
constructor(redis, groupName, streamName, concurrency = 1, blockTimeMs = 100) {
|
|
15
|
-
super(redis, groupName, streamName, concurrency,
|
|
14
|
+
constructor(redis, groupName, streamName, concurrency = 1, maxRetries = 3, blockTimeMs = 100, claimIntervalMs = 60000, minIdleTimeMs = 300000) {
|
|
15
|
+
super(redis, groupName, streamName, concurrency, maxRetries, blockTimeMs, claimIntervalMs, minIdleTimeMs);
|
|
16
16
|
this.processedCount = 0;
|
|
17
17
|
this.lastProcessedId = null;
|
|
18
18
|
this.shouldFail = false;
|
|
@@ -70,8 +70,8 @@ class TestWorker extends worker_1.Worker {
|
|
|
70
70
|
};
|
|
71
71
|
(0, vitest_1.describe)('Core Functionality', () => {
|
|
72
72
|
(0, vitest_1.it)('Should deliver message to all target groups', async () => {
|
|
73
|
-
const w1 = new TestWorker(redis, 'group-A', streamName, 1, 100);
|
|
74
|
-
const w2 = new TestWorker(redis, 'group-B', streamName, 1, 100);
|
|
73
|
+
const w1 = new TestWorker(redis, 'group-A', streamName, 1, 3, 100);
|
|
74
|
+
const w2 = new TestWorker(redis, 'group-B', streamName, 1, 3, 100);
|
|
75
75
|
workers.push(w1, w2);
|
|
76
76
|
await w1.start();
|
|
77
77
|
await w2.start();
|
|
@@ -86,8 +86,8 @@ class TestWorker extends worker_1.Worker {
|
|
|
86
86
|
(0, vitest_1.expect)(await redis.exists(dataKey)).toBe(0);
|
|
87
87
|
});
|
|
88
88
|
(0, vitest_1.it)('Should only deliver to targeted groups', async () => {
|
|
89
|
-
const wA = new TestWorker(redis, 'group-A', streamName, 1, 100);
|
|
90
|
-
const wB = new TestWorker(redis, 'group-B', streamName, 1, 100);
|
|
89
|
+
const wA = new TestWorker(redis, 'group-A', streamName, 1, 3, 100);
|
|
90
|
+
const wB = new TestWorker(redis, 'group-B', streamName, 1, 3, 100);
|
|
91
91
|
workers.push(wA, wB);
|
|
92
92
|
await wA.start();
|
|
93
93
|
await wB.start();
|
|
@@ -97,8 +97,8 @@ class TestWorker extends worker_1.Worker {
|
|
|
97
97
|
(0, vitest_1.expect)(wB.processedCount).toBe(0);
|
|
98
98
|
});
|
|
99
99
|
(0, vitest_1.it)('Should retry only the failed group', async () => {
|
|
100
|
-
const wOk = new TestWorker(redis, 'group-Ok', streamName, 1, 100);
|
|
101
|
-
const wFail = new TestWorker(redis, 'group-Fail', streamName, 1, 100);
|
|
100
|
+
const wOk = new TestWorker(redis, 'group-Ok', streamName, 1, 3, 100);
|
|
101
|
+
const wFail = new TestWorker(redis, 'group-Fail', streamName, 1, 3, 100);
|
|
102
102
|
wFail.shouldFail = true;
|
|
103
103
|
wFail.maxFails = 1; // Fail once, then succeed
|
|
104
104
|
workers.push(wOk, wFail);
|
|
@@ -113,7 +113,7 @@ class TestWorker extends worker_1.Worker {
|
|
|
113
113
|
(0, vitest_1.expect)(wOk.processedCount).toBe(1); // wOk should NOT process the retry
|
|
114
114
|
});
|
|
115
115
|
(0, vitest_1.it)('Should move to DLQ after max retries', async () => {
|
|
116
|
-
const wDead = new TestWorker(redis, 'group-Dead', streamName, 1, 100);
|
|
116
|
+
const wDead = new TestWorker(redis, 'group-Dead', streamName, 1, 3, 100);
|
|
117
117
|
wDead.shouldFail = true;
|
|
118
118
|
wDead.maxFails = 10; // Fail forever (more than max retries which is 3)
|
|
119
119
|
workers.push(wDead);
|
|
@@ -130,7 +130,7 @@ class TestWorker extends worker_1.Worker {
|
|
|
130
130
|
});
|
|
131
131
|
(0, vitest_1.describe)('Metrics & Monitoring', () => {
|
|
132
132
|
(0, vitest_1.it)('Should track throughput and queue size', async () => {
|
|
133
|
-
const w = new TestWorker(redis, 'group-Metrics', streamName, 1, 100);
|
|
133
|
+
const w = new TestWorker(redis, 'group-Metrics', streamName, 1, 3, 100);
|
|
134
134
|
const metricsService = new metrics_1.Metrics(redis, streamName);
|
|
135
135
|
workers.push(w);
|
|
136
136
|
await w.start();
|
|
@@ -148,7 +148,7 @@ class TestWorker extends worker_1.Worker {
|
|
|
148
148
|
(0, vitest_1.expect)(metrics.dlqLength).toBe(1);
|
|
149
149
|
});
|
|
150
150
|
(0, vitest_1.it)('Should export Prometheus metrics', async () => {
|
|
151
|
-
const w = new TestWorker(redis, 'group-Prom', streamName, 1, 100);
|
|
151
|
+
const w = new TestWorker(redis, 'group-Prom', streamName, 1, 3, 100);
|
|
152
152
|
const metricsService = new metrics_1.Metrics(redis, streamName);
|
|
153
153
|
workers.push(w);
|
|
154
154
|
await w.start();
|
|
@@ -165,7 +165,7 @@ class TestWorker extends worker_1.Worker {
|
|
|
165
165
|
});
|
|
166
166
|
(0, vitest_1.describe)('Stream Cleanup', () => {
|
|
167
167
|
(0, vitest_1.it)('Should delete message from stream after processing', async () => {
|
|
168
|
-
const w1 = new TestWorker(redis, 'group-A', streamName, 1, 100);
|
|
168
|
+
const w1 = new TestWorker(redis, 'group-A', streamName, 1, 3, 100);
|
|
169
169
|
workers.push(w1);
|
|
170
170
|
await w1.start();
|
|
171
171
|
const id = await producer.push({ id: 'msg-cleanup' }, ['group-A']);
|
|
@@ -182,8 +182,8 @@ class TestWorker extends worker_1.Worker {
|
|
|
182
182
|
(0, vitest_1.expect)(messages.length).toBe(0);
|
|
183
183
|
});
|
|
184
184
|
(0, vitest_1.it)('Should delete message from stream only after ALL groups processed it', async () => {
|
|
185
|
-
const w1 = new TestWorker(redis, 'group-A', streamName, 1, 100);
|
|
186
|
-
const w2 = new TestWorker(redis, 'group-B', streamName, 1, 100);
|
|
185
|
+
const w1 = new TestWorker(redis, 'group-A', streamName, 1, 3, 100);
|
|
186
|
+
const w2 = new TestWorker(redis, 'group-B', streamName, 1, 3, 100);
|
|
187
187
|
workers.push(w1, w2);
|
|
188
188
|
await w1.start(); // Only start w1
|
|
189
189
|
const id = await producer.push({ id: 'msg-multi' }, ['group-A', 'group-B']);
|
|
@@ -203,4 +203,33 @@ class TestWorker extends worker_1.Worker {
|
|
|
203
203
|
(0, vitest_1.expect)(success).toBe(true);
|
|
204
204
|
});
|
|
205
205
|
});
|
|
206
|
+
(0, vitest_1.it)('Should recover stuck messages via Auto-Claim', async () => {
|
|
207
|
+
const groupName = 'group-Recover';
|
|
208
|
+
// Start worker with short minIdleTime (e.g., 1000ms) to trigger claim quickly
|
|
209
|
+
// minIdleTimeMs = 1000. claimIntervalMs = 500 (check frequently)
|
|
210
|
+
const w = new TestWorker(redis, groupName, streamName, 1, 3, 100, 500, 1000);
|
|
211
|
+
workers.push(w);
|
|
212
|
+
// 1. Setup group manually
|
|
213
|
+
await redis.xgroup('CREATE', streamName, groupName, '0', 'MKSTREAM');
|
|
214
|
+
// 2. Push message
|
|
215
|
+
const id = await producer.push({ id: 'stuck-msg' }, [groupName]);
|
|
216
|
+
// 3. Simulate a consumer reading but crashing (no ACK)
|
|
217
|
+
// consumer name 'bad-consumer'
|
|
218
|
+
await redis.xreadgroup('GROUP', groupName, 'bad-consumer', 'COUNT', 1, 'STREAMS', streamName, '>');
|
|
219
|
+
// 4. Wait for minIdleTime (1000ms) + buffer
|
|
220
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
221
|
+
// 5. Start our worker
|
|
222
|
+
await w.start();
|
|
223
|
+
// 6. Verify worker picks it up
|
|
224
|
+
await waitFor(() => w.processedCount === 1, 5000);
|
|
225
|
+
(0, vitest_1.expect)(w.processedCount).toBe(1);
|
|
226
|
+
(0, vitest_1.expect)(w.lastProcessedId).toBe('stuck-msg');
|
|
227
|
+
// Verify it was claimed (delivered to new consumer)
|
|
228
|
+
// We can check PEL or just trust processedCount
|
|
229
|
+
const pending = await redis.xpending(streamName, groupName);
|
|
230
|
+
// After processing, it should be ACKed, so pending count => 0 (if deleted)
|
|
231
|
+
// or if finalize runs, it deletes the message entirely.
|
|
232
|
+
const len = await redis.xlen(streamName);
|
|
233
|
+
(0, vitest_1.expect)(len).toBe(0);
|
|
234
|
+
});
|
|
206
235
|
});
|
|
@@ -6,10 +6,9 @@ export declare class StreamMessageEntity<T extends Record<string, unknown>> {
|
|
|
6
6
|
private readonly _routes;
|
|
7
7
|
private readonly _messageUuid;
|
|
8
8
|
private readonly _retryCount;
|
|
9
|
-
private _data;
|
|
9
|
+
private readonly _data;
|
|
10
10
|
constructor(message: StreamMessage);
|
|
11
|
-
|
|
12
|
-
get data(): T | null;
|
|
11
|
+
get data(): T;
|
|
13
12
|
get streamMessageId(): string;
|
|
14
13
|
get messageUuid(): string;
|
|
15
14
|
get routes(): string[];
|
|
@@ -6,7 +6,6 @@ class StreamMessageEntity {
|
|
|
6
6
|
this._rawFields = [];
|
|
7
7
|
this._fields = {};
|
|
8
8
|
this._routes = [];
|
|
9
|
-
this._data = null;
|
|
10
9
|
this._streamMessageId = message[0];
|
|
11
10
|
this._rawFields = message[1];
|
|
12
11
|
for (let i = 0; i < this._rawFields.length; i += 2) {
|
|
@@ -15,9 +14,17 @@ class StreamMessageEntity {
|
|
|
15
14
|
this._messageUuid = this._fields['id'];
|
|
16
15
|
this._routes = this._fields['target'].split(',');
|
|
17
16
|
this._retryCount = parseInt(this._fields['retryCount'] || '0', 10);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
try {
|
|
18
|
+
this._data = JSON.parse(this._fields['data']);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
// Handle corrupt or missing data gracefully
|
|
22
|
+
// We can set it to null (need to update type to T | null) or a dummy.
|
|
23
|
+
// Since strict T is expected, we might have to cast or throw controlled error.
|
|
24
|
+
// For now, let's assume T can be null-ish or cast. But getter says T.
|
|
25
|
+
// Let's coerce to {} as any to avoid crash, let validation downstream handle it.
|
|
26
|
+
this._data = {};
|
|
27
|
+
}
|
|
21
28
|
}
|
|
22
29
|
get data() {
|
|
23
30
|
return this._data;
|
package/dist/worker.d.ts
CHANGED
|
@@ -6,18 +6,22 @@ export declare abstract class Worker<T extends Record<string, unknown>> {
|
|
|
6
6
|
protected concurrency: number;
|
|
7
7
|
protected MAX_RETRIES: number;
|
|
8
8
|
protected blockTimeMs: number;
|
|
9
|
+
protected claimIntervalMs: number;
|
|
10
|
+
protected minIdleTimeMs: number;
|
|
9
11
|
private isRunning;
|
|
10
12
|
private activeCount;
|
|
11
13
|
private readonly events;
|
|
12
14
|
private keys;
|
|
13
15
|
private consumerId;
|
|
14
|
-
|
|
16
|
+
private blockingRedis;
|
|
17
|
+
constructor(redis: Redis, groupName: string, streamName: string, concurrency?: number, MAX_RETRIES?: number, blockTimeMs?: number, claimIntervalMs?: number, minIdleTimeMs?: number);
|
|
15
18
|
/**
|
|
16
19
|
* Start worker
|
|
17
20
|
* @returns
|
|
18
21
|
*/
|
|
19
22
|
start(): Promise<void>;
|
|
20
23
|
stop(): Promise<void>;
|
|
24
|
+
private autoClaimLoop;
|
|
21
25
|
private fetchLoop;
|
|
22
26
|
private spawnWorker;
|
|
23
27
|
private processInternal;
|
package/dist/worker.js
CHANGED
|
@@ -7,19 +7,22 @@ const keys_1 = require("./keys");
|
|
|
7
7
|
const stream_message_entity_1 = require("./stream-message-entity");
|
|
8
8
|
const uuid_1 = require("uuid");
|
|
9
9
|
class Worker {
|
|
10
|
-
constructor(redis, groupName, streamName, concurrency = 1, MAX_RETRIES = 3, blockTimeMs = 2000) {
|
|
10
|
+
constructor(redis, groupName, streamName, concurrency = 1, MAX_RETRIES = 3, blockTimeMs = 2000, claimIntervalMs = 60000, minIdleTimeMs = 300000) {
|
|
11
11
|
this.redis = redis;
|
|
12
12
|
this.groupName = groupName;
|
|
13
13
|
this.streamName = streamName;
|
|
14
14
|
this.concurrency = concurrency;
|
|
15
15
|
this.MAX_RETRIES = MAX_RETRIES;
|
|
16
16
|
this.blockTimeMs = blockTimeMs;
|
|
17
|
+
this.claimIntervalMs = claimIntervalMs;
|
|
18
|
+
this.minIdleTimeMs = minIdleTimeMs;
|
|
17
19
|
this.isRunning = false;
|
|
18
20
|
this.activeCount = 0;
|
|
19
21
|
this.events = new events_1.EventEmitter();
|
|
20
22
|
this.consumerId = (0, uuid_1.v7)();
|
|
21
23
|
this.events.setMaxListeners(100);
|
|
22
24
|
this.keys = new keys_1.KeyManager(streamName);
|
|
25
|
+
this.blockingRedis = this.redis.duplicate();
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
28
|
* Start worker
|
|
@@ -39,15 +42,59 @@ class Worker {
|
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
this.fetchLoop();
|
|
45
|
+
this.autoClaimLoop();
|
|
42
46
|
}
|
|
43
47
|
async stop() {
|
|
44
48
|
this.isRunning = false;
|
|
45
|
-
this.events.emit('job_finished');
|
|
46
|
-
|
|
49
|
+
this.events.emit('job_finished');
|
|
50
|
+
if (this.blockingRedis) {
|
|
51
|
+
try {
|
|
52
|
+
await this.blockingRedis.quit();
|
|
53
|
+
}
|
|
54
|
+
catch (e) { }
|
|
55
|
+
}
|
|
47
56
|
while (this.activeCount > 0) {
|
|
48
57
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
49
58
|
}
|
|
50
59
|
}
|
|
60
|
+
async autoClaimLoop() {
|
|
61
|
+
while (this.isRunning) {
|
|
62
|
+
try {
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, this.claimIntervalMs));
|
|
64
|
+
if (!this.isRunning) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
let cursor = '0-0';
|
|
68
|
+
let continueClaiming = true;
|
|
69
|
+
while (continueClaiming && this.isRunning) {
|
|
70
|
+
const result = await this.redis.xautoclaim(this.streamName, this.groupName, this.consumerName(), this.minIdleTimeMs, cursor, 'COUNT', this.concurrency);
|
|
71
|
+
if (!result) {
|
|
72
|
+
continueClaiming = false;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const [nextCursor, messages] = result;
|
|
76
|
+
cursor = nextCursor;
|
|
77
|
+
if (messages && messages.length > 0) {
|
|
78
|
+
console.log(`[${this.groupName}] Recovered ${messages.length} stuck messages`);
|
|
79
|
+
for (const msg of messages) {
|
|
80
|
+
this.spawnWorker(msg);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
continueClaiming = false;
|
|
85
|
+
}
|
|
86
|
+
if (nextCursor === '0-0') {
|
|
87
|
+
continueClaiming = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
if (this.isRunning) {
|
|
93
|
+
console.error(`[${this.groupName}] auto claim err:`, e.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
51
98
|
async fetchLoop() {
|
|
52
99
|
while (this.isRunning) {
|
|
53
100
|
const freeSlots = this.concurrency - this.activeCount;
|
|
@@ -56,7 +103,7 @@ class Worker {
|
|
|
56
103
|
continue;
|
|
57
104
|
}
|
|
58
105
|
try {
|
|
59
|
-
const results = await this.
|
|
106
|
+
const results = await this.blockingRedis.xreadgroup('GROUP', this.groupName, this.consumerName(), 'COUNT', freeSlots, 'BLOCK', this.blockTimeMs, 'STREAMS', this.streamName, '>');
|
|
60
107
|
if (results) {
|
|
61
108
|
const messages = results[0][1];
|
|
62
109
|
for (const msg of messages) {
|
|
@@ -84,52 +131,37 @@ class Worker {
|
|
|
84
131
|
return;
|
|
85
132
|
}
|
|
86
133
|
try {
|
|
87
|
-
|
|
88
|
-
const payload = await this.redis.get(dataKey);
|
|
89
|
-
if (!payload) {
|
|
90
|
-
// Data missing or expired
|
|
91
|
-
await this.finalize(streamMessage.messageUuid, streamMessage.streamMessageId);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
await this.process(JSON.parse(payload));
|
|
134
|
+
await this.process(streamMessage.data);
|
|
95
135
|
await this.finalize(streamMessage.messageUuid, streamMessage.streamMessageId);
|
|
96
136
|
}
|
|
97
137
|
catch (err) {
|
|
98
138
|
console.error(`[${this.groupName}] Job failed ${streamMessage.messageUuid}`, err);
|
|
99
|
-
await this.handleFailure(streamMessage.messageUuid, streamMessage.streamMessageId, streamMessage.retryCount, err.message);
|
|
139
|
+
await this.handleFailure(streamMessage.messageUuid, streamMessage.streamMessageId, streamMessage.retryCount, err.message, streamMessage.data);
|
|
100
140
|
}
|
|
101
141
|
}
|
|
102
|
-
async handleFailure(uuid, msgId, currentRetries, errorMsg) {
|
|
103
|
-
//
|
|
142
|
+
async handleFailure(uuid, msgId, currentRetries, errorMsg, payloadData) {
|
|
143
|
+
// Ack
|
|
104
144
|
await this.redis.xack(this.streamName, this.groupName, msgId);
|
|
105
|
-
|
|
106
|
-
if (currentRetries < this.MAX_RETRIES) {
|
|
145
|
+
const payloadString = payloadData ? JSON.stringify(payloadData) : '';
|
|
146
|
+
if (currentRetries < this.MAX_RETRIES && payloadData) {
|
|
107
147
|
console.log(`[${this.groupName}] Retrying job ${uuid} (Attempt ${currentRetries + 1}/${this.MAX_RETRIES})`);
|
|
108
148
|
const pipeline = this.redis.pipeline();
|
|
109
|
-
|
|
110
|
-
pipeline.expire(this.keys.getJobDataKey(uuid), 3600);
|
|
111
|
-
pipeline.expire(this.keys.getJobStatusKey(uuid), 3600);
|
|
112
|
-
pipeline.xadd(this.streamName, '*', 'id', uuid, 'target', this.groupName, // Instead of all groups, target the failed one
|
|
113
|
-
'retryCount', currentRetries + 1);
|
|
149
|
+
pipeline.xadd(this.streamName, '*', 'id', uuid, 'target', this.groupName, 'retryCount', currentRetries + 1, 'data', payloadString);
|
|
114
150
|
await pipeline.exec();
|
|
115
151
|
}
|
|
116
152
|
else {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const payload = await this.redis.get(this.keys.getJobDataKey(uuid));
|
|
121
|
-
await this.redis.xadd(this.keys.getDlqStreamKey(), '*', 'id', uuid, 'group', this.groupName, 'error', errorMsg, 'payload', payload || 'MISSING', 'failedAt', Date.now());
|
|
122
|
-
// Delete job from stream and mark it as "done"
|
|
123
|
-
await this.finalize(uuid, msgId, true);
|
|
153
|
+
console.error(`[${this.groupName}] Job ${uuid} run outof retries. Moving to DLQ`);
|
|
154
|
+
await this.redis.xadd(this.keys.getDlqStreamKey(), '*', 'id', uuid, 'group', this.groupName, 'error', errorMsg, 'payload', payloadString, 'failedAt', Date.now());
|
|
155
|
+
await this.finalize(uuid, msgId);
|
|
124
156
|
}
|
|
125
157
|
}
|
|
126
|
-
async finalize(messageUuid, msgId
|
|
158
|
+
async finalize(messageUuid, msgId) {
|
|
127
159
|
const timestamp = Date.now();
|
|
128
160
|
const statusKey = this.keys.getJobStatusKey(messageUuid);
|
|
129
161
|
const dataKey = this.keys.getJobDataKey(messageUuid);
|
|
130
162
|
const throughputKey = this.keys.getThroughputKey(this.groupName, timestamp);
|
|
131
163
|
const totalKey = this.keys.getTotalKey(this.groupName);
|
|
132
|
-
await this.redis.eval(lua_1.LUA_MARK_DONE,
|
|
164
|
+
await this.redis.eval(lua_1.LUA_MARK_DONE, 5, statusKey, this.streamName, this.groupName, throughputKey, totalKey, this.groupName, timestamp, msgId);
|
|
133
165
|
}
|
|
134
166
|
consumerName() {
|
|
135
167
|
return `${this.groupName}-${process.pid}-${this.consumerId}`;
|