feat: configurable job queue processing order (LIFO/FIFO), allow sequential execution of jobs (#11897)

Previously, jobs were executed in FIFO order on MongoDB, and LIFO on
Postgres, with no way to configure this behavior.

This PR makes FIFO the default on both MongoDB and Postgres and
introduces the following new options to configure the processing order
globally or on a queue-by-queue basis:
- a `processingOrder` property to the jobs config
- a `processingOrder` argument to `payload.jobs.run()` to override
what's set in the jobs config

It also adds a new `sequential` option to `payload.jobs.run()`, which
can be useful for debugging.
This commit is contained in:
Alessio Gravili
2025-03-31 15:00:36 -06:00
committed by GitHub
parent 9c88af4b20
commit c844b4c848
9 changed files with 340 additions and 21 deletions

View File

@@ -24,6 +24,7 @@ import { updatePostJSONWorkflow } from './workflows/updatePostJSON.js'
import { workflowAndTasksRetriesUndefinedWorkflow } from './workflows/workflowAndTasksRetriesUndefined.js'
import { workflowRetries2TasksRetries0Workflow } from './workflows/workflowRetries2TasksRetries0.js'
import { workflowRetries2TasksRetriesUndefinedWorkflow } from './workflows/workflowRetries2TasksRetriesUndefined.js'
import { inlineTaskTestDelayedWorkflow } from './workflows/inlineTaskTestDelayed.js'
import { parallelTaskWorkflow } from './workflows/parallelTaskWorkflow.js'
const filename = fileURLToPath(import.meta.url)
@@ -104,6 +105,11 @@ export default buildConfigWithDefaults({
},
}
},
processingOrder: {
queues: {
lifo: '-createdAt',
},
},
tasks: [
{
retries: 2,
@@ -376,6 +382,7 @@ export default buildConfigWithDefaults({
workflowRetries2TasksRetriesUndefinedWorkflow,
workflowRetries2TasksRetries0Workflow,
inlineTaskTestWorkflow,
inlineTaskTestDelayedWorkflow,
externalWorkflow,
retriesBackoffTestWorkflow,
subTaskWorkflow,

View File

@@ -533,6 +533,106 @@ describe('Queues', () => {
payload.config.jobs.deleteJobOnComplete = true
})
it('ensure jobs run in FIFO order by default', async () => {
await payload.jobs.queue({
workflow: 'inlineTaskTestDelayed',
input: {
message: 'task 1',
},
})
await new Promise((resolve) => setTimeout(resolve, 100))
await payload.jobs.queue({
workflow: 'inlineTaskTestDelayed',
input: {
message: 'task 2',
},
})
await payload.jobs.run({
sequential: true,
})
const allSimples = await payload.find({
collection: 'simple',
limit: 100,
sort: 'createdAt',
})
expect(allSimples.totalDocs).toBe(2)
expect(allSimples.docs?.[0]?.title).toBe('task 1')
expect(allSimples.docs?.[1]?.title).toBe('task 2')
})
it('ensure jobs can run LIFO if processingOrder is passed', async () => {
await payload.jobs.queue({
workflow: 'inlineTaskTestDelayed',
input: {
message: 'task 1',
},
})
await new Promise((resolve) => setTimeout(resolve, 100))
await payload.jobs.queue({
workflow: 'inlineTaskTestDelayed',
input: {
message: 'task 2',
},
})
await payload.jobs.run({
sequential: true,
processingOrder: '-createdAt',
})
const allSimples = await payload.find({
collection: 'simple',
limit: 100,
sort: 'createdAt',
})
expect(allSimples.totalDocs).toBe(2)
expect(allSimples.docs?.[0]?.title).toBe('task 2')
expect(allSimples.docs?.[1]?.title).toBe('task 1')
})
it('ensure job config processingOrder using queues object is respected', async () => {
await payload.jobs.queue({
workflow: 'inlineTaskTestDelayed',
queue: 'lifo',
input: {
message: 'task 1',
},
})
await new Promise((resolve) => setTimeout(resolve, 100))
await payload.jobs.queue({
workflow: 'inlineTaskTestDelayed',
queue: 'lifo',
input: {
message: 'task 2',
},
})
await payload.jobs.run({
sequential: true,
queue: 'lifo',
})
const allSimples = await payload.find({
collection: 'simple',
limit: 100,
sort: 'createdAt',
})
expect(allSimples.totalDocs).toBe(2)
expect(allSimples.docs?.[0]?.title).toBe('task 2')
expect(allSimples.docs?.[1]?.title).toBe('task 1')
})
it('can create new inline jobs', async () => {
await payload.jobs.queue({
workflow: 'inlineTaskTest',

View File

@@ -123,6 +123,7 @@ export interface Config {
workflowRetries2TasksRetriesUndefined: WorkflowWorkflowRetries2TasksRetriesUndefined;
workflowRetries2TasksRetries0: WorkflowWorkflowRetries2TasksRetries0;
inlineTaskTest: WorkflowInlineTaskTest;
inlineTaskTestDelayed: WorkflowInlineTaskTestDelayed;
externalWorkflow: WorkflowExternalWorkflow;
retriesBackoffTest: WorkflowRetriesBackoffTest;
subTask: WorkflowSubTask;
@@ -313,6 +314,7 @@ export interface PayloadJob {
| 'workflowRetries2TasksRetriesUndefined'
| 'workflowRetries2TasksRetries0'
| 'inlineTaskTest'
| 'inlineTaskTestDelayed'
| 'externalWorkflow'
| 'retriesBackoffTest'
| 'subTask'
@@ -722,6 +724,15 @@ export interface WorkflowInlineTaskTest {
message: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "WorkflowInlineTaskTestDelayed".
*/
export interface WorkflowInlineTaskTestDelayed {
input: {
message: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "WorkflowExternalWorkflow".

View File

@@ -0,0 +1,38 @@
import type { WorkflowConfig } from 'payload'
export const inlineTaskTestDelayedWorkflow: WorkflowConfig<'inlineTaskTestDelayed'> = {
slug: 'inlineTaskTestDelayed',
inputSchema: [
{
name: 'message',
type: 'text',
required: true,
},
],
handler: async ({ job, inlineTask }) => {
await inlineTask('1', {
task: async ({ input, req }) => {
// Wait 100ms
await new Promise((resolve) => setTimeout(resolve, 100))
const newSimple = await req.payload.create({
collection: 'simple',
req,
data: {
title: input.message,
},
})
await new Promise((resolve) => setTimeout(resolve, 100))
return {
output: {
simpleID: newSimple.id,
},
}
},
input: {
message: job.input.message,
},
})
},
}