Skip to content

Commit 47ba055

Browse files
authored
feat(queue): support global concurrency (#2496) ref #2465
1 parent 7fdd892 commit 47ba055

32 files changed

+896
-98
lines changed

docs/gitbook/guide/workers/concurrency.md

+22-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,29 @@
22

33
There are basically two ways to achieve concurrency with BullMQ. You can run a worker with a concurrency factor larger than 1 \(which is the default value\), or you can run several workers in different node processes.
44

5-
#### Concurrency factor
5+
#### Global Concurrency factor
66

7-
The concurrency factor is a worker option that determines how many jobs are allowed to be processed in parallel. This means that the same worker is able to process several jobs in parallel, however the queue guarantees such as "at-least-once" and order of processing are still preserved.
7+
The global concurrency factor is a queue option that determines how many jobs are allowed to be processed in parallel across all your worker instances.
8+
9+
```typescript
10+
import { Queue } from 'bullmq';
11+
12+
await queue.setGlobalConcurrency(4);
13+
```
14+
15+
And in order to get this value:
16+
17+
```typescript
18+
const globalConcurrency = await queue.getGlobalConcurrency();
19+
```
20+
21+
{% hint style="info" %}
22+
Note that if you choose a concurrency level in your workers, it will not override the global one, it will just be the maximum jobs a given worker can process in parallel but never more than the global one.
23+
{% endhint %}
24+
25+
#### Local Concurrency factor
26+
27+
The local concurrency factor is a worker option that determines how many jobs are allowed to be processed in parallel for that instance. This means that the same worker is able to process several jobs in parallel, however the queue guarantees such as "at-least-once" and order of processing are still preserved.
828

929
```typescript
1030
import { Worker, Job } from 'bullmq';

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"nyc": "^15.1.0",
108108
"prettier": "^2.7.1",
109109
"pretty-quick": "^3.1.3",
110+
"progress": "^2.0.3",
110111
"rimraf": "^3.0.2",
111112
"rrule": "^2.6.9",
112113
"semantic-release": "^19.0.3",

python/bullmq/scripts.py

+14-12
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ def __init__(self, prefix: str, queueName: str, redisConnection: RedisConnection
3131
self.redisConnection = redisConnection
3232
self.redisClient = redisConnection.conn
3333
self.commands = {
34-
"addStandardJob": self.redisClient.register_script(self.getScript("addStandardJob-7.lua")),
34+
"addStandardJob": self.redisClient.register_script(self.getScript("addStandardJob-8.lua")),
3535
"addDelayedJob": self.redisClient.register_script(self.getScript("addDelayedJob-6.lua")),
3636
"addParentJob": self.redisClient.register_script(self.getScript("addParentJob-4.lua")),
37-
"addPrioritizedJob": self.redisClient.register_script(self.getScript("addPrioritizedJob-7.lua")),
38-
"changePriority": self.redisClient.register_script(self.getScript("changePriority-6.lua")),
37+
"addPrioritizedJob": self.redisClient.register_script(self.getScript("addPrioritizedJob-8.lua")),
38+
"changePriority": self.redisClient.register_script(self.getScript("changePriority-7.lua")),
3939
"cleanJobsInSet": self.redisClient.register_script(self.getScript("cleanJobsInSet-2.lua")),
4040
"extendLock": self.redisClient.register_script(self.getScript("extendLock-2.lua")),
4141
"getCounts": self.redisClient.register_script(self.getScript("getCounts-1.lua")),
@@ -51,11 +51,11 @@ def __init__(self, prefix: str, queueName: str, redisConnection: RedisConnection
5151
"moveToWaitingChildren": self.redisClient.register_script(self.getScript("moveToWaitingChildren-5.lua")),
5252
"obliterate": self.redisClient.register_script(self.getScript("obliterate-2.lua")),
5353
"pause": self.redisClient.register_script(self.getScript("pause-7.lua")),
54-
"promote": self.redisClient.register_script(self.getScript("promote-8.lua")),
55-
"removeJob": self.redisClient.register_script(self.getScript("removeJob-1.lua")),
56-
"reprocessJob": self.redisClient.register_script(self.getScript("reprocessJob-7.lua")),
54+
"promote": self.redisClient.register_script(self.getScript("promote-9.lua")),
55+
"removeJob": self.redisClient.register_script(self.getScript("removeJob-2.lua")),
56+
"reprocessJob": self.redisClient.register_script(self.getScript("reprocessJob-8.lua")),
5757
"retryJob": self.redisClient.register_script(self.getScript("retryJob-11.lua")),
58-
"moveJobsToWait": self.redisClient.register_script(self.getScript("moveJobsToWait-7.lua")),
58+
"moveJobsToWait": self.redisClient.register_script(self.getScript("moveJobsToWait-8.lua")),
5959
"saveStacktrace": self.redisClient.register_script(self.getScript("saveStacktrace-1.lua")),
6060
"updateData": self.redisClient.register_script(self.getScript("updateData-1.lua")),
6161
"updateProgress": self.redisClient.register_script(self.getScript("updateProgress-3.lua")),
@@ -119,7 +119,7 @@ def addStandardJob(self, job: Job, timestamp: int, pipe = None):
119119
Add a standard job to the queue
120120
"""
121121
keys = self.getKeys(['wait', 'paused', 'meta', 'id',
122-
'completed', 'events', 'marker'])
122+
'completed', 'active', 'events', 'marker'])
123123
args = self.addJobArgs(job, None)
124124
args.append(timestamp)
125125

@@ -141,7 +141,7 @@ def addPrioritizedJob(self, job: Job, timestamp: int, pipe = None):
141141
Add a prioritized job to the queue
142142
"""
143143
keys = self.getKeys(['marker', 'meta', 'id',
144-
'prioritized', 'completed', 'events', 'pc'])
144+
'prioritized', 'completed', 'active', 'events', 'pc'])
145145
args = self.addJobArgs(job, None)
146146
args.append(timestamp)
147147

@@ -285,7 +285,7 @@ async def moveToDelayed(self, job_id: str, timestamp: int, delay: int, token: st
285285
return None
286286

287287
def promoteArgs(self, job_id: str):
288-
keys = self.getKeys(['delayed', 'wait', 'paused', 'meta', 'prioritized', 'pc', 'events', 'marker'])
288+
keys = self.getKeys(['delayed', 'wait', 'paused', 'meta', 'prioritized', 'active', 'pc', 'events', 'marker'])
289289
keys.append(self.toKey(job_id))
290290
keys.append(self.keys['events'])
291291
keys.append(self.keys['paused'])
@@ -306,7 +306,7 @@ async def promote(self, job_id: str):
306306
return None
307307

308308
def remove(self, job_id: str, remove_children: bool):
309-
keys = self.getKeys([''])
309+
keys = self.getKeys(['', 'meta'])
310310
args = [job_id, 1 if remove_children else 0]
311311

312312
return self.commands["removeJob"](keys=keys, args=args)
@@ -363,6 +363,7 @@ async def changePriority(self, job_id: str, priority:int = 0, lifo:bool = False)
363363
self.keys['paused'],
364364
self.keys['meta'],
365365
self.keys['prioritized'],
366+
self.keys['active'],
366367
self.keys['pc'],
367368
self.keys['marker']]
368369

@@ -394,6 +395,7 @@ async def reprocessJob(self, job: Job, state: str):
394395
keys.append(self.keys['wait'])
395396
keys.append(self.keys['meta'])
396397
keys.append(self.keys['paused'])
398+
keys.append(self.keys['active'])
397399
keys.append(self.keys['marker'])
398400

399401
args = [
@@ -434,7 +436,7 @@ async def obliterate(self, count: int, force: bool = False):
434436

435437
def moveJobsToWaitArgs(self, state: str, count: int, timestamp: int) -> int:
436438
keys = self.getKeys(
437-
['', 'events', state, 'wait', 'paused', 'meta', 'marker'])
439+
['', 'events', state, 'wait', 'paused', 'meta', 'active', 'marker'])
438440

439441
args = [count or 1000, timestamp or round(time.time()*1000), state]
440442
return (keys, args)

src/classes/queue.ts

+33
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,32 @@ export class Queue<
183183
});
184184
}
185185

186+
/**
187+
* Get global concurrency value.
188+
* Returns null in case no value is set.
189+
*/
190+
async getGlobalConcurrency():Promise<number|null> {
191+
const client = await this.client;
192+
const concurrency = await client.hget(this.keys.meta, 'concurrency');
193+
if(concurrency){
194+
return Number(concurrency);
195+
}
196+
return null;
197+
}
198+
199+
/**
200+
* Enable and set global concurrency value.
201+
* @param concurrency - Maximum number of simultaneous jobs that the workers can handle.
202+
* For instance, setting this value to 1 ensures that no more than one job
203+
* is processed at any given time. If this limit is not defined, there will be no
204+
* restriction on the number of concurrent jobs.
205+
*/
206+
async setGlobalConcurrency(concurrency: number) {
207+
const client = await this.client;
208+
return client.hset(this.keys.meta, 'concurrency', concurrency);
209+
}
210+
211+
186212
/**
187213
* Adds a new job to the queue.
188214
*
@@ -301,6 +327,13 @@ export class Queue<
301327
return pausedKeyExists === 1;
302328
}
303329

330+
/**
331+
* Returns true if the queue is currently maxed.
332+
*/
333+
isMaxed(): Promise<boolean> {
334+
return this.scripts.isMaxed();
335+
}
336+
304337
/**
305338
* Get all repeatable meta jobs.
306339
*

src/classes/scripts.ts

+27-3
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export class Scripts {
107107
queueKeys.id,
108108
queueKeys.prioritized,
109109
queueKeys.completed,
110+
queueKeys.active,
110111
queueKeys.events,
111112
queueKeys.pc,
112113
];
@@ -148,6 +149,7 @@ export class Scripts {
148149
queueKeys.meta,
149150
queueKeys.id,
150151
queueKeys.completed,
152+
queueKeys.active,
151153
queueKeys.events,
152154
queueKeys.marker,
153155
];
@@ -283,7 +285,9 @@ export class Scripts {
283285
async remove(jobId: string, removeChildren: boolean): Promise<number> {
284286
const client = await this.queue.client;
285287

286-
const keys: (string | number)[] = [''].map(name => this.queue.toKey(name));
288+
const keys: (string | number)[] = ['', 'meta'].map(name =>
289+
this.queue.toKey(name),
290+
);
287291
return (<any>client).removeJob(
288292
keys.concat([jobId, removeChildren ? 1 : 0]),
289293
);
@@ -614,7 +618,9 @@ export class Scripts {
614618
return (<any>client).getCounts(args);
615619
}
616620

617-
protected getCountsPerPriorityArgs(priorities: number[]): (string | number)[] {
621+
protected getCountsPerPriorityArgs(
622+
priorities: number[],
623+
): (string | number)[] {
618624
const keys: (string | number)[] = [
619625
this.queue.keys.wait,
620626
this.queue.keys.paused,
@@ -772,6 +778,7 @@ export class Scripts {
772778
this.queue.keys.paused,
773779
this.queue.keys.meta,
774780
this.queue.keys.prioritized,
781+
this.queue.keys.active,
775782
this.queue.keys.pc,
776783
this.queue.keys.marker,
777784
];
@@ -850,6 +857,20 @@ export class Scripts {
850857
]);
851858
}
852859

860+
isMaxedArgs(): string[] {
861+
const queueKeys = this.queue.keys;
862+
const keys: string[] = [queueKeys.meta, queueKeys.active];
863+
864+
return keys;
865+
}
866+
867+
async isMaxed(): Promise<boolean> {
868+
const client = await this.queue.client;
869+
870+
const args = this.isMaxedArgs();
871+
return !!(await (<any>client).isMaxed(args));
872+
}
873+
853874
async moveToDelayed(
854875
jobId: string,
855876
timestamp: number,
@@ -984,6 +1005,7 @@ export class Scripts {
9841005
this.queue.toKey('wait'),
9851006
this.queue.toKey('paused'),
9861007
this.queue.keys.meta,
1008+
this.queue.keys.active,
9871009
this.queue.keys.marker,
9881010
];
9891011

@@ -1038,6 +1060,7 @@ export class Scripts {
10381060
this.queue.keys.wait,
10391061
this.queue.keys.meta,
10401062
this.queue.keys.paused,
1063+
this.queue.keys.active,
10411064
this.queue.keys.marker,
10421065
];
10431066

@@ -1108,6 +1131,7 @@ export class Scripts {
11081131
this.queue.keys.paused,
11091132
this.queue.keys.meta,
11101133
this.queue.keys.prioritized,
1134+
this.queue.keys.active,
11111135
this.queue.keys.pc,
11121136
this.queue.keys.events,
11131137
this.queue.keys.marker,
@@ -1179,7 +1203,7 @@ export class Scripts {
11791203
const client = await this.queue.client;
11801204
const lockKey = `${this.queue.toKey(jobId)}:lock`;
11811205

1182-
const keys = [
1206+
const keys: (string | number)[] = [
11831207
this.queue.keys.active,
11841208
this.queue.keys.wait,
11851209
this.queue.keys.stalled,

src/classes/worker.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ will never work with more accuracy than 1ms. */
619619

620620
let timeout: NodeJS.Timeout;
621621
try {
622-
if (!this.closing) {
622+
if (!this.closing && !this.limitUntil) {
623623
let blockTimeout = this.getBlockTimeout(blockUntil);
624624

625625
if (blockTimeout > 0) {

src/commands/addPrioritizedJob-7.lua src/commands/addPrioritizedJob-8.lua

+9-7
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
KEYS[3] 'id'
1111
KEYS[4] 'prioritized'
1212
KEYS[5] 'completed'
13-
KEYS[6] events stream key
14-
KEYS[7] 'pc' priority counter
13+
KEYS[6] 'active'
14+
KEYS[7] events stream key
15+
KEYS[8] 'pc' priority counter
1516
1617
ARGV[1] msgpacked arguments array
1718
[1] key prefix,
@@ -36,8 +37,9 @@ local idKey = KEYS[3]
3637
local priorityKey = KEYS[4]
3738

3839
local completedKey = KEYS[5]
39-
local eventsKey = KEYS[6]
40-
local priorityCounterKey = KEYS[7]
40+
local activeKey = KEYS[6]
41+
local eventsKey = KEYS[7]
42+
local priorityCounterKey = KEYS[8]
4143

4244
local jobId
4345
local jobIdKey
@@ -58,7 +60,7 @@ local parentData
5860
--- @include "includes/storeJob"
5961
--- @include "includes/getOrSetMaxEvents"
6062
--- @include "includes/handleDuplicatedJob"
61-
--- @include "includes/isQueuePaused"
63+
--- @include "includes/isQueuePausedOrMaxed"
6264

6365
if parentKey ~= nil then
6466
if rcall("EXISTS", parentKey) ~= 1 then return -5 end
@@ -91,8 +93,8 @@ local delay, priority = storeJob(eventsKey, jobIdKey, jobId, args[3], ARGV[2],
9193
repeatJobKey)
9294

9395
-- Add the job to the prioritized set
94-
local isPause = isQueuePaused(metaKey)
95-
addJobWithPriority( KEYS[1], priorityKey, priority, jobId, priorityCounterKey, isPause)
96+
local isPausedOrMaxed = isQueuePausedOrMaxed(metaKey, activeKey)
97+
addJobWithPriority( KEYS[1], priorityKey, priority, jobId, priorityCounterKey, isPausedOrMaxed)
9698

9799
-- Emit waiting event
98100
rcall("XADD", eventsKey, "MAXLEN", "~", maxEvents, "*", "event", "waiting",

src/commands/addStandardJob-7.lua src/commands/addStandardJob-8.lua

+6-5
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
KEYS[3] 'meta'
2121
KEYS[4] 'id'
2222
KEYS[5] 'completed'
23-
KEYS[6] events stream key
24-
KEYS[7] marker key
23+
KEYS[6] 'active'
24+
KEYS[7] events stream key
25+
KEYS[8] marker key
2526
2627
ARGV[1] msgpacked arguments array
2728
[1] key prefix,
@@ -41,7 +42,7 @@
4142
jobId - OK
4243
-5 - Missing parent key
4344
]]
44-
local eventsKey = KEYS[6]
45+
local eventsKey = KEYS[7]
4546

4647
local jobId
4748
local jobIdKey
@@ -94,11 +95,11 @@ end
9495
storeJob(eventsKey, jobIdKey, jobId, args[3], ARGV[2], opts, timestamp,
9596
parentKey, parentData, repeatJobKey)
9697

97-
local target, paused = getTargetQueueList(metaKey, KEYS[1], KEYS[2])
98+
local target, isPausedOrMaxed = getTargetQueueList(metaKey, KEYS[6], KEYS[1], KEYS[2])
9899

99100
-- LIFO or FIFO
100101
local pushCmd = opts['lifo'] and 'RPUSH' or 'LPUSH'
101-
addJobInTargetList(target, KEYS[7], pushCmd, paused, jobId)
102+
addJobInTargetList(target, KEYS[8], pushCmd, isPausedOrMaxed, jobId)
102103

103104
-- Emit waiting event
104105
rcall("XADD", eventsKey, "MAXLEN", "~", maxEvents, "*", "event", "waiting",

0 commit comments

Comments
 (0)