fix: ensure scheduling by default only handles default queue, add allQueues config to autoRun (#13395)

By default, `payload.jobs.run` only runs jobs from the `default` queue
(since https://github.com/payloadcms/payload/pull/12799). It exposes an
`allQueues` property to run jobs from all queues.

For handling schedules (`payload.jobs.handleSchedules` and
`config.jobs.autoRun`), this behaves differently - jobs are run from all
queues by default, and no `allQueues` property exists.

This PR adds an `allQueues` property to scheduling, as well as changes
the default behavior to only handle schedules for the `default` queue.
That way, the behavior of running and scheduling jobs matches.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210982048221260
This commit is contained in:
Alessio Gravili
2025-08-12 08:55:17 -07:00
committed by GitHub
parent 995f96bc70
commit ad2564e5fa
9 changed files with 93 additions and 15 deletions

View File

@@ -14,6 +14,7 @@ export const baseIDField: TextField = {
defaultValue: () => new ObjectId().toHexString(), defaultValue: () => new ObjectId().toHexString(),
hooks: { hooks: {
beforeChange: [({ value }) => value || new ObjectId().toHexString()], beforeChange: [({ value }) => value || new ObjectId().toHexString()],
// ID field values for arrays and blocks need to be unique when duplicating, as on postgres they are stored on the same table as primary keys.
beforeDuplicate: [() => new ObjectId().toHexString()], beforeDuplicate: [() => new ObjectId().toHexString()],
}, },
label: 'ID', label: 'ID',

View File

@@ -63,7 +63,8 @@ export const promise = async <T>({
let fieldData = siblingDoc?.[field.name!] let fieldData = siblingDoc?.[field.name!]
const fieldIsLocalized = localization && fieldShouldBeLocalized({ field, parentIsLocalized }) const fieldIsLocalized = localization && fieldShouldBeLocalized({ field, parentIsLocalized })
// Run field beforeDuplicate hooks // Run field beforeDuplicate hooks.
// These hooks are responsible for resetting the `id` field values of array and block rows. See `baseIDField`.
if (Array.isArray(field.hooks?.beforeDuplicate)) { if (Array.isArray(field.hooks?.beforeDuplicate)) {
if (fieldIsLocalized) { if (fieldIsLocalized) {
const localeData: JsonObject = {} const localeData: JsonObject = {}

View File

@@ -873,6 +873,7 @@ export class BasePayload {
this.config.jobs.scheduling this.config.jobs.scheduling
) { ) {
await this.jobs.handleSchedules({ await this.jobs.handleSchedules({
allQueues: cronConfig.allQueues,
queue: cronConfig.queue, queue: cronConfig.queue,
}) })
} }
@@ -891,6 +892,7 @@ export class BasePayload {
} }
await this.jobs.run({ await this.jobs.run({
allQueues: cronConfig.allQueues,
limit: cronConfig.limit ?? DEFAULT_LIMIT, limit: cronConfig.limit ?? DEFAULT_LIMIT,
queue: cronConfig.queue, queue: cronConfig.queue,
silent: cronConfig.silent, silent: cronConfig.silent,

View File

@@ -7,6 +7,13 @@ import type { TaskConfig } from './taskTypes.js'
import type { WorkflowConfig } from './workflowTypes.js' import type { WorkflowConfig } from './workflowTypes.js'
export type AutorunCronConfig = { export type AutorunCronConfig = {
/**
* If you want to autoRUn jobs from all queues, set this to true.
* If you set this to true, the `queue` property will be ignored.
*
* @default false
*/
allQueues?: boolean
/** /**
* The cron schedule for the job. * The cron schedule for the job.
* @default '* * * * *' (every minute). * @default '* * * * *' (every minute).
@@ -43,6 +50,8 @@ export type AutorunCronConfig = {
limit?: number limit?: number
/** /**
* The queue name for the job. * The queue name for the job.
*
* @default 'default'
*/ */
queue?: string queue?: string
/** /**

View File

@@ -45,11 +45,18 @@ export const handleSchedulesJobsEndpoint: Endpoint = {
) )
} }
const { queue } = req.query as { const { allQueues, queue } = req.query as {
allQueues?: 'false' | 'true'
queue?: string queue?: string
} }
const { errored, queued, skipped } = await handleSchedules({ queue, req }) const runAllQueues = allQueues && !(typeof allQueues === 'string' && allQueues === 'false')
const { errored, queued, skipped } = await handleSchedules({
allQueues: runAllQueues,
queue,
req,
})
return Response.json( return Response.json(
{ {

View File

@@ -56,7 +56,7 @@ export const runJobsEndpoint: Endpoint = {
if (shouldHandleSchedules && jobsConfig.scheduling) { if (shouldHandleSchedules && jobsConfig.scheduling) {
// If should handle schedules and schedules are defined // If should handle schedules and schedules are defined
await req.payload.jobs.handleSchedules({ queue: runAllQueues ? undefined : queue, req }) await req.payload.jobs.handleSchedules({ allQueues: runAllQueues, queue, req })
} }
const runJobsArgs: RunJobsArgs = { const runJobsArgs: RunJobsArgs = {

View File

@@ -22,13 +22,20 @@ export type RunJobsSilent =
| boolean | boolean
export const getJobsLocalAPI = (payload: Payload) => ({ export const getJobsLocalAPI = (payload: Payload) => ({
handleSchedules: async (args?: { handleSchedules: async (args?: {
/**
* If you want to schedule jobs from all queues, set this to true.
* If you set this to true, the `queue` property will be ignored.
*
* @default false
*/
allQueues?: boolean
// By default, schedule all queues - only scheduling jobs scheduled to be added to the `default` queue would not make sense // By default, schedule all queues - only scheduling jobs scheduled to be added to the `default` queue would not make sense
// here, as you'd usually specify a different queue than `default` here, especially if this is used in combination with autorun. // here, as you'd usually specify a different queue than `default` here, especially if this is used in combination with autorun.
// The `queue` property for setting up schedules is required, and not optional. // The `queue` property for setting up schedules is required, and not optional.
/** /**
* If you want to only schedule jobs that are set to schedule in a specific queue, set this to the queue name. * If you want to only schedule jobs that are set to schedule in a specific queue, set this to the queue name.
* *
* @default all jobs for all queues will be scheduled. * @default jobs from the `default` queue will be executed.
*/ */
queue?: string queue?: string
req?: PayloadRequest req?: PayloadRequest
@@ -36,6 +43,7 @@ export const getJobsLocalAPI = (payload: Payload) => ({
const newReq: PayloadRequest = args?.req ?? (await createLocalReq({}, payload)) const newReq: PayloadRequest = args?.req ?? (await createLocalReq({}, payload))
return await handleSchedules({ return await handleSchedules({
allQueues: args?.allQueues,
queue: args?.queue, queue: args?.queue,
req: newReq, req: newReq,
}) })

View File

@@ -23,17 +23,26 @@ export type HandleSchedulesResult = {
* after they are scheduled * after they are scheduled
*/ */
export async function handleSchedules({ export async function handleSchedules({
queue, allQueues = false,
queue: _queue,
req, req,
}: { }: {
/**
* If you want to schedule jobs from all queues, set this to true.
* If you set this to true, the `queue` property will be ignored.
*
* @default false
*/
allQueues?: boolean
/** /**
* If you want to only schedule jobs that are set to schedule in a specific queue, set this to the queue name. * If you want to only schedule jobs that are set to schedule in a specific queue, set this to the queue name.
* *
* @default all jobs for all queues will be scheduled. * @default jobs from the `default` queue will be executed.
*/ */
queue?: string queue?: string
req: PayloadRequest req: PayloadRequest
}): Promise<HandleSchedulesResult> { }): Promise<HandleSchedulesResult> {
const queue = _queue ?? 'default'
const jobsConfig = req.payload.config.jobs const jobsConfig = req.payload.config.jobs
const queuesWithSchedules = getQueuesWithSchedules({ const queuesWithSchedules = getQueuesWithSchedules({
jobsConfig, jobsConfig,
@@ -53,7 +62,7 @@ export async function handleSchedules({
// Need to know when that particular job was last scheduled in that particular queue // Need to know when that particular job was last scheduled in that particular queue
for (const [queueName, { schedules }] of Object.entries(queuesWithSchedules)) { for (const [queueName, { schedules }] of Object.entries(queuesWithSchedules)) {
if (queue && queueName !== queue) { if (!allQueues && queueName !== queue) {
// If a queue is specified, only schedule jobs for that queue // If a queue is specified, only schedule jobs for that queue
continue continue
} }

View File

@@ -69,7 +69,7 @@ describe('Queues - scheduling, without automatic scheduling handling', () => {
it('can auto-schedule through local API and autorun jobs', async () => { it('can auto-schedule through local API and autorun jobs', async () => {
// Do not call payload.jobs.queue() - the `EverySecond` task should be scheduled here // Do not call payload.jobs.queue() - the `EverySecond` task should be scheduled here
await payload.jobs.handleSchedules() await payload.jobs.handleSchedules({ queue: 'autorunSecond' })
// Do not call payload.jobs.run{silent: true}) // Do not call payload.jobs.run{silent: true})
@@ -88,9 +88,50 @@ describe('Queues - scheduling, without automatic scheduling handling', () => {
expect(allSimples?.docs?.[0]?.title).toBe('This task runs every second') expect(allSimples?.docs?.[0]?.title).toBe('This task runs every second')
}) })
it('can auto-schedule through local API and autorun jobs when passing allQueues', async () => {
// Do not call payload.jobs.queue() - the `EverySecond` task should be scheduled here
await payload.jobs.handleSchedules({ queue: 'autorunSecond', allQueues: true })
// Do not call payload.jobs.run{silent: true})
await waitUntilAutorunIsDone({
payload,
queue: 'autorunSecond',
onlyScheduled: true,
})
const allSimples = await payload.find({
collection: 'simple',
limit: 100,
})
expect(allSimples.totalDocs).toBe(1)
expect(allSimples?.docs?.[0]?.title).toBe('This task runs every second')
})
it('should not auto-schedule through local API and autorun jobs when not passing queue and schedule is not set on the default queue', async () => {
// Do not call payload.jobs.queue() - the `EverySecond` task should be scheduled here
await payload.jobs.handleSchedules()
// Do not call payload.jobs.run{silent: true})
await waitUntilAutorunIsDone({
payload,
queue: 'autorunSecond',
onlyScheduled: true,
})
const allSimples = await payload.find({
collection: 'simple',
limit: 100,
})
expect(allSimples.totalDocs).toBe(0)
})
it('can auto-schedule through handleSchedules REST API and autorun jobs', async () => { it('can auto-schedule through handleSchedules REST API and autorun jobs', async () => {
// Do not call payload.jobs.queue() - the `EverySecond` task should be scheduled here // Do not call payload.jobs.queue() - the `EverySecond` task should be scheduled here
await restClient.GET('/payload-jobs/handle-schedules', { await restClient.GET('/payload-jobs/handle-schedules?queue=autorunSecond', {
headers: { headers: {
Authorization: `JWT ${token}`, Authorization: `JWT ${token}`,
}, },
@@ -115,7 +156,7 @@ describe('Queues - scheduling, without automatic scheduling handling', () => {
it('can auto-schedule through run REST API and autorun jobs', async () => { it('can auto-schedule through run REST API and autorun jobs', async () => {
// Do not call payload.jobs.queue() - the `EverySecond` task should be scheduled here // Do not call payload.jobs.queue() - the `EverySecond` task should be scheduled here
await restClient.GET('/payload-jobs/run?silent=true', { await restClient.GET('/payload-jobs/run?silent=true&allQueues=true', {
headers: { headers: {
Authorization: `JWT ${token}`, Authorization: `JWT ${token}`,
}, },
@@ -161,7 +202,7 @@ describe('Queues - scheduling, without automatic scheduling handling', () => {
it('ensure scheduler does not schedule more jobs than needed if executed sequentially', async () => { it('ensure scheduler does not schedule more jobs than needed if executed sequentially', async () => {
await withoutAutoRun(async () => { await withoutAutoRun(async () => {
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
await payload.jobs.handleSchedules() await payload.jobs.handleSchedules({ allQueues: true })
} }
}) })
@@ -192,7 +233,7 @@ describe('Queues - scheduling, without automatic scheduling handling', () => {
}) })
} }
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
await payload.jobs.handleSchedules() await payload.jobs.handleSchedules({ allQueues: true })
} }
}) })
@@ -271,8 +312,8 @@ describe('Queues - scheduling, without automatic scheduling handling', () => {
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
await withoutAutoRun(async () => { await withoutAutoRun(async () => {
// Call it twice to test that it only schedules one // Call it twice to test that it only schedules one
await payload.jobs.handleSchedules() await payload.jobs.handleSchedules({ allQueues: true })
await payload.jobs.handleSchedules() await payload.jobs.handleSchedules({ allQueues: true })
}) })
// Advance time to satisfy the waitUntil of newly scheduled jobs // Advance time to satisfy the waitUntil of newly scheduled jobs
timeTravel(20) timeTravel(20)