-
Notifications
You must be signed in to change notification settings - Fork 431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(job): allow passing debounce as option #2666
Changes from all commits
a175b55
d9c0abf
87399a9
d916f47
dc3837a
bb16307
0755ced
a851681
a28cc43
34f1fed
375641a
bbfa2e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# Debouncing | ||
|
||
Debouncing in BullMQ is a process where job execution is delayed and deduplicated based on specific identifiers. It ensures that within a specified period, or until a specific job is completed or failed, no new jobs with the same identifier will be added to the queue. Instead, these attempts will trigger a debounced event. | ||
|
||
## Fixed Mode | ||
|
||
In the Fixed Mode, debouncing works by assigning a delay (Time to Live, TTL) to a job upon its creation. If a similar job (identified by a unique debouncer ID) is added during this delay period, it is ignored. This prevents the queue from being overwhelmed with multiple instances of the same task, thus optimizing the processing time and resource utilization. | ||
|
||
```typescript | ||
import { Queue } from 'bullmq'; | ||
|
||
const myQueue = new Queue('Paint'); | ||
|
||
// Add a job that will be debounced for 5 seconds. | ||
await myQueue.add( | ||
'house', | ||
{ color: 'white' }, | ||
{ debounce: { id: 'customValue', ttl: 5000 } }, | ||
); | ||
``` | ||
|
||
In this example, after adding the house painting job with the debouncing parameters (id and ttl), any subsequent job with the same debouncing ID customValue added within 5 seconds will be ignored. This is useful for scenarios where rapid, repetitive requests are made, such as multiple users or processes attempting to trigger the same job. | ||
|
||
Note that you must provide a debounce id that should represent your job. You can hash your entire job data or a subset of attributes for creating this identifier. | ||
|
||
## Extended Mode | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Extended Mode takes a different approach by extending the debouncing duration until the job's completion or failure. This means as long as the job remains in an incomplete state (neither succeeded nor failed), any subsequent job with the same debouncer ID will be ignored. |
||
The Extended Mode takes a different approach by extending the debouncing duration until the job's completion or failure. This means as long as the job remains in an incomplete state (neither succeeded nor failed), any subsequent job with the same debouncer ID will be ignored. | ||
|
||
```typescript | ||
// Add a job that will be debounced as this record is not finished (completed or failed). | ||
await myQueue.add( | ||
'house', | ||
{ color: 'white' }, | ||
{ debounce: { id: 'customValue' } }, | ||
); | ||
``` | ||
|
||
While this job is not moved to completed or failed state, next jobs added with same **debounce id** will be ignored and a _debounced_ event will be triggered by our QueueEvent class. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This mode is particularly useful for jobs that have a long running time or those that must not be duplicated until they are resolved, such as processing a file upload or performing a critical update that should not be repeated if the initial attempt is still in progress. |
||
This mode is particularly useful for jobs that have a long running time or those that must not be duplicated until they are resolved, such as processing a file upload or performing a critical update that should not be repeated if the initial attempt is still in progress. | ||
|
||
{% hint style="warning" %} | ||
Any manual deletion will disable the debouncing. For example, when calling _job.remove_ method. | ||
{% endhint %} | ||
|
||
## Read more: | ||
|
||
- 💡 [Add Job API Reference](https://api.docs.bullmq.io/classes/v5.Queue.html#add) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,7 @@ import type { QueueEvents } from './queue-events'; | |
const logger = debuglog('bull'); | ||
|
||
const optsDecodeMap = { | ||
de: 'debounce', | ||
fpof: 'failParentOnFailure', | ||
idof: 'ignoreDependencyOnFailure', | ||
kl: 'keepLogs', | ||
|
@@ -136,6 +137,11 @@ export class Job< | |
*/ | ||
parent?: ParentKeys; | ||
|
||
/** | ||
* Debounce identifier. | ||
*/ | ||
debounceId?: string; | ||
|
||
/** | ||
* Base repeat job key. | ||
*/ | ||
|
@@ -199,6 +205,8 @@ export class Job< | |
? { id: opts.parent.id, queueKey: opts.parent.queue } | ||
: undefined; | ||
|
||
this.debounceId = opts.debounce ? opts.debounce.id : undefined; | ||
|
||
this.toKey = queue.toKey.bind(queue); | ||
this.setScripts(); | ||
|
||
|
@@ -322,6 +330,10 @@ export class Job< | |
job.repeatJobKey = json.rjk; | ||
} | ||
|
||
if (json.deid) { | ||
job.debounceId = json.deid; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wouldn't it be better to use the same opts structure instead of having a new debounceId field in the job class? This will keep the class cleaner, specially if we add more options to the debounce option. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't save all the option because ttl is not reuse as debounceId, similar to repeatable jobs, we only save repeatJobKey instead of saving all repeat options in job instance. |
||
} | ||
|
||
job.failedReason = json.failedReason; | ||
|
||
job.attemptsStarted = parseInt(json.ats || '0'); | ||
|
@@ -445,6 +457,7 @@ export class Job< | |
timestamp: this.timestamp, | ||
failedReason: JSON.stringify(this.failedReason), | ||
stacktrace: JSON.stringify(this.stacktrace), | ||
debounceId: this.debounceId, | ||
repeatJobKey: this.repeatJobKey, | ||
returnvalue: JSON.stringify(this.returnvalue), | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,7 @@ | |
[7] parent dependencies key. | ||
[8] parent? {id, queueKey} | ||
[9] repeat job key | ||
[10] debounce key | ||
|
||
ARGV[2] Json stringified job data | ||
ARGV[3] msgpacked options | ||
|
@@ -49,12 +50,14 @@ local args = cmsgpack.unpack(ARGV[1]) | |
local data = ARGV[2] | ||
|
||
local parentKey = args[5] | ||
local repeatJobKey = args[9] | ||
local parent = args[8] | ||
local repeatJobKey = args[9] | ||
local debounceKey = args[10] | ||
local parentData | ||
|
||
-- Includes | ||
--- @include "includes/addDelayMarkerIfNeeded" | ||
--- @include "includes/debounceJob" | ||
--- @include "includes/getDelayedScore" | ||
--- @include "includes/getOrSetMaxEvents" | ||
--- @include "includes/handleDuplicatedJob" | ||
|
@@ -73,6 +76,7 @@ local opts = cmsgpack.unpack(ARGV[3]) | |
|
||
local parentDependenciesKey = args[7] | ||
local timestamp = args[4] | ||
|
||
if args[2] == "" then | ||
jobId = jobCounter | ||
jobIdKey = args[1] .. jobId | ||
|
@@ -86,6 +90,12 @@ else | |
end | ||
end | ||
|
||
local debouncedJobId = debounceJob(args[1], opts['de'], | ||
jobId, debounceKey, eventsKey, maxEvents) | ||
if debouncedJobId then | ||
return debouncedJobId | ||
end | ||
|
||
-- Store the job. | ||
local delay, priority = storeJob(eventsKey, jobIdKey, jobId, args[3], ARGV[2], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as we are storing the opts with the debounce options, why do we need to pass the debounceId again? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah I can refactor it |
||
opts, timestamp, parentKey, parentData, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
--[[ | ||
Function to debounce a job. | ||
]] | ||
|
||
local function debounceJob(prefixKey, debounceOpts, jobId, debounceKey, eventsKey, maxEvents) | ||
local debounceId = debounceOpts and debounceOpts['id'] | ||
if debounceId then | ||
local ttl = debounceOpts['ttl'] | ||
local debounceKeyExists | ||
if ttl then | ||
debounceKeyExists = not rcall('SET', debounceKey, jobId, 'PX', ttl, 'NX') | ||
else | ||
debounceKeyExists = not rcall('SET', debounceKey, jobId, 'NX') | ||
end | ||
if debounceKeyExists then | ||
local currentDebounceJobId = rcall('GET', debounceKey) | ||
rcall("XADD", eventsKey, "MAXLEN", "~", maxEvents, "*", "event", | ||
"debounced", "jobId", currentDebounceJobId) | ||
return currentDebounceJobId | ||
end | ||
end | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-> In the Fixed Mode, debouncing works by assigning a delay (Time to Live, TTL) to a job upon its creation. If a similar job (identified by a unique debouncer ID) is added during this delay period, it is ignored. This prevents the queue from being overwhelmed with multiple instances of the same task, thus optimizing the processing time and resource utilization.