queasy 0.3.0 → 0.3.1
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/.github/workflows/check.yml +3 -0
- package/.github/workflows/publish.yml +3 -0
- package/CLAUDE.md +5 -4
- package/biome.json +5 -1
- package/dist/client.d.ts +33 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +199 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/{src → dist}/constants.js +1 -9
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/{src → dist}/errors.js +1 -13
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/manager.d.ts +19 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +67 -0
- package/dist/manager.js.map +1 -0
- package/dist/pool.d.ts +29 -0
- package/dist/pool.d.ts.map +1 -0
- package/{src → dist}/pool.js +23 -82
- package/dist/pool.js.map +1 -0
- package/dist/queasy.lua +390 -0
- package/dist/queue.d.ts +22 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +81 -0
- package/dist/queue.js.map +1 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +24 -0
- package/dist/utils.js.map +1 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +42 -0
- package/dist/worker.js.map +1 -0
- package/fuzztest/{fuzz.js → fuzz.ts} +53 -51
- package/fuzztest/handlers/{cascade-a.js → cascade-a.ts} +11 -15
- package/fuzztest/handlers/{cascade-b.js → cascade-b.ts} +8 -9
- package/fuzztest/handlers/{fail-handler.js → fail-handler.ts} +7 -12
- package/fuzztest/handlers/{periodic.js → periodic.ts} +11 -15
- package/fuzztest/{process.js → process.ts} +15 -15
- package/fuzztest/shared/{chaos.js → chaos.ts} +5 -4
- package/fuzztest/shared/{stream.js → stream.ts} +7 -7
- package/package.json +7 -5
- package/src/{client.js → client.ts} +86 -128
- package/src/constants.ts +33 -0
- package/src/errors.ts +13 -0
- package/src/index.ts +2 -0
- package/src/manager.ts +78 -0
- package/src/pool.ts +129 -0
- package/src/queue.ts +108 -0
- package/src/types.ts +1 -0
- package/src/{utils.js → utils.ts} +3 -20
- package/src/{worker.js → worker.ts} +5 -12
- package/test/{client.test.js → client.test.ts} +6 -7
- package/test/{errors.test.js → errors.test.ts} +1 -1
- package/test/fixtures/always-fail-handler.ts +5 -0
- package/test/fixtures/data-logger-handler.ts +11 -0
- package/test/fixtures/failure-handler.ts +6 -0
- package/test/fixtures/permanent-error-handler.ts +6 -0
- package/test/fixtures/slow-handler.ts +6 -0
- package/test/fixtures/success-handler.js +0 -5
- package/test/fixtures/success-handler.ts +6 -0
- package/test/fixtures/with-failure-handler.ts +5 -0
- package/test/{guards.test.js → guards.test.ts} +9 -12
- package/test/{manager.test.js → manager.test.ts} +23 -33
- package/test/{pool.test.js → pool.test.ts} +10 -14
- package/test/{queue.test.js → queue.test.ts} +16 -17
- package/test/{redis-functions.test.js → redis-functions.test.ts} +14 -20
- package/test/{utils.test.js → utils.test.ts} +1 -1
- package/tsconfig.json +20 -0
- package/jsconfig.json +0 -17
- package/src/index.js +0 -2
- package/src/manager.js +0 -94
- package/src/queue.js +0 -154
- package/test/fixtures/always-fail-handler.js +0 -8
- package/test/fixtures/data-logger-handler.js +0 -19
- package/test/fixtures/failure-handler.js +0 -9
- package/test/fixtures/permanent-error-handler.js +0 -10
- package/test/fixtures/slow-handler.js +0 -9
- package/test/fixtures/with-failure-handler.js +0 -8
- /package/test/fixtures/{no-handle-handler.js → no-handle-handler.ts} +0 -0
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
|
|
3
3
|
import { createClient } from 'redis';
|
|
4
|
-
import {
|
|
4
|
+
import type { RedisClientType } from 'redis';
|
|
5
|
+
import { Client } from '../src/index.ts';
|
|
5
6
|
|
|
6
7
|
const QUEUE_NAME = 'test';
|
|
7
8
|
|
|
8
9
|
describe('Queue E2E', () => {
|
|
9
|
-
|
|
10
|
-
let
|
|
11
|
-
/** @type {import('../src/client.js').Client}*/
|
|
12
|
-
let client;
|
|
10
|
+
let redis: RedisClientType;
|
|
11
|
+
let client: Client;
|
|
13
12
|
|
|
14
13
|
beforeEach(async () => {
|
|
15
14
|
redis = createClient();
|
|
@@ -129,7 +128,7 @@ describe('Queue E2E', () => {
|
|
|
129
128
|
const q = client.queue(QUEUE_NAME);
|
|
130
129
|
const jobId = await q.dispatch({ greeting: 'hello' });
|
|
131
130
|
|
|
132
|
-
const handlerPath = new URL('./fixtures/success-handler.
|
|
131
|
+
const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
|
|
133
132
|
await q.listen(handlerPath);
|
|
134
133
|
await (await q.dequeue(1)).promise;
|
|
135
134
|
|
|
@@ -149,7 +148,7 @@ describe('Queue E2E', () => {
|
|
|
149
148
|
q.dispatch({ id: 3 }),
|
|
150
149
|
]);
|
|
151
150
|
|
|
152
|
-
const handlerPath = new URL('./fixtures/success-handler.
|
|
151
|
+
const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
|
|
153
152
|
await q.listen(handlerPath);
|
|
154
153
|
await (await q.dequeue(5)).promise;
|
|
155
154
|
|
|
@@ -166,7 +165,7 @@ describe('Queue E2E', () => {
|
|
|
166
165
|
const futureTime = Date.now() + 10000;
|
|
167
166
|
const jobId = await q.dispatch({ task: 'future' }, { runAt: futureTime });
|
|
168
167
|
|
|
169
|
-
const handlerPath = new URL('./fixtures/success-handler.
|
|
168
|
+
const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
|
|
170
169
|
await q.listen(handlerPath);
|
|
171
170
|
await (await q.dequeue(1)).promise;
|
|
172
171
|
|
|
@@ -185,7 +184,7 @@ describe('Queue E2E', () => {
|
|
|
185
184
|
const cancelled = await q.cancel(jobId1);
|
|
186
185
|
assert.ok(cancelled);
|
|
187
186
|
|
|
188
|
-
const handlerPath = new URL('./fixtures/success-handler.
|
|
187
|
+
const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
|
|
189
188
|
await q.listen(handlerPath);
|
|
190
189
|
await (await q.dequeue(1)).promise;
|
|
191
190
|
|
|
@@ -213,7 +212,7 @@ describe('Queue E2E', () => {
|
|
|
213
212
|
const jobId1 = await queue1.dispatch({ queue: 1 });
|
|
214
213
|
const jobId2 = await queue2.dispatch({ queue: 2 });
|
|
215
214
|
|
|
216
|
-
const handlerPath = new URL('./fixtures/success-handler.
|
|
215
|
+
const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
|
|
217
216
|
await queue1.listen(handlerPath);
|
|
218
217
|
await queue2.listen(handlerPath);
|
|
219
218
|
// First wait for these jobs to be dequeued and sent to workers
|
|
@@ -246,9 +245,9 @@ describe('Queue E2E', () => {
|
|
|
246
245
|
const q = client.queue('fail-opt');
|
|
247
246
|
await q.dispatch({ task: 'will-fail' });
|
|
248
247
|
|
|
249
|
-
const mainHandler = new URL('./fixtures/with-failure-handler.
|
|
248
|
+
const mainHandler = new URL('./fixtures/with-failure-handler.ts', import.meta.url)
|
|
250
249
|
.pathname;
|
|
251
|
-
const failHandler = new URL('./fixtures/success-handler.
|
|
250
|
+
const failHandler = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
|
|
252
251
|
|
|
253
252
|
// listen with failHandler option — this covers queue.js lines 60-63
|
|
254
253
|
await q.listen(mainHandler, { maxRetries: 0, failHandler });
|
|
@@ -271,7 +270,7 @@ describe('Queue E2E', () => {
|
|
|
271
270
|
const q = client.queue(QUEUE_NAME);
|
|
272
271
|
const jobId = await q.dispatch({ task: 'will-fail' });
|
|
273
272
|
|
|
274
|
-
const handlerPath = new URL('./fixtures/always-fail-handler.
|
|
273
|
+
const handlerPath = new URL('./fixtures/always-fail-handler.ts', import.meta.url)
|
|
275
274
|
.pathname;
|
|
276
275
|
await q.listen(handlerPath, { maxRetries: 2, minBackoff: 1000, maxBackoff: 10000 });
|
|
277
276
|
|
|
@@ -296,7 +295,7 @@ describe('Queue E2E', () => {
|
|
|
296
295
|
// Manually set stall_count to exceed maxStalls (default 3)
|
|
297
296
|
await redis.hSet(`{stall-test}:waiting_job:${jobId}`, 'stall_count', '5');
|
|
298
297
|
|
|
299
|
-
const handlerPath = new URL('./fixtures/success-handler.
|
|
298
|
+
const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
|
|
300
299
|
await q.listen(handlerPath, { maxStalls: 3 });
|
|
301
300
|
q.failKey = '{stall-test}-fail';
|
|
302
301
|
|
|
@@ -333,7 +332,7 @@ describe('Queue E2E', () => {
|
|
|
333
332
|
|
|
334
333
|
await redis.hSet(`{stall-nofail}:waiting_job:${jobId}`, 'stall_count', '5');
|
|
335
334
|
|
|
336
|
-
const handlerPath = new URL('./fixtures/success-handler.
|
|
335
|
+
const handlerPath = new URL('./fixtures/success-handler.ts', import.meta.url).pathname;
|
|
337
336
|
await q.listen(handlerPath, { maxStalls: 3 });
|
|
338
337
|
// No failKey set — job should just be finished
|
|
339
338
|
|
|
@@ -360,7 +359,7 @@ describe('Queue E2E', () => {
|
|
|
360
359
|
const q = client.queue('bad-handler');
|
|
361
360
|
await q.dispatch({ task: 'test' });
|
|
362
361
|
|
|
363
|
-
const handlerPath = new URL('./fixtures/no-handle-handler.
|
|
362
|
+
const handlerPath = new URL('./fixtures/no-handle-handler.ts', import.meta.url)
|
|
364
363
|
.pathname;
|
|
365
364
|
await q.listen(handlerPath, { maxRetries: 0 });
|
|
366
365
|
q.failKey = '{bad-handler}-fail';
|
|
@@ -398,7 +397,7 @@ describe('Queue E2E', () => {
|
|
|
398
397
|
const q = client.queue('fail-test');
|
|
399
398
|
const jobId = await q.dispatch({ task: 'will-fail' });
|
|
400
399
|
|
|
401
|
-
const handlerPath = new URL('./fixtures/with-failure-handler.
|
|
400
|
+
const handlerPath = new URL('./fixtures/with-failure-handler.ts', import.meta.url)
|
|
402
401
|
.pathname;
|
|
403
402
|
|
|
404
403
|
// Listen without failHandler so no fail queue listener races us
|
|
@@ -4,13 +4,13 @@ import { dirname, join } from 'node:path';
|
|
|
4
4
|
import { after, afterEach, before, describe, it } from 'node:test';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { createClient } from 'redis';
|
|
7
|
+
import type { RedisClientType } from 'redis';
|
|
7
8
|
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = dirname(__filename);
|
|
10
11
|
|
|
11
12
|
describe('Redis Lua functions', () => {
|
|
12
|
-
|
|
13
|
-
let redis;
|
|
13
|
+
let redis: RedisClientType;
|
|
14
14
|
const QUEUE_NAME = '{test-queue}';
|
|
15
15
|
|
|
16
16
|
before(async () => {
|
|
@@ -281,12 +281,10 @@ describe('Redis Lua functions', () => {
|
|
|
281
281
|
|
|
282
282
|
// Dequeue
|
|
283
283
|
const expiryTime = now + 10000;
|
|
284
|
-
const result =
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
})
|
|
289
|
-
);
|
|
284
|
+
const result = await redis.fCall('queasy_dequeue', {
|
|
285
|
+
keys: [QUEUE_NAME],
|
|
286
|
+
arguments: [clientId, now.toString(), expiryTime.toString(), '10'],
|
|
287
|
+
}) as string[][];
|
|
290
288
|
|
|
291
289
|
assert.equal(result.length, 2);
|
|
292
290
|
assert.deepEqual(result[0], ['id', jobId1]);
|
|
@@ -318,12 +316,10 @@ describe('Redis Lua functions', () => {
|
|
|
318
316
|
await redis.hSet(`${QUEUE_NAME}:waiting_job:${jobId}`, 'id', jobId);
|
|
319
317
|
|
|
320
318
|
// Try to dequeue
|
|
321
|
-
const result =
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
})
|
|
326
|
-
);
|
|
319
|
+
const result = await redis.fCall('queasy_dequeue', {
|
|
320
|
+
keys: [QUEUE_NAME],
|
|
321
|
+
arguments: [clientId, now.toString(), String(now + 10000), '10'],
|
|
322
|
+
}) as string[][];
|
|
327
323
|
|
|
328
324
|
assert.equal(result.length, 0);
|
|
329
325
|
});
|
|
@@ -338,12 +334,10 @@ describe('Redis Lua functions', () => {
|
|
|
338
334
|
await redis.hSet(`${QUEUE_NAME}:waiting_job:${jobId}`, 'id', jobId);
|
|
339
335
|
|
|
340
336
|
// Try to dequeue
|
|
341
|
-
const result =
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
})
|
|
346
|
-
);
|
|
337
|
+
const result = await redis.fCall('queasy_dequeue', {
|
|
338
|
+
keys: [QUEUE_NAME],
|
|
339
|
+
arguments: [clientId, now.toString(), String(now + 10000), '10'],
|
|
340
|
+
}) as string[][];
|
|
347
341
|
|
|
348
342
|
assert.equal(result.length, 0);
|
|
349
343
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
|
-
import { compareSemver, parseVersion } from '../src/utils.
|
|
3
|
+
import { compareSemver, parseVersion } from '../src/utils.ts';
|
|
4
4
|
|
|
5
5
|
describe('parseVersion', () => {
|
|
6
6
|
it('should parse a simple major.minor version', () => {
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"target": "esnext",
|
|
5
|
+
"module": "nodenext",
|
|
6
|
+
"moduleResolution": "nodenext",
|
|
7
|
+
"rewriteRelativeImportExtensions": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationMap": true,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"erasableSyntaxOnly": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"lib": ["ES2022"]
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*.ts"]
|
|
20
|
+
}
|
package/jsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"checkJs": true,
|
|
4
|
-
"strict": true,
|
|
5
|
-
"target": "ES2022",
|
|
6
|
-
"module": "ESNext",
|
|
7
|
-
"moduleResolution": "node",
|
|
8
|
-
"allowSyntheticDefaultImports": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"noEmit": true,
|
|
14
|
-
"lib": ["ES2022"]
|
|
15
|
-
},
|
|
16
|
-
"include": ["src/**/*.js", "test/**/*.js"]
|
|
17
|
-
}
|
package/src/index.js
DELETED
package/src/manager.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This class manages resource allocation between
|
|
3
|
-
* different queues based on the size of the queue
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { DEQUEUE_INTERVAL } from './constants.js';
|
|
7
|
-
|
|
8
|
-
/** @typedef {import('./pool').Pool} Pool */
|
|
9
|
-
/** @typedef {import('./queue').ProcessingQueue} Queue */
|
|
10
|
-
/** @typedef {{ queue: Queue, lastDequeuedAt: number, isBusy: boolean }} QueueEntry */
|
|
11
|
-
|
|
12
|
-
export class Manager {
|
|
13
|
-
/** @param {Pool} pool */
|
|
14
|
-
constructor(pool) {
|
|
15
|
-
this.pool = pool;
|
|
16
|
-
|
|
17
|
-
/** @type {Array<QueueEntry>} */
|
|
18
|
-
this.queues = [];
|
|
19
|
-
|
|
20
|
-
/** @type {NodeJS.Timeout?} */
|
|
21
|
-
this.timer = null;
|
|
22
|
-
|
|
23
|
-
this.busyCount = 0;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** @param {Queue} queue */
|
|
27
|
-
addQueue(queue) {
|
|
28
|
-
// Add this at the beginning so we dequeue it at the next available opportunity.
|
|
29
|
-
this.queues.unshift({ queue, lastDequeuedAt: 0, isBusy: false });
|
|
30
|
-
this.busyCount += 1;
|
|
31
|
-
|
|
32
|
-
// This delay is required for queue listen tests
|
|
33
|
-
// as they need to be able to control dequeueing
|
|
34
|
-
this.next();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async next() {
|
|
38
|
-
// If this function is called while the previous execution is in progress,
|
|
39
|
-
// we do not want both executions to use the same queue.
|
|
40
|
-
const entry = this.queues.shift();
|
|
41
|
-
if (!entry) return;
|
|
42
|
-
if (this.timer) {
|
|
43
|
-
clearTimeout(this.timer);
|
|
44
|
-
this.timer = null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const size = entry.queue.handlerOptions.size;
|
|
48
|
-
if (this.pool.capacity < size) return;
|
|
49
|
-
|
|
50
|
-
const batchSize = Math.max(1, Math.floor(this.pool.capacity / this.busyCount / size));
|
|
51
|
-
entry.lastDequeuedAt = Date.now(); // We store the time just before the call to dequeue.
|
|
52
|
-
const { count } = await entry.queue.dequeue(batchSize);
|
|
53
|
-
|
|
54
|
-
// Update
|
|
55
|
-
const nowBusy = count >= batchSize;
|
|
56
|
-
this.busyCount += Number(nowBusy) - Number(entry.isBusy);
|
|
57
|
-
entry.isBusy = nowBusy;
|
|
58
|
-
|
|
59
|
-
this.queues.push(entry);
|
|
60
|
-
this.queues.sort(compareQueueEntries);
|
|
61
|
-
|
|
62
|
-
if (!this.timer && this.queues.length) {
|
|
63
|
-
const { isBusy, lastDequeuedAt } = this.queues[0];
|
|
64
|
-
// If the current top queue is busy, retry now.
|
|
65
|
-
const delay = isBusy ? 0 : Math.max(0, lastDequeuedAt - Date.now() + DEQUEUE_INTERVAL);
|
|
66
|
-
this.timer = setTimeout(() => this.next(), delay);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
close() {
|
|
71
|
-
if (this.timer) clearTimeout(this.timer);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* @param {QueueEntry} a
|
|
77
|
-
* @param {QueueEntry} b
|
|
78
|
-
* @returns -1 | 0 | 1
|
|
79
|
-
*/
|
|
80
|
-
function compareQueueEntries(a, b) {
|
|
81
|
-
if (a.isBusy > b.isBusy) return -1; // a busy, b not -> a first
|
|
82
|
-
if (a.isBusy < b.isBusy) return 1; // a free, b busy -> b first
|
|
83
|
-
|
|
84
|
-
if (a.queue.handlerOptions.priority > b.queue.handlerOptions.priority) return 1; // a higher -> a first
|
|
85
|
-
if (a.queue.handlerOptions.priority < b.queue.handlerOptions.priority) return -1; // b higher -> b first
|
|
86
|
-
|
|
87
|
-
if (a.lastDequeuedAt > b.lastDequeuedAt) return -1; // a newer -> b first
|
|
88
|
-
if (a.lastDequeuedAt < b.lastDequeuedAt) return 1; // a older -> a first
|
|
89
|
-
|
|
90
|
-
if (a.queue.handlerOptions.size > b.queue.handlerOptions.size) return 1; // a larger -> a first
|
|
91
|
-
if (a.queue.handlerOptions.size < b.queue.handlerOptions.size) return -1; // b larger -> b first
|
|
92
|
-
|
|
93
|
-
return 0;
|
|
94
|
-
}
|
package/src/queue.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_RETRY_OPTIONS, FAILJOB_RETRY_OPTIONS } from './constants.js';
|
|
2
|
-
import { generateId } from './utils.js';
|
|
3
|
-
|
|
4
|
-
// Import types:
|
|
5
|
-
/** @typedef {import('redis').RedisClientType} RedisClient */
|
|
6
|
-
/** @typedef {import('./types').HandlerOptions} HandlerOptions */
|
|
7
|
-
/** @typedef {import('./types').ListenOptions} ListenOptions */
|
|
8
|
-
/** @typedef {import('./types').JobOptions} JobOptions */
|
|
9
|
-
/** @typedef {import('./types').Job} Job */
|
|
10
|
-
/** @typedef {import('./types').DoneMessage} DoneMessage */
|
|
11
|
-
/** @typedef {import('./client').Client} Client */
|
|
12
|
-
/** @typedef {import('./pool').Pool} Pool */
|
|
13
|
-
/** @typedef {import('./manager').Manager} Manager */
|
|
14
|
-
|
|
15
|
-
/** @typedef {Required<Partial<Pick<Queue, keyof Queue>>>} ProcessingQueue */
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Queue instance for managing a named job queue
|
|
19
|
-
*/
|
|
20
|
-
export class Queue {
|
|
21
|
-
/**
|
|
22
|
-
* @param {string} key - Queue key
|
|
23
|
-
* @param {Client} client - Redis client wrapper
|
|
24
|
-
* @param {Pool | undefined} pool - Worker pool
|
|
25
|
-
* @param {Manager | undefined} manager - Capacity allocation manager
|
|
26
|
-
*/
|
|
27
|
-
constructor(key, client, pool, manager) {
|
|
28
|
-
this.key = key;
|
|
29
|
-
this.client = client;
|
|
30
|
-
this.pool = pool;
|
|
31
|
-
this.manager = manager;
|
|
32
|
-
|
|
33
|
-
/** @type {NodeJS.Timeout | undefined} */
|
|
34
|
-
// this.dequeueInterval = undefined;
|
|
35
|
-
|
|
36
|
-
/** @type {Required<HandlerOptions> | undefined} */
|
|
37
|
-
this.handlerOptions = undefined;
|
|
38
|
-
|
|
39
|
-
/** @type {string | undefined} */
|
|
40
|
-
this.handlerPath = undefined;
|
|
41
|
-
|
|
42
|
-
/** @type {string | undefined} */
|
|
43
|
-
this.failKey = undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Attach handlers to process jobs from this queue
|
|
48
|
-
* @param {string} handlerPath - Path to handler module
|
|
49
|
-
* @param {ListenOptions} [options] - Retry strategy options and failure handler
|
|
50
|
-
* @returns {Promise<void>}
|
|
51
|
-
*/
|
|
52
|
-
async listen(handlerPath, { failHandler, failRetryOptions, ...retryOptions } = {}) {
|
|
53
|
-
if (this.client.disconnected) throw new Error('Can’t listen: client disconnected');
|
|
54
|
-
if (!this.pool || !this.manager) throw new Error('Can’t listen: non-processing client');
|
|
55
|
-
|
|
56
|
-
this.handlerPath = handlerPath;
|
|
57
|
-
this.handlerOptions = { ...DEFAULT_RETRY_OPTIONS, ...retryOptions };
|
|
58
|
-
|
|
59
|
-
// Initialize failure handler on all workers if provided
|
|
60
|
-
if (failHandler) {
|
|
61
|
-
this.failKey = `${this.key}-fail`;
|
|
62
|
-
const failQueue = this.client.queue(this.failKey, true);
|
|
63
|
-
failQueue.listen(failHandler, { ...FAILJOB_RETRY_OPTIONS, ...failRetryOptions });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
this.manager.addQueue(/** @type {ProcessingQueue} */ (this));
|
|
67
|
-
|
|
68
|
-
// if (!this.dequeueInterval) {
|
|
69
|
-
// this.dequeueInterval = setInterval(() => this.dequeue(), DEQUEUE_INTERVAL);
|
|
70
|
-
// }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Add a job to the queue
|
|
75
|
-
* @param {any} data - Job data (any JSON-serializable value)
|
|
76
|
-
* @param {JobOptions} [options] - Job options
|
|
77
|
-
* @returns {Promise<string>} Job ID
|
|
78
|
-
*/
|
|
79
|
-
async dispatch(data, options = {}) {
|
|
80
|
-
if (this.client.disconnected) throw new Error('Can’t dispatch: client disconnected');
|
|
81
|
-
const {
|
|
82
|
-
id = generateId(),
|
|
83
|
-
runAt = 0,
|
|
84
|
-
updateData = false,
|
|
85
|
-
updateRunAt = false,
|
|
86
|
-
resetCounts = false,
|
|
87
|
-
} = options;
|
|
88
|
-
|
|
89
|
-
await this.client.dispatch(this.key, id, runAt, data, updateData, updateRunAt, resetCounts);
|
|
90
|
-
return id;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Cancel a waiting job
|
|
95
|
-
* @param {string} id - Job ID
|
|
96
|
-
* @returns {Promise<boolean>} True if job was cancelled
|
|
97
|
-
*/
|
|
98
|
-
async cancel(id) {
|
|
99
|
-
if (this.client.disconnected) throw new Error('Can’t cancel: client disconnected');
|
|
100
|
-
return await this.client.cancel(this.key, id);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Picks jobs from the queue and processes them
|
|
105
|
-
* @param {number} count
|
|
106
|
-
* @returns {Promise<{count: number, promise: Promise<Array<unknown>>}>}
|
|
107
|
-
*/
|
|
108
|
-
|
|
109
|
-
async dequeue(count) {
|
|
110
|
-
const { pool, handlerPath, handlerOptions } = /** @type {ProcessingQueue} */ (this);
|
|
111
|
-
const { maxRetries, maxStalls, maxBackoff, minBackoff, size, timeout } = handlerOptions;
|
|
112
|
-
|
|
113
|
-
// const capacity = pool.getCapacity(size);
|
|
114
|
-
// if (capacity <= 0) return;
|
|
115
|
-
|
|
116
|
-
const jobs = await this.client.dequeue(this.key, count);
|
|
117
|
-
|
|
118
|
-
const promise = Promise.all(
|
|
119
|
-
jobs.map(async (job) => {
|
|
120
|
-
// Check if job has exceeded stall limit
|
|
121
|
-
if (job.stallCount >= maxStalls) {
|
|
122
|
-
// Job has stalled too many times - fail it permanently
|
|
123
|
-
if (!this.failKey) return this.client.finish(this.key, job.id);
|
|
124
|
-
|
|
125
|
-
const failJobData = [job.id, job.data, { message: 'Max stalls exceeded' }];
|
|
126
|
-
return this.client.fail(this.key, this.failKey, job.id, failJobData);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
await pool.process(handlerPath, job, size, timeout);
|
|
131
|
-
await this.client.finish(this.key, job.id);
|
|
132
|
-
} catch (message) {
|
|
133
|
-
const { error } = /** @type {Required<DoneMessage>} */ (message);
|
|
134
|
-
const { retryAt = 0, kind } = error;
|
|
135
|
-
|
|
136
|
-
if (kind === 'permanent' || job.retryCount >= maxRetries) {
|
|
137
|
-
if (!this.failKey) return this.client.finish(this.key, job.id);
|
|
138
|
-
|
|
139
|
-
const failJobData = [job.id, job.data, error];
|
|
140
|
-
return this.client.fail(this.key, this.failKey, job.id, failJobData);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const backoffUntil =
|
|
144
|
-
Date.now() + Math.min(maxBackoff, minBackoff * 2 ** job.retryCount);
|
|
145
|
-
|
|
146
|
-
// Retriable error: call retry
|
|
147
|
-
await this.client.retry(this.key, job.id, Math.max(retryAt, backoffUntil));
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
return { count: jobs.length, promise };
|
|
153
|
-
}
|
|
154
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handler that logs received data to a global array (for testing)
|
|
3
|
-
* @param {any} data - Job data
|
|
4
|
-
* @param {import('../../src/types.js').Job} job - Job metadata
|
|
5
|
-
*/
|
|
6
|
-
/** @type {{ data: any, job: import('../../src/types.js').Job }[]} */
|
|
7
|
-
export const receivedJobs = [];
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @param {any} data - Job data
|
|
11
|
-
* @param {import('../../src/types.js').Job} job - Job metadata
|
|
12
|
-
*/
|
|
13
|
-
export async function handle(data, job) {
|
|
14
|
-
receivedJobs.push({ data, job });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function clearLogs() {
|
|
18
|
-
receivedJobs.length = 0;
|
|
19
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Failure handler
|
|
3
|
-
* @param {any} data - Failure data [originalJobId, originalData, error]
|
|
4
|
-
* @param {import('../../src/types.js').Job} _job - Job metadata
|
|
5
|
-
*/
|
|
6
|
-
export async function handle(data, _job) {
|
|
7
|
-
// Just succeed to process the failure
|
|
8
|
-
console.log('Failure handler called with:', data);
|
|
9
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { PermanentError } from '../../src/errors.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Handler that throws a PermanentError
|
|
5
|
-
* @param {any} _data - Job data
|
|
6
|
-
* @param {import('../../src/types.js').Job} _job - Job metadata
|
|
7
|
-
*/
|
|
8
|
-
export async function handle(_data, _job) {
|
|
9
|
-
throw new PermanentError('Permanent failure');
|
|
10
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handler that takes some time to complete (for testing heartbeat)
|
|
3
|
-
* @param {any} data - Job data
|
|
4
|
-
* @param {import('../../src/types.js').Job} _job - Job metadata
|
|
5
|
-
*/
|
|
6
|
-
export async function handle(data, _job) {
|
|
7
|
-
const delay = data?.delay || 1000;
|
|
8
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
9
|
-
}
|
|
File without changes
|