@koala42/redis-highway 0.1.7 → 0.1.8
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 +1 -0
- package/dist/batch-worker.js +35 -19
- package/dist/lua.d.ts +1 -0
- package/dist/lua.js +30 -1
- package/dist/queue.spec.js +2 -2
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +9 -2
- package/package.json +1 -1
package/dist/batch-worker.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare abstract class BatchWorker<T extends Record<string, unknown>> {
|
|
|
11
11
|
private activeCount;
|
|
12
12
|
private readonly events;
|
|
13
13
|
private keys;
|
|
14
|
+
private readonly consumerId;
|
|
14
15
|
constructor(redis: Redis, groupName: string, streamName: string, batchSize?: number, concurrency?: number, maxRetries?: number, blockTimeMs?: number);
|
|
15
16
|
start(): Promise<void>;
|
|
16
17
|
stop(): Promise<void>;
|
package/dist/batch-worker.js
CHANGED
|
@@ -5,6 +5,7 @@ const events_1 = require("events");
|
|
|
5
5
|
const keys_1 = require("./keys");
|
|
6
6
|
const stream_message_entity_1 = require("./stream-message-entity");
|
|
7
7
|
const lua_1 = require("./lua");
|
|
8
|
+
const uuid_1 = require("uuid");
|
|
8
9
|
class BatchWorker {
|
|
9
10
|
constructor(redis, groupName, streamName, batchSize = 10, concurrency = 1, maxRetries = 3, blockTimeMs = 2000) {
|
|
10
11
|
this.redis = redis;
|
|
@@ -17,6 +18,7 @@ class BatchWorker {
|
|
|
17
18
|
this.isRunning = false;
|
|
18
19
|
this.activeCount = 0;
|
|
19
20
|
this.events = new events_1.EventEmitter();
|
|
21
|
+
this.consumerId = (0, uuid_1.v7)();
|
|
20
22
|
if (batchSize < 1) {
|
|
21
23
|
throw new Error('Batch size cannot be less then 0');
|
|
22
24
|
}
|
|
@@ -38,12 +40,12 @@ class BatchWorker {
|
|
|
38
40
|
}
|
|
39
41
|
this.fetchLoop();
|
|
40
42
|
}
|
|
41
|
-
// TODO: implement waiting for runnnig jobs
|
|
42
43
|
async stop() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
this.isRunning = false;
|
|
45
|
+
this.events.emit('job_finished');
|
|
46
|
+
while (this.activeCount > 0) {
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
48
|
+
}
|
|
47
49
|
}
|
|
48
50
|
async fetchLoop() {
|
|
49
51
|
while (this.isRunning) {
|
|
@@ -132,19 +134,21 @@ class BatchWorker {
|
|
|
132
134
|
}
|
|
133
135
|
message.data = JSON.parse(data);
|
|
134
136
|
});
|
|
135
|
-
const messagesData =
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
const messagesData = [];
|
|
138
|
+
const messagesToFinalize = [];
|
|
139
|
+
messages.forEach((message) => {
|
|
140
|
+
messagesToFinalize.push(message);
|
|
141
|
+
if (message.data) {
|
|
142
|
+
messagesData.push(message.data);
|
|
138
143
|
}
|
|
139
|
-
|
|
140
|
-
}, []);
|
|
144
|
+
});
|
|
141
145
|
// TODO improve error handling
|
|
142
146
|
if (!messagesData.length) {
|
|
143
147
|
return;
|
|
144
148
|
}
|
|
145
149
|
try {
|
|
146
150
|
await this.process(messagesData);
|
|
147
|
-
await this.finalize(
|
|
151
|
+
await this.finalize(messagesToFinalize);
|
|
148
152
|
}
|
|
149
153
|
catch (err) {
|
|
150
154
|
console.error(`[${this.groupName}] Jobs failed`, err);
|
|
@@ -180,19 +184,31 @@ class BatchWorker {
|
|
|
180
184
|
}
|
|
181
185
|
}
|
|
182
186
|
async finalize(messages) {
|
|
187
|
+
if (messages.length === 0)
|
|
188
|
+
return;
|
|
183
189
|
const pipeline = this.redis.pipeline();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
const timestamp = Date.now();
|
|
191
|
+
const throughputKey = this.keys.getThroughputKey(this.groupName, timestamp);
|
|
192
|
+
const totalKey = this.keys.getTotalKey(this.groupName);
|
|
193
|
+
// 1. Batch xacks
|
|
194
|
+
const ids = messages.map(m => m.streamMessageId);
|
|
195
|
+
pipeline.xack(this.streamName, this.groupName, ...ids);
|
|
196
|
+
// 2. Batch metrics
|
|
197
|
+
pipeline.incrby(throughputKey, ids.length);
|
|
198
|
+
pipeline.expire(throughputKey, 86400);
|
|
199
|
+
pipeline.incrby(totalKey, ids.length);
|
|
200
|
+
// Lua scripts to only check if data should be deleted
|
|
201
|
+
for (const msg of messages) {
|
|
202
|
+
const statusKey = this.keys.getJobStatusKey(msg.messageUuid);
|
|
203
|
+
const dataKey = this.keys.getJobDataKey(msg.messageUuid);
|
|
204
|
+
pipeline.eval(lua_1.LUA_FINALIZE_COMPLEX, 3, statusKey, dataKey, this.streamName, // Keys
|
|
205
|
+
this.groupName, timestamp, msg.streamMessageId // args
|
|
206
|
+
);
|
|
191
207
|
}
|
|
192
208
|
await pipeline.exec();
|
|
193
209
|
}
|
|
194
210
|
getConsumerName() {
|
|
195
|
-
return `${this.groupName}-${process.pid}`;
|
|
211
|
+
return `${this.groupName}-${process.pid}-${this.consumerId}`;
|
|
196
212
|
}
|
|
197
213
|
}
|
|
198
214
|
exports.BatchWorker = BatchWorker;
|
package/dist/lua.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export declare const LUA_MARK_DONE = "\n-- KEYS[1] = status key status key for jog\n-- KEYS[2] = data key for job\n-- KEYS[3] = stream key\n-- KEYS[4] = group name\n-- KEYS[5] = metrics key\n-- KEYS[6] = 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[3], KEYS[4], 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[5])\n redis.call('EXPIRE', KEYS[5], 86400)\nend\n\n-- 3.1 Increment Total Metric\nif KEYS[6] then\n redis.call('INCR', KEYS[6])\nend\n\n-- 4. Check for completed routes\nlocal current_fields = redis.call('HLEN', KEYS[1])\n\n-- 5. 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-- 6. 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], KEYS[2])\n redis.call('XDEL', KEYS[3], 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] = data key\n-- KEYS[3] = 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], KEYS[2])\n redis.call('XDEL', KEYS[3], ARGV[3])\n return 1\nend\n\nreturn 0\n";
|
package/dist/lua.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LUA_MARK_DONE = void 0;
|
|
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
6
|
-- KEYS[2] = data key for job
|
|
@@ -50,3 +50,32 @@ end
|
|
|
50
50
|
|
|
51
51
|
return 0 -- Some routes are not done yet
|
|
52
52
|
`;
|
|
53
|
+
exports.LUA_FINALIZE_COMPLEX = `
|
|
54
|
+
-- KEYS[1] = status key
|
|
55
|
+
-- KEYS[2] = data key
|
|
56
|
+
-- KEYS[3] = stream key
|
|
57
|
+
-- ARGV[1] = group name
|
|
58
|
+
-- ARGV[2] = timestamp
|
|
59
|
+
-- ARGV[3] = msgId
|
|
60
|
+
|
|
61
|
+
-- 1. Update status
|
|
62
|
+
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
|
|
63
|
+
|
|
64
|
+
-- 2. Check completions
|
|
65
|
+
local current_fields = redis.call('HLEN', KEYS[1])
|
|
66
|
+
local target_str = redis.call('HGET', KEYS[1], '__target')
|
|
67
|
+
local target = tonumber(target_str)
|
|
68
|
+
|
|
69
|
+
if not target then
|
|
70
|
+
return 0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
-- 3. Cleanup if done
|
|
74
|
+
if current_fields >= (target + 1) then
|
|
75
|
+
redis.call('DEL', KEYS[1], KEYS[2])
|
|
76
|
+
redis.call('XDEL', KEYS[3], ARGV[3])
|
|
77
|
+
return 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
return 0
|
|
81
|
+
`;
|
package/dist/queue.spec.js
CHANGED
|
@@ -12,7 +12,7 @@ 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
14
|
constructor(redis, groupName, streamName, concurrency = 1, blockTimeMs = 100) {
|
|
15
|
-
super(redis, groupName, streamName, concurrency, blockTimeMs);
|
|
15
|
+
super(redis, groupName, streamName, concurrency, 3, blockTimeMs);
|
|
16
16
|
this.processedCount = 0;
|
|
17
17
|
this.lastProcessedId = null;
|
|
18
18
|
this.shouldFail = false;
|
|
@@ -48,7 +48,7 @@ class TestWorker extends worker_1.Worker {
|
|
|
48
48
|
});
|
|
49
49
|
(0, vitest_1.afterEach)(async () => {
|
|
50
50
|
for (const w of workers) {
|
|
51
|
-
w.stop();
|
|
51
|
+
await w.stop();
|
|
52
52
|
}
|
|
53
53
|
await new Promise(r => setTimeout(r, 500));
|
|
54
54
|
// Cleanup Redis keys using the existing connection before closing
|
package/dist/worker.d.ts
CHANGED
|
@@ -10,13 +10,14 @@ export declare abstract class Worker<T extends Record<string, unknown>> {
|
|
|
10
10
|
private activeCount;
|
|
11
11
|
private readonly events;
|
|
12
12
|
private keys;
|
|
13
|
+
private consumerId;
|
|
13
14
|
constructor(redis: Redis, groupName: string, streamName: string, concurrency?: number, MAX_RETRIES?: number, blockTimeMs?: number);
|
|
14
15
|
/**
|
|
15
16
|
* Start worker
|
|
16
17
|
* @returns
|
|
17
18
|
*/
|
|
18
19
|
start(): Promise<void>;
|
|
19
|
-
stop(): void
|
|
20
|
+
stop(): Promise<void>;
|
|
20
21
|
private fetchLoop;
|
|
21
22
|
private spawnWorker;
|
|
22
23
|
private processInternal;
|
package/dist/worker.js
CHANGED
|
@@ -5,6 +5,7 @@ const events_1 = require("events");
|
|
|
5
5
|
const lua_1 = require("./lua");
|
|
6
6
|
const keys_1 = require("./keys");
|
|
7
7
|
const stream_message_entity_1 = require("./stream-message-entity");
|
|
8
|
+
const uuid_1 = require("uuid");
|
|
8
9
|
class Worker {
|
|
9
10
|
constructor(redis, groupName, streamName, concurrency = 1, MAX_RETRIES = 3, blockTimeMs = 2000) {
|
|
10
11
|
this.redis = redis;
|
|
@@ -16,6 +17,7 @@ class Worker {
|
|
|
16
17
|
this.isRunning = false;
|
|
17
18
|
this.activeCount = 0;
|
|
18
19
|
this.events = new events_1.EventEmitter();
|
|
20
|
+
this.consumerId = (0, uuid_1.v7)();
|
|
19
21
|
this.events.setMaxListeners(100);
|
|
20
22
|
this.keys = new keys_1.KeyManager(streamName);
|
|
21
23
|
}
|
|
@@ -38,8 +40,13 @@ class Worker {
|
|
|
38
40
|
}
|
|
39
41
|
this.fetchLoop();
|
|
40
42
|
}
|
|
41
|
-
stop() {
|
|
43
|
+
async stop() {
|
|
42
44
|
this.isRunning = false;
|
|
45
|
+
this.events.emit('job_finished'); // Wake up fetch loop if it's waiting
|
|
46
|
+
// Wait for active jobs to finish
|
|
47
|
+
while (this.activeCount > 0) {
|
|
48
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
51
|
async fetchLoop() {
|
|
45
52
|
while (this.isRunning) {
|
|
@@ -125,7 +132,7 @@ class Worker {
|
|
|
125
132
|
await this.redis.eval(lua_1.LUA_MARK_DONE, 6, statusKey, dataKey, this.streamName, this.groupName, throughputKey, totalKey, this.groupName, timestamp, msgId);
|
|
126
133
|
}
|
|
127
134
|
consumerName() {
|
|
128
|
-
return `${this.groupName}-${process.pid}`;
|
|
135
|
+
return `${this.groupName}-${process.pid}-${this.consumerId}`;
|
|
129
136
|
}
|
|
130
137
|
}
|
|
131
138
|
exports.Worker = Worker;
|