You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add exclusive concurrency controls for workflows and tasks (#15177)
Adds exclusive concurrency controls to prevent race conditions when
multiple jobs operate on the same resource. Jobs with the same
concurrency key will not run in parallel.
## Key Changes
- Added `concurrency` option to workflows and tasks for defining
resource keys
- Added `enableConcurrencyControl` feature flag (defaults to `false`,
will be `true` in 4.0)
- Added indexed `concurrencyKey` field to jobs collection when
`enableConcurrencyControl` is set to `true`
## Usage Example
```typescript
export default buildConfig({
jobs: {
enableConcurrencyControl: true,
workflows: [{
slug: 'syncDocument',
concurrency: ({ input }) => `sync:${input.documentId}`,
handler: async ({ job }) => {
// Only one job per documentId runs at a time
}
}]
}
})
```
Copy file name to clipboardExpand all lines: docs/jobs-queue/tasks.mdx
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -32,6 +32,7 @@ Simply add a task to the `jobs.tasks` array in your Payload config. A task consi
32
32
|`onFail`| Function to be executed if the task fails. |
33
33
|`onSuccess`| Function to be executed if the task succeeds. |
34
34
|`retries`| Specify the number of times that this step should be retried if it fails. If this is undefined, the task will either inherit the retries from the workflow or have no retries. If this is 0, the task will not be retried. By default, this is undefined. |
35
+
|`concurrency`| Control how jobs with the same concurrency key are handled. Jobs with the same key will run exclusively (one at a time). Requires `jobs.enableConcurrencyControl: true` to be set. See [Concurrency Controls](/docs/jobs-queue/workflows#concurrency-controls) for details. |
35
36
36
37
The logic for the Task is defined in the `handler` - which can be defined as a function, or a path to a function. The `handler` will run once a worker picks up a Job that includes this task.
Copy file name to clipboardExpand all lines: docs/jobs-queue/workflows.mdx
+146Lines changed: 146 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -56,6 +56,7 @@ To define a JS-based workflow, simply add a workflow to the `jobs.workflows` arr
56
56
|`label`| Define a human-friendly label for this workflow. |
57
57
|`queue`| Optionally, define the queue name that this workflow should be tied to. Defaults to "default". |
58
58
|`retries`| You can define `retries` on the workflow level, which will enforce that the workflow can only fail up to that number of retries. If a task does not have retries specified, it will inherit the retry count as specified on the workflow. You can specify `0` as `workflow` retries, which will disregard all `task` retry specifications and fail the entire workflow on any task failure. You can leave `workflow` retries as undefined, in which case, the workflow will respect what each task dictates as their own retry count. By default this is undefined, meaning workflows retries are defined by their tasks |
59
+
|`concurrency`| Control how jobs with the same concurrency key are handled. Jobs with the same key will run exclusively (one at a time). Requires `jobs.enableConcurrencyControl: true` to be set. See [Concurrency Controls](#concurrency-controls) below for details. |
When multiple jobs operate on the same resource, race conditions can occur. For example, if a user creates a document and then quickly updates it, two jobs might be queued that both try to process the same document simultaneously, leading to unexpected results.
349
+
350
+
The `concurrency` option allows you to prevent this by ensuring that jobs with the same "key" run exclusively (one at a time).
351
+
352
+
<Bannertype="warning">
353
+
**Important:** To use concurrency controls, you must first enable them in your
354
+
Payload config by setting `jobs.enableConcurrencyControl: true`. This adds an
355
+
indexed `concurrencyKey` field to your jobs collection schema and may require
356
+
a database migration depending on your database adapter.
357
+
</Banner>
358
+
359
+
#### Enabling Concurrency Controls
360
+
361
+
First, enable the feature in your Payload config:
362
+
363
+
```ts
364
+
exportdefaultbuildConfig({
365
+
jobs: {
366
+
enableConcurrencyControl: true,
367
+
// ... your tasks and workflows
368
+
},
369
+
})
370
+
```
371
+
372
+
Then add the `concurrency` option to your workflow configuration:
// This runs exclusively - no other job for the same
387
+
// documentId can run at the same time
388
+
const doc =awaitreq.payload.findByID({
389
+
collection: 'posts',
390
+
id: job.input.documentId,
391
+
})
392
+
393
+
awaitreq.payload.update({
394
+
collection: 'posts',
395
+
id: job.input.documentId,
396
+
data: { syncedAt: newDate().toISOString() },
397
+
})
398
+
399
+
return { output: { synced: true } }
400
+
},
401
+
})
402
+
},
403
+
},
404
+
],
405
+
},
406
+
})
407
+
```
408
+
409
+
#### How It Works
410
+
411
+
When you define a `concurrency` key:
412
+
413
+
1.**When queuing:** The concurrency key is computed from the job's input and stored on the job document.
414
+
415
+
2.**When running:** The job runner enforces exclusive execution through two mechanisms:
416
+
417
+
- It first checks which concurrency keys are currently being processed and excludes pending jobs with those keys from the query
418
+
- If multiple pending jobs with the same key are picked up in the same batch, only the first one (by creation order) runs - the others are released back to `processing: false` and will be picked up on subsequent runs
419
+
420
+
3.**Result:** Jobs with the same concurrency key are guaranteed to run sequentially, never in parallel. All jobs are preserved and will eventually complete - they just wait their turn.
421
+
422
+
#### Concurrency Configuration Options
423
+
424
+
The `concurrency` option accepts either a function (shorthand) or an object with more options:
Include the queue name in the key to allow the same resource to be processed concurrently in different queues (e.g., `emails` queue vs `default` queue).
480
+
481
+
#### Important Considerations
482
+
483
+
-**Key uniqueness:** The concurrency key should uniquely identify the resource being operated on. Include all relevant identifiers (collection slug, document ID, locale, etc.).
484
+
485
+
-**Global by default:** By default, concurrency is global across all queues. A job with key `sync:doc1` in the `default` queue will block a job with the same key in the `emails` queue. Include the queue name in your key if you want queue-specific concurrency.
486
+
487
+
-**No concurrency key = no restrictions:** Jobs without a concurrency configuration run in parallel as before.
488
+
489
+
-**Pending jobs wait:** Jobs that can't run due to concurrency constraints remain in the queue with `processing: false` and will be picked up on subsequent runs.
0 commit comments