@koala42/redis-highway 0.1.10 → 0.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/dist/src/base-worker.d.ts +64 -0
- package/dist/src/base-worker.js +180 -0
- package/dist/src/batch-worker.d.ts +16 -0
- package/dist/src/batch-worker.js +89 -0
- package/dist/src/interfaces.d.ts +19 -0
- package/dist/src/interfaces.js +10 -0
- package/dist/{keys.d.ts → src/keys.d.ts} +0 -4
- package/dist/{keys.js → src/keys.js} +0 -6
- package/dist/src/lua.d.ts +1 -0
- package/dist/src/lua.js +31 -0
- package/dist/{stream-message-entity.d.ts → src/stream-message-entity.d.ts} +2 -0
- package/dist/{stream-message-entity.js → src/stream-message-entity.js} +5 -11
- package/dist/src/worker.d.ts +24 -0
- package/dist/src/worker.js +72 -0
- package/dist/{queue.spec.js → test/queue.spec.js} +25 -4
- package/package.json +1 -1
- package/dist/batch-worker.d.ts +0 -40
- package/dist/batch-worker.js +0 -234
- package/dist/batch-worker.spec.d.ts +0 -1
- package/dist/batch-worker.spec.js +0 -124
- package/dist/interfaces.d.ts +0 -2
- package/dist/interfaces.js +0 -2
- package/dist/lua.d.ts +0 -2
- package/dist/lua.js +0 -77
- package/dist/worker.d.ts +0 -32
- package/dist/worker.js +0 -170
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{index.js → src/index.js} +0 -0
- /package/dist/{metrics.d.ts → src/metrics.d.ts} +0 -0
- /package/dist/{metrics.js → src/metrics.js} +0 -0
- /package/dist/{producer.d.ts → src/producer.d.ts} +0 -0
- /package/dist/{producer.js → src/producer.js} +0 -0
- /package/dist/{queue.spec.d.ts → test/queue.spec.d.ts} +0 -0
package/dist/batch-worker.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import Redis from "ioredis";
|
|
2
|
-
export declare abstract class BatchWorker<T extends Record<string, unknown>> {
|
|
3
|
-
protected redis: Redis;
|
|
4
|
-
protected groupName: string;
|
|
5
|
-
protected streamName: string;
|
|
6
|
-
protected batchSize: number;
|
|
7
|
-
protected concurrency: number;
|
|
8
|
-
protected maxFetchSize: number;
|
|
9
|
-
protected maxRetries: number;
|
|
10
|
-
protected blockTimeMs: number;
|
|
11
|
-
protected maxFetchCount: number;
|
|
12
|
-
protected claimIntervalMs: number;
|
|
13
|
-
protected minIdleTimeMs: number;
|
|
14
|
-
private isRunning;
|
|
15
|
-
private activeCount;
|
|
16
|
-
private keys;
|
|
17
|
-
private blockingRedis;
|
|
18
|
-
private readonly events;
|
|
19
|
-
private readonly consumerId;
|
|
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);
|
|
26
|
-
start(): Promise<void>;
|
|
27
|
-
stop(): Promise<void>;
|
|
28
|
-
private autoClaimLoop;
|
|
29
|
-
private fetchLoop;
|
|
30
|
-
/**
|
|
31
|
-
* Spawn worker for current processing
|
|
32
|
-
* @param messages
|
|
33
|
-
*/
|
|
34
|
-
private spawnWorker;
|
|
35
|
-
private processInternal;
|
|
36
|
-
private handleFailure;
|
|
37
|
-
private finalize;
|
|
38
|
-
private getConsumerName;
|
|
39
|
-
abstract process(data: T[]): Promise<void>;
|
|
40
|
-
}
|
package/dist/batch-worker.js
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BatchWorker = void 0;
|
|
4
|
-
const events_1 = require("events");
|
|
5
|
-
const keys_1 = require("./keys");
|
|
6
|
-
const stream_message_entity_1 = require("./stream-message-entity");
|
|
7
|
-
const lua_1 = require("./lua");
|
|
8
|
-
const uuid_1 = require("uuid");
|
|
9
|
-
class BatchWorker {
|
|
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) {
|
|
16
|
-
this.redis = redis;
|
|
17
|
-
this.groupName = groupName;
|
|
18
|
-
this.streamName = streamName;
|
|
19
|
-
this.batchSize = batchSize;
|
|
20
|
-
this.concurrency = concurrency;
|
|
21
|
-
this.maxFetchSize = maxFetchSize;
|
|
22
|
-
this.maxRetries = maxRetries;
|
|
23
|
-
this.blockTimeMs = blockTimeMs;
|
|
24
|
-
this.maxFetchCount = maxFetchCount;
|
|
25
|
-
this.claimIntervalMs = claimIntervalMs;
|
|
26
|
-
this.minIdleTimeMs = minIdleTimeMs;
|
|
27
|
-
this.isRunning = false;
|
|
28
|
-
this.activeCount = 0;
|
|
29
|
-
this.events = new events_1.EventEmitter();
|
|
30
|
-
this.consumerId = (0, uuid_1.v7)();
|
|
31
|
-
if (batchSize < 1) {
|
|
32
|
-
throw new Error('Batch size cannot be less then 0');
|
|
33
|
-
}
|
|
34
|
-
this.events.setMaxListeners(100);
|
|
35
|
-
this.keys = new keys_1.KeyManager(streamName);
|
|
36
|
-
this.blockingRedis = this.redis.duplicate();
|
|
37
|
-
}
|
|
38
|
-
async start() {
|
|
39
|
-
if (this.isRunning) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
this.isRunning = true;
|
|
43
|
-
try {
|
|
44
|
-
await this.redis.xgroup('CREATE', this.streamName, this.groupName, '0', 'MKSTREAM');
|
|
45
|
-
}
|
|
46
|
-
catch (e) {
|
|
47
|
-
if (!e.message.includes('BUSYGROUP')) {
|
|
48
|
-
throw e;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
this.fetchLoop();
|
|
52
|
-
this.autoClaimLoop();
|
|
53
|
-
}
|
|
54
|
-
async stop() {
|
|
55
|
-
this.isRunning = false;
|
|
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
|
-
}
|
|
65
|
-
while (this.activeCount > 0) {
|
|
66
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
67
|
-
}
|
|
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
|
-
}
|
|
108
|
-
async fetchLoop() {
|
|
109
|
-
while (this.isRunning) {
|
|
110
|
-
const freeSlots = this.concurrency - this.activeCount;
|
|
111
|
-
if (freeSlots <= 0) {
|
|
112
|
-
await new Promise((resolve) => this.events.once('job_finished', resolve));
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
const calculatedCount = freeSlots * this.batchSize;
|
|
116
|
-
const itemsCount = Math.min(calculatedCount, this.maxFetchCount);
|
|
117
|
-
try {
|
|
118
|
-
const results = await this.blockingRedis.xreadgroup('GROUP', this.groupName, this.getConsumerName(), 'COUNT', itemsCount, 'BLOCK', this.blockTimeMs, 'STREAMS', this.streamName, '>');
|
|
119
|
-
if (!results) {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const messages = results[0][1];
|
|
123
|
-
for (let i = 0; i < messages.length; i += this.batchSize) {
|
|
124
|
-
const chunk = messages.slice(i, i + this.batchSize);
|
|
125
|
-
this.spawnWorker(chunk);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
catch (err) {
|
|
129
|
-
if (this.isRunning) { // Quicker grace shutdown
|
|
130
|
-
console.error(`[${this.groupName}] Fetch Error: `, err);
|
|
131
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Spawn worker for current processing
|
|
138
|
-
* @param messages
|
|
139
|
-
*/
|
|
140
|
-
spawnWorker(messages) {
|
|
141
|
-
this.activeCount++;
|
|
142
|
-
this.processInternal(messages).finally(() => {
|
|
143
|
-
this.activeCount--;
|
|
144
|
-
this.events.emit('job_finished');
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
async processInternal(rawMessages) {
|
|
148
|
-
const allMessages = rawMessages.map((msg) => new stream_message_entity_1.StreamMessageEntity(msg));
|
|
149
|
-
const messages = []; // Messages to process
|
|
150
|
-
const ignoredMessages = []; // Messages to ignore
|
|
151
|
-
for (const message of allMessages) {
|
|
152
|
-
if (message.routes.includes(this.groupName)) {
|
|
153
|
-
messages.push(message);
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
ignoredMessages.push(message);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
// ACK ignored messages
|
|
160
|
-
if (ignoredMessages.length) {
|
|
161
|
-
const pipeline = this.redis.pipeline();
|
|
162
|
-
for (const ignoredMessage of ignoredMessages) {
|
|
163
|
-
pipeline.xack(this.streamName, this.groupName, ignoredMessage.streamMessageId);
|
|
164
|
-
}
|
|
165
|
-
await pipeline.exec();
|
|
166
|
-
}
|
|
167
|
-
if (!messages.length) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
const messagesData = messages.map((msg) => msg.data);
|
|
171
|
-
try {
|
|
172
|
-
await this.process(messagesData);
|
|
173
|
-
await this.finalize(messages);
|
|
174
|
-
}
|
|
175
|
-
catch (err) {
|
|
176
|
-
console.error(`[${this.groupName}] Processing failed`, err);
|
|
177
|
-
await this.handleFailure(messages, err.message);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async handleFailure(messages, errorMessage) {
|
|
181
|
-
const pipeline = this.redis.pipeline();
|
|
182
|
-
// ack
|
|
183
|
-
for (const message of messages) {
|
|
184
|
-
pipeline.xack(this.streamName, this.groupName, message.streamMessageId);
|
|
185
|
-
}
|
|
186
|
-
const messagesToDlq = [];
|
|
187
|
-
for (const message of messages) {
|
|
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
|
-
}
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
console.error(`[${this.groupName}] Job ${message.messageUuid} failed but not routed to this group. Moving to DLQ.`);
|
|
202
|
-
messagesToDlq.push(message);
|
|
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());
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
await pipeline.exec();
|
|
207
|
-
if (messagesToDlq.length > 0) {
|
|
208
|
-
await this.finalize(messagesToDlq);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
async finalize(messages) {
|
|
212
|
-
if (messages.length === 0) {
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
const pipeline = this.redis.pipeline();
|
|
216
|
-
const timestamp = Date.now();
|
|
217
|
-
const throughputKey = this.keys.getThroughputKey(this.groupName, timestamp);
|
|
218
|
-
const totalKey = this.keys.getTotalKey(this.groupName);
|
|
219
|
-
const ids = messages.map(m => m.streamMessageId);
|
|
220
|
-
pipeline.xack(this.streamName, this.groupName, ...ids);
|
|
221
|
-
pipeline.incrby(throughputKey, ids.length);
|
|
222
|
-
pipeline.expire(throughputKey, 86400);
|
|
223
|
-
pipeline.incrby(totalKey, ids.length);
|
|
224
|
-
for (const msg of messages) {
|
|
225
|
-
const statusKey = this.keys.getJobStatusKey(msg.messageUuid);
|
|
226
|
-
pipeline.eval(lua_1.LUA_FINALIZE_COMPLEX, 2, statusKey, this.streamName, this.groupName, timestamp, msg.streamMessageId);
|
|
227
|
-
}
|
|
228
|
-
await pipeline.exec();
|
|
229
|
-
}
|
|
230
|
-
getConsumerName() {
|
|
231
|
-
return `${this.groupName}-${process.pid}-${this.consumerId}`;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
exports.BatchWorker = BatchWorker;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const vitest_1 = require("vitest");
|
|
7
|
-
const ioredis_1 = __importDefault(require("ioredis"));
|
|
8
|
-
const producer_1 = require("./producer");
|
|
9
|
-
const batch_worker_1 = require("./batch-worker");
|
|
10
|
-
const uuid_1 = require("uuid");
|
|
11
|
-
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
|
|
12
|
-
class TestBatchWorker extends batch_worker_1.BatchWorker {
|
|
13
|
-
constructor(redis, groupName, streamName, batchSize = 10, concurrency = 1, maxRetries = 3, blockTimeMs = 100) {
|
|
14
|
-
// Fix argument order: batchSize, concurrency, maxFetchSize, maxRetries, blockTimeMs
|
|
15
|
-
super(redis, groupName, streamName, batchSize, concurrency, 20, maxRetries, blockTimeMs);
|
|
16
|
-
this.processedBatches = [];
|
|
17
|
-
this.shouldFail = false;
|
|
18
|
-
this.failCount = 0;
|
|
19
|
-
this.maxFails = 0;
|
|
20
|
-
}
|
|
21
|
-
async process(data) {
|
|
22
|
-
if (this.shouldFail) {
|
|
23
|
-
this.failCount++;
|
|
24
|
-
if (this.maxFails > 0 && this.failCount > this.maxFails) {
|
|
25
|
-
// Stop failing
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
throw new Error("Simulated Batch Failure");
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
this.processedBatches.push(data);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
(0, vitest_1.describe)('Batch Worker Integration', () => {
|
|
35
|
-
let redisProducer;
|
|
36
|
-
let redisWorker;
|
|
37
|
-
let producer;
|
|
38
|
-
let streamName;
|
|
39
|
-
let worker;
|
|
40
|
-
(0, vitest_1.beforeEach)(() => {
|
|
41
|
-
// Use separate connections to avoid XREAD BLOCK blocking the producer
|
|
42
|
-
redisProducer = new ioredis_1.default(REDIS_URL);
|
|
43
|
-
redisWorker = new ioredis_1.default(REDIS_URL);
|
|
44
|
-
streamName = `test-batch-queue-${(0, uuid_1.v7)()}`;
|
|
45
|
-
producer = new producer_1.Producer(redisProducer, streamName);
|
|
46
|
-
});
|
|
47
|
-
(0, vitest_1.afterEach)(async () => {
|
|
48
|
-
if (worker)
|
|
49
|
-
await worker.stop();
|
|
50
|
-
// Wait a bit
|
|
51
|
-
await new Promise(r => setTimeout(r, 100));
|
|
52
|
-
// Cleanup
|
|
53
|
-
if (redisProducer.status === 'ready') {
|
|
54
|
-
const keys = await redisProducer.keys(`${streamName}*`);
|
|
55
|
-
if (keys.length)
|
|
56
|
-
await redisProducer.del(...keys);
|
|
57
|
-
}
|
|
58
|
-
redisProducer.disconnect();
|
|
59
|
-
redisWorker.disconnect();
|
|
60
|
-
});
|
|
61
|
-
const waitFor = async (condition, timeout = 8000) => {
|
|
62
|
-
const start = Date.now();
|
|
63
|
-
while (Date.now() - start < timeout) {
|
|
64
|
-
if (await condition())
|
|
65
|
-
return true;
|
|
66
|
-
await new Promise(r => setTimeout(r, 100));
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
69
|
-
};
|
|
70
|
-
(0, vitest_1.it)('Should process a batch of messages', async () => {
|
|
71
|
-
worker = new TestBatchWorker(redisWorker, 'group-Batch', streamName, 5, 1);
|
|
72
|
-
// Push messages BEFORE starting worker to ensure a full batch is available
|
|
73
|
-
// BatchWorker uses '0' pointer for group creation, so it will pick up existing messages
|
|
74
|
-
for (let i = 0; i < 5; i++) {
|
|
75
|
-
await producer.push({ id: `msg-${i}` }, ['group-Batch']);
|
|
76
|
-
}
|
|
77
|
-
await worker.start();
|
|
78
|
-
await waitFor(() => worker.processedBatches.length > 0);
|
|
79
|
-
(0, vitest_1.expect)(worker.processedBatches.length).toBeGreaterThanOrEqual(1);
|
|
80
|
-
// It might still split it if redis pagination behaves weirdly, but usually it grabs Count
|
|
81
|
-
const allProcessed = worker.processedBatches.flat();
|
|
82
|
-
(0, vitest_1.expect)(allProcessed.length).toBe(5);
|
|
83
|
-
(0, vitest_1.expect)(worker.processedBatches[0].length).toBe(5);
|
|
84
|
-
(0, vitest_1.expect)(worker.processedBatches[0].map(j => j.id)).toEqual(['msg-0', 'msg-1', 'msg-2', 'msg-3', 'msg-4']);
|
|
85
|
-
});
|
|
86
|
-
(0, vitest_1.it)('Should process multiple batches', async () => {
|
|
87
|
-
worker = new TestBatchWorker(redisWorker, 'group-Batch-Multi', streamName, 2, 1);
|
|
88
|
-
for (let i = 0; i < 4; i++) {
|
|
89
|
-
await producer.push({ id: `msg-${i}` }, ['group-Batch-Multi']);
|
|
90
|
-
}
|
|
91
|
-
await worker.start();
|
|
92
|
-
await waitFor(() => worker.processedBatches.flat().length === 4);
|
|
93
|
-
(0, vitest_1.expect)(worker.processedBatches.length).toBe(2);
|
|
94
|
-
});
|
|
95
|
-
(0, vitest_1.it)('Should retry failed batches', async () => {
|
|
96
|
-
worker = new TestBatchWorker(redisWorker, 'group-Batch-Retry', streamName, 5, 1, 3);
|
|
97
|
-
worker.shouldFail = true;
|
|
98
|
-
worker.maxFails = 1;
|
|
99
|
-
await worker.start();
|
|
100
|
-
await producer.push({ id: 'retry-1' }, ['group-Batch-Retry']);
|
|
101
|
-
await waitFor(() => worker.processedBatches.length === 1);
|
|
102
|
-
(0, vitest_1.expect)(worker.failCount).toBeGreaterThanOrEqual(1);
|
|
103
|
-
(0, vitest_1.expect)(worker.processedBatches.length).toBe(1);
|
|
104
|
-
(0, vitest_1.expect)(worker.processedBatches[0][0].id).toBe('retry-1');
|
|
105
|
-
});
|
|
106
|
-
(0, vitest_1.it)('Should move to DLQ after max retries', async () => {
|
|
107
|
-
worker = new TestBatchWorker(redisWorker, 'group-Batch-DLQ', streamName, 1, 1, 2);
|
|
108
|
-
worker.shouldFail = true;
|
|
109
|
-
worker.maxFails = 10;
|
|
110
|
-
await worker.start();
|
|
111
|
-
const id = await producer.push({ id: 'dlq-1' }, ['group-Batch-DLQ']);
|
|
112
|
-
await waitFor(async () => {
|
|
113
|
-
const len = await redisProducer.xlen(`${streamName}:dlq`);
|
|
114
|
-
return len > 0;
|
|
115
|
-
}, 10000);
|
|
116
|
-
const dlqLen = await redisProducer.xlen(`${streamName}:dlq`);
|
|
117
|
-
(0, vitest_1.expect)(dlqLen).toBe(1);
|
|
118
|
-
const dlqMsgs = await redisProducer.xrange(`${streamName}:dlq`, '-', '+');
|
|
119
|
-
const body = dlqMsgs[0][1];
|
|
120
|
-
const payloadIdx = body.indexOf('payload');
|
|
121
|
-
const payload = body[payloadIdx + 1];
|
|
122
|
-
(0, vitest_1.expect)(JSON.parse(payload)).toEqual({ id: 'dlq-1' });
|
|
123
|
-
});
|
|
124
|
-
});
|
package/dist/interfaces.d.ts
DELETED
package/dist/interfaces.js
DELETED
package/dist/lua.d.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LUA_FINALIZE_COMPLEX = exports.LUA_MARK_DONE = void 0;
|
|
4
|
-
exports.LUA_MARK_DONE = `
|
|
5
|
-
-- KEYS[1] = status key status key for jog
|
|
6
|
-
-- KEYS[2] = stream key
|
|
7
|
-
-- KEYS[3] = group name
|
|
8
|
-
-- KEYS[4] = metrics key
|
|
9
|
-
-- KEYS[5] = total metrics key(persistent)
|
|
10
|
-
|
|
11
|
-
-- ARGV[1] = route name
|
|
12
|
-
-- ARGV[2] = timestamp
|
|
13
|
-
-- ARGV[3] = msgId - redis stream item ID
|
|
14
|
-
|
|
15
|
-
-- 1 Ack the stream message
|
|
16
|
-
redis.call('XACK', KEYS[2], KEYS[3], ARGV[3])
|
|
17
|
-
|
|
18
|
-
-- 2 in status key mark the current route as done by saving timestamp
|
|
19
|
-
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
|
|
20
|
-
|
|
21
|
-
-- 3 Increment throughput metric
|
|
22
|
-
if KEYS[5] then
|
|
23
|
-
redis.call('INCR', KEYS[4])
|
|
24
|
-
redis.call('EXPIRE', KEYS[4], 86400)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
-- 4 Increment Total Metric
|
|
28
|
-
redis.call('INCR', KEYS[5])
|
|
29
|
-
|
|
30
|
-
-- 5 Check for completed routes
|
|
31
|
-
local current_fields = redis.call('HLEN', KEYS[1])
|
|
32
|
-
|
|
33
|
-
-- 6 Get the target completed routes
|
|
34
|
-
local target_str = redis.call('HGET', KEYS[1], '__target')
|
|
35
|
-
local target = tonumber(target_str)
|
|
36
|
-
|
|
37
|
-
if not target then
|
|
38
|
-
return 0
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
-- 7 If completed routes is status hash length - 1 -> all were done and we can cleanup
|
|
42
|
-
if current_fields >= (target + 1) then
|
|
43
|
-
redis.call('DEL', KEYS[1]) -- Only delete status key
|
|
44
|
-
redis.call('XDEL', KEYS[2], ARGV[3])
|
|
45
|
-
return 1 -- Cleanup, DONE
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
return 0 -- Some routes are not done yet
|
|
49
|
-
`;
|
|
50
|
-
exports.LUA_FINALIZE_COMPLEX = `
|
|
51
|
-
-- KEYS[1] = status key
|
|
52
|
-
-- KEYS[2] = stream key
|
|
53
|
-
-- ARGV[1] = group name
|
|
54
|
-
-- ARGV[2] = timestamp
|
|
55
|
-
-- ARGV[3] = msgId
|
|
56
|
-
|
|
57
|
-
-- 1. Update status
|
|
58
|
-
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
|
|
59
|
-
|
|
60
|
-
-- 2. Check completions
|
|
61
|
-
local current_fields = redis.call('HLEN', KEYS[1])
|
|
62
|
-
local target_str = redis.call('HGET', KEYS[1], '__target')
|
|
63
|
-
local target = tonumber(target_str)
|
|
64
|
-
|
|
65
|
-
if not target then
|
|
66
|
-
return 0
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
-- 3. Cleanup if done
|
|
70
|
-
if current_fields >= (target + 1) then
|
|
71
|
-
redis.call('DEL', KEYS[1])
|
|
72
|
-
redis.call('XDEL', KEYS[2], ARGV[3])
|
|
73
|
-
return 1
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
return 0
|
|
77
|
-
`;
|
package/dist/worker.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import Redis from "ioredis";
|
|
2
|
-
export declare abstract class Worker<T extends Record<string, unknown>> {
|
|
3
|
-
protected redis: Redis;
|
|
4
|
-
protected groupName: string;
|
|
5
|
-
protected streamName: string;
|
|
6
|
-
protected concurrency: number;
|
|
7
|
-
protected MAX_RETRIES: number;
|
|
8
|
-
protected blockTimeMs: number;
|
|
9
|
-
protected claimIntervalMs: number;
|
|
10
|
-
protected minIdleTimeMs: number;
|
|
11
|
-
private isRunning;
|
|
12
|
-
private activeCount;
|
|
13
|
-
private readonly events;
|
|
14
|
-
private keys;
|
|
15
|
-
private consumerId;
|
|
16
|
-
private blockingRedis;
|
|
17
|
-
constructor(redis: Redis, groupName: string, streamName: string, concurrency?: number, MAX_RETRIES?: number, blockTimeMs?: number, claimIntervalMs?: number, minIdleTimeMs?: number);
|
|
18
|
-
/**
|
|
19
|
-
* Start worker
|
|
20
|
-
* @returns
|
|
21
|
-
*/
|
|
22
|
-
start(): Promise<void>;
|
|
23
|
-
stop(): Promise<void>;
|
|
24
|
-
private autoClaimLoop;
|
|
25
|
-
private fetchLoop;
|
|
26
|
-
private spawnWorker;
|
|
27
|
-
private processInternal;
|
|
28
|
-
private handleFailure;
|
|
29
|
-
private finalize;
|
|
30
|
-
private consumerName;
|
|
31
|
-
abstract process(data: T): Promise<void>;
|
|
32
|
-
}
|