Skip to content

Commit 8bb27ea

Browse files
authored
fix(dynamic-rate-limit): validate job lock cases (#2975)
1 parent e8ca2ec commit 8bb27ea

7 files changed

+132
-68
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"tsc:all": "tsc && tsc -p tsconfig-cjs.json"
5454
},
5555
"dependencies": {
56-
"cron-parser": "^4.6.0",
56+
"cron-parser": "^4.9.0",
5757
"ioredis": "^5.4.1",
5858
"msgpackr": "^1.11.2",
5959
"node-abort-controller": "^3.1.1",

src/classes/scripts.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,13 +1387,11 @@ export class Scripts {
13871387
*/
13881388
async moveJobFromActiveToWait(jobId: string, token: string) {
13891389
const client = await this.queue.client;
1390-
const lockKey = `${this.queue.toKey(jobId)}:lock`;
13911390

13921391
const keys: (string | number)[] = [
13931392
this.queue.keys.active,
13941393
this.queue.keys.wait,
13951394
this.queue.keys.stalled,
1396-
lockKey,
13971395
this.queue.keys.paused,
13981396
this.queue.keys.meta,
13991397
this.queue.keys.limiter,
@@ -1404,13 +1402,22 @@ export class Scripts {
14041402

14051403
const args = [jobId, token, this.queue.toKey(jobId)];
14061404

1407-
const pttl = await this.execCommand(
1405+
const result = await this.execCommand(
14081406
client,
14091407
'moveJobFromActiveToWait',
14101408
keys.concat(args),
14111409
);
14121410

1413-
return pttl < 0 ? 0 : pttl;
1411+
if (result < 0) {
1412+
throw this.finishedErrors({
1413+
code: result,
1414+
jobId,
1415+
command: 'moveJobFromActiveToWait',
1416+
state: 'active',
1417+
});
1418+
}
1419+
1420+
return result;
14141421
}
14151422

14161423
async obliterate(opts: { force: boolean; count: number }): Promise<number> {

src/commands/moveJobFromActiveToWait-10.lua

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
--[[
2+
Function to move job from active state to wait.
3+
Input:
4+
KEYS[1] active key
5+
KEYS[2] wait key
6+
7+
KEYS[3] stalled key
8+
KEYS[4] paused key
9+
KEYS[5] meta key
10+
KEYS[6] limiter key
11+
KEYS[7] prioritized key
12+
KEYS[8] marker key
13+
KEYS[9] event key
14+
15+
ARGV[1] job id
16+
ARGV[2] lock token
17+
ARGV[3] job id key
18+
]]
19+
local rcall = redis.call
20+
21+
-- Includes
22+
--- @include "includes/addJobInTargetList"
23+
--- @include "includes/pushBackJobWithPriority"
24+
--- @include "includes/getOrSetMaxEvents"
25+
--- @include "includes/getTargetQueueList"
26+
--- @include "includes/removeLock"
27+
28+
local jobId = ARGV[1]
29+
local token = ARGV[2]
30+
local jobKey = ARGV[3]
31+
32+
local errorCode = removeLock(jobKey, KEYS[3], token, jobId)
33+
if errorCode < 0 then
34+
return errorCode
35+
end
36+
37+
local metaKey = KEYS[5]
38+
local removed = rcall("LREM", KEYS[1], 1, jobId)
39+
if removed > 0 then
40+
local target, isPausedOrMaxed = getTargetQueueList(metaKey, KEYS[1], KEYS[2], KEYS[4])
41+
42+
local priority = tonumber(rcall("HGET", ARGV[3], "priority")) or 0
43+
44+
if priority > 0 then
45+
pushBackJobWithPriority(KEYS[7], priority, jobId)
46+
else
47+
addJobInTargetList(target, KEYS[8], "RPUSH", isPausedOrMaxed, jobId)
48+
end
49+
50+
local maxEvents = getOrSetMaxEvents(metaKey)
51+
52+
-- Emit waiting event
53+
rcall("XADD", KEYS[9], "MAXLEN", "~", maxEvents, "*", "event", "waiting",
54+
"jobId", jobId)
55+
end
56+
57+
local pttl = rcall("PTTL", KEYS[6])
58+
59+
if pttl > 0 then
60+
return pttl
61+
else
62+
return 0
63+
end

tests/test_events.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -791,9 +791,15 @@ describe('events', function () {
791791
);
792792

793793
let deduplicatedCounter = 0;
794-
queueEvents.on('deduplicated', ({ jobId }) => {
795-
deduplicatedCounter++;
794+
const deduplication = new Promise<void>(resolve => {
795+
queueEvents.on('deduplicated', () => {
796+
deduplicatedCounter++;
797+
if (deduplicatedCounter == 2) {
798+
resolve();
799+
}
800+
});
796801
});
802+
797803
await job.remove();
798804

799805
await queue.add(
@@ -814,6 +820,7 @@ describe('events', function () {
814820
{ deduplication: { id: 'a1' } },
815821
);
816822
await secondJob.remove();
823+
await deduplication;
817824

818825
expect(deduplicatedCounter).to.be.equal(2);
819826
});

tests/test_rate_limiter.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,53 @@ describe('Rate Limiter', function () {
423423
await worker.close();
424424
});
425425

426+
describe('when job does not exist', () => {
427+
it('should fail with job existence error', async () => {
428+
const dynamicLimit = 250;
429+
const duration = 100;
430+
431+
const worker = new Worker(
432+
queueName,
433+
async job => {
434+
if (job.attemptsStarted === 1) {
435+
await queue.rateLimit(dynamicLimit);
436+
await queue.obliterate({ force: true });
437+
throw Worker.RateLimitError();
438+
}
439+
},
440+
{
441+
autorun: false,
442+
concurrency: 10,
443+
drainDelay: 10, // If test hangs, 10 seconds here helps to fail quicker.
444+
limiter: {
445+
max: 2,
446+
duration,
447+
},
448+
connection,
449+
prefix,
450+
},
451+
);
452+
453+
await worker.waitUntilReady();
454+
455+
const failing = new Promise<void>(resolve => {
456+
worker.on('error', err => {
457+
expect(err.message).to.be.equal(
458+
`Missing lock for job ${job.id}. moveJobFromActiveToWait`,
459+
);
460+
resolve();
461+
});
462+
});
463+
464+
const job = await queue.add('test', { foo: 'bar' });
465+
466+
worker.run();
467+
468+
await failing;
469+
await worker.close();
470+
}).timeout(4000);
471+
});
472+
426473
describe('when rate limit is too low', () => {
427474
it('should move job to wait anyway', async function () {
428475
this.timeout(4000);

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2241,7 +2241,7 @@ create-require@^1.1.0:
22412241
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
22422242
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
22432243

2244-
cron-parser@^4.6.0:
2244+
cron-parser@^4.9.0:
22452245
version "4.9.0"
22462246
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5"
22472247
integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==

0 commit comments

Comments
 (0)