feat: allow running sub-tasks from tasks (#10373)
Task handlers now receive `inlineTask` as an arg, which can be used to
run inline sub-tasks. In the task log, those inline tasks will have a
`parent` property that points to the parent task.
Example:
```ts
{
slug: 'subTask',
inputSchema: [
{
name: 'message',
type: 'text',
required: true,
},
],
handler: async ({ job, inlineTask }) => {
await inlineTask('create two docs', {
task: async ({ input, inlineTask }) => {
const { newSimple } = await inlineTask('create doc 1', {
task: async ({ req }) => {
const newSimple = await req.payload.create({
collection: 'simple',
req,
data: {
title: input.message,
},
})
return {
output: {
newSimple,
},
}
},
})
const { newSimple2 } = await inlineTask('create doc 2', {
task: async ({ req }) => {
const newSimple2 = await req.payload.create({
collection: 'simple',
req,
data: {
title: input.message,
},
})
return {
output: {
newSimple2,
},
}
},
})
return {
output: {
simpleID1: newSimple.id,
simpleID2: newSimple2.id,
},
}
},
input: {
message: job.input.message,
},
})
},
} as WorkflowConfig<'subTask'>
```
Job log example:
```ts
[
{
executedAt: '2025-01-06T03:55:44.682Z',
completedAt: '2025-01-06T03:55:44.684Z',
taskSlug: 'inline',
taskID: 'create doc 1',
output: { newSimple: [Object] },
parent: { taskSlug: 'inline', taskID: 'create two docs' }, // <= New
state: 'succeeded',
id: '677b5440ba35d345d1214d1b'
},
{
executedAt: '2025-01-06T03:55:44.690Z',
completedAt: '2025-01-06T03:55:44.692Z',
taskSlug: 'inline',
taskID: 'create doc 2',
output: { newSimple2: [Object] },
parent: { taskSlug: 'inline', taskID: 'create two docs' }, // <= New
state: 'succeeded',
id: '677b5440ba35d345d1214d1c'
},
{
executedAt: '2025-01-06T03:55:44.681Z',
completedAt: '2025-01-06T03:55:44.697Z',
taskSlug: 'inline',
taskID: 'create two docs',
input: { message: 'hello!' },
output: {
simpleID1: '677b54401e34772cc63c8693',
simpleID2: '677b54401e34772cc63c8697'
},
parent: {},
state: 'succeeded',
id: '677b5440ba35d345d1214d1d'
}
]
```
This commit is contained in:
@@ -203,3 +203,49 @@ export default buildConfig({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Nested tasks
|
||||||
|
|
||||||
|
You can run sub-tasks within an existing task, by using the `tasks` or `ìnlineTask` arguments passed to the task `handler` function:
|
||||||
|
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default buildConfig({
|
||||||
|
// ...
|
||||||
|
jobs: {
|
||||||
|
// It is recommended to set `addParentToTaskLog` to `true` when using nested tasks, so that the parent task is included in the task log
|
||||||
|
// This allows for better observability and debugging of the task execution
|
||||||
|
addParentToTaskLog: true,
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
slug: 'parentTask',
|
||||||
|
inputSchema: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
handler: async ({ input, req, tasks, inlineTask }) => {
|
||||||
|
|
||||||
|
await inlineTask('Sub Task 1', {
|
||||||
|
task: () => {
|
||||||
|
// Do something
|
||||||
|
return {
|
||||||
|
output: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await tasks.CreateSimple('Sub Task 2', {
|
||||||
|
input: { message: 'hello' },
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
output: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as TaskConfig<'parentTask'>,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|||||||
@@ -1304,6 +1304,7 @@ export type { JobsConfig, RunJobAccess, RunJobAccessArgs } from './queues/config
|
|||||||
export type {
|
export type {
|
||||||
RunInlineTaskFunction,
|
RunInlineTaskFunction,
|
||||||
RunTaskFunction,
|
RunTaskFunction,
|
||||||
|
RunTaskFunctions,
|
||||||
TaskConfig,
|
TaskConfig,
|
||||||
TaskHandler,
|
TaskHandler,
|
||||||
TaskHandlerArgs,
|
TaskHandlerArgs,
|
||||||
|
|||||||
@@ -30,6 +30,70 @@ export const getDefaultJobsCollection: (config: Config) => CollectionConfig | nu
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logFields: Field[] = [
|
||||||
|
{
|
||||||
|
name: 'executedAt',
|
||||||
|
type: 'date',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'completedAt',
|
||||||
|
type: 'date',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'taskSlug',
|
||||||
|
type: 'select',
|
||||||
|
options: [...taskSlugs],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'taskID',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'input',
|
||||||
|
type: 'json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'output',
|
||||||
|
type: 'json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'state',
|
||||||
|
type: 'radio',
|
||||||
|
options: ['failed', 'succeeded'],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'error',
|
||||||
|
type: 'json',
|
||||||
|
admin: {
|
||||||
|
condition: (_, data) => data.state === 'failed',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (config?.jobs?.addParentToTaskLog) {
|
||||||
|
logFields.push({
|
||||||
|
name: 'parent',
|
||||||
|
type: 'group',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'taskSlug',
|
||||||
|
type: 'select',
|
||||||
|
options: [...taskSlugs],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'taskID',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const jobsCollection: CollectionConfig = {
|
const jobsCollection: CollectionConfig = {
|
||||||
slug: 'payload-jobs',
|
slug: 'payload-jobs',
|
||||||
admin: {
|
admin: {
|
||||||
@@ -89,51 +153,7 @@ export const getDefaultJobsCollection: (config: Config) => CollectionConfig | nu
|
|||||||
admin: {
|
admin: {
|
||||||
description: 'Task execution log',
|
description: 'Task execution log',
|
||||||
},
|
},
|
||||||
fields: [
|
fields: logFields,
|
||||||
{
|
|
||||||
name: 'executedAt',
|
|
||||||
type: 'date',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'completedAt',
|
|
||||||
type: 'date',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'taskSlug',
|
|
||||||
type: 'select',
|
|
||||||
options: [...taskSlugs],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'taskID',
|
|
||||||
type: 'text',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'input',
|
|
||||||
type: 'json',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'output',
|
|
||||||
type: 'json',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'state',
|
|
||||||
type: 'radio',
|
|
||||||
options: ['failed', 'succeeded'],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'error',
|
|
||||||
type: 'json',
|
|
||||||
admin: {
|
|
||||||
condition: (_, data) => data.state === 'failed',
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
@@ -204,5 +224,6 @@ export const getDefaultJobsCollection: (config: Config) => CollectionConfig | nu
|
|||||||
},
|
},
|
||||||
lockDocuments: false,
|
lockDocuments: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
return jobsCollection
|
return jobsCollection
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ export type JobsConfig = {
|
|||||||
*/
|
*/
|
||||||
run?: RunJobAccess
|
run?: RunJobAccess
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Adds information about the parent job to the task log. This is useful for debugging and tracking the flow of tasks.
|
||||||
|
*
|
||||||
|
* In 4.0, this will default to `true`.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
addParentToTaskLog?: boolean
|
||||||
/**
|
/**
|
||||||
* Determine whether or not to delete a job after it has successfully completed.
|
* Determine whether or not to delete a job after it has successfully completed.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ export type TaskHandlerArgs<
|
|||||||
TTaskSlugOrInputOutput extends keyof TypedJobs['tasks'] | TaskInputOutput,
|
TTaskSlugOrInputOutput extends keyof TypedJobs['tasks'] | TaskInputOutput,
|
||||||
TWorkflowSlug extends keyof TypedJobs['workflows'] = string,
|
TWorkflowSlug extends keyof TypedJobs['workflows'] = string,
|
||||||
> = {
|
> = {
|
||||||
|
/**
|
||||||
|
* Use this function to run a sub-task from within another task.
|
||||||
|
*/
|
||||||
|
inlineTask: RunInlineTaskFunction
|
||||||
input: TTaskSlugOrInputOutput extends keyof TypedJobs['tasks']
|
input: TTaskSlugOrInputOutput extends keyof TypedJobs['tasks']
|
||||||
? TypedJobs['tasks'][TTaskSlugOrInputOutput]['input']
|
? TypedJobs['tasks'][TTaskSlugOrInputOutput]['input']
|
||||||
: TTaskSlugOrInputOutput extends TaskInputOutput // Check if it's actually TaskInputOutput type
|
: TTaskSlugOrInputOutput extends TaskInputOutput // Check if it's actually TaskInputOutput type
|
||||||
@@ -27,6 +31,7 @@ export type TaskHandlerArgs<
|
|||||||
: never
|
: never
|
||||||
job: RunningJob<TWorkflowSlug>
|
job: RunningJob<TWorkflowSlug>
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
|
tasks: RunTaskFunctions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +97,13 @@ export type RunInlineTaskFunction = <TTaskInput extends object, TTaskOutput exte
|
|||||||
*/
|
*/
|
||||||
retries?: number | RetryConfig | undefined
|
retries?: number | RetryConfig | undefined
|
||||||
// This is the same as TaskHandler, but typed out explicitly in order to improve type inference
|
// This is the same as TaskHandler, but typed out explicitly in order to improve type inference
|
||||||
task: (args: { input: TTaskInput; job: RunningJob<any>; req: PayloadRequest }) =>
|
task: (args: {
|
||||||
|
inlineTask: RunInlineTaskFunction
|
||||||
|
input: TTaskInput
|
||||||
|
job: RunningJob<any>
|
||||||
|
req: PayloadRequest
|
||||||
|
tasks: RunTaskFunctions
|
||||||
|
}) =>
|
||||||
| {
|
| {
|
||||||
output: TTaskOutput
|
output: TTaskOutput
|
||||||
state?: 'failed' | 'succeeded'
|
state?: 'failed' | 'succeeded'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Field } from '../../../fields/config/types.js'
|
import type { Field } from '../../../fields/config/types.js'
|
||||||
import type { PayloadRequest, StringKeyOf, TypedCollection, TypedJobs } from '../../../index.js'
|
import type { PayloadRequest, StringKeyOf, TypedCollection, TypedJobs } from '../../../index.js'
|
||||||
|
import type { TaskParent } from '../../operations/runJobs/runJob/getRunTaskFunction.js'
|
||||||
import type {
|
import type {
|
||||||
RetryConfig,
|
RetryConfig,
|
||||||
RunInlineTaskFunction,
|
RunInlineTaskFunction,
|
||||||
@@ -18,8 +19,12 @@ export type JobLog = {
|
|||||||
* ID added by the array field when the log is saved in the database
|
* ID added by the array field when the log is saved in the database
|
||||||
*/
|
*/
|
||||||
id?: string
|
id?: string
|
||||||
input?: any
|
input?: Record<string, any>
|
||||||
output?: any
|
output?: Record<string, any>
|
||||||
|
/**
|
||||||
|
* Sub-tasks (tasks that are run within a task) will have a parent task ID
|
||||||
|
*/
|
||||||
|
parent?: TaskParent
|
||||||
state: 'failed' | 'succeeded'
|
state: 'failed' | 'succeeded'
|
||||||
taskID: string
|
taskID: string
|
||||||
taskSlug: string
|
taskSlug: string
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export async function handleTaskFailed({
|
|||||||
job,
|
job,
|
||||||
maxRetries,
|
maxRetries,
|
||||||
output,
|
output,
|
||||||
|
parent,
|
||||||
req,
|
req,
|
||||||
retriesConfig,
|
retriesConfig,
|
||||||
runnerOutput,
|
runnerOutput,
|
||||||
@@ -60,6 +61,7 @@ export async function handleTaskFailed({
|
|||||||
job: BaseJob
|
job: BaseJob
|
||||||
maxRetries: number
|
maxRetries: number
|
||||||
output: object
|
output: object
|
||||||
|
parent?: TaskParent
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
retriesConfig: number | RetryConfig
|
retriesConfig: number | RetryConfig
|
||||||
runnerOutput?: TaskHandlerResult<string>
|
runnerOutput?: TaskHandlerResult<string>
|
||||||
@@ -93,6 +95,7 @@ export async function handleTaskFailed({
|
|||||||
executedAt: executedAt.toISOString(),
|
executedAt: executedAt.toISOString(),
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
|
parent: req?.payload?.config?.jobs?.addParentToTaskLog ? parent : undefined,
|
||||||
state: 'failed',
|
state: 'failed',
|
||||||
taskID,
|
taskID,
|
||||||
taskSlug,
|
taskSlug,
|
||||||
@@ -142,6 +145,11 @@ export async function handleTaskFailed({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TaskParent = {
|
||||||
|
taskID: string
|
||||||
|
taskSlug: string
|
||||||
|
}
|
||||||
|
|
||||||
export const getRunTaskFunction = <TIsInline extends boolean>(
|
export const getRunTaskFunction = <TIsInline extends boolean>(
|
||||||
state: RunTaskFunctionState,
|
state: RunTaskFunctionState,
|
||||||
job: BaseJob,
|
job: BaseJob,
|
||||||
@@ -149,6 +157,7 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
|
|||||||
req: PayloadRequest,
|
req: PayloadRequest,
|
||||||
isInline: TIsInline,
|
isInline: TIsInline,
|
||||||
updateJob: UpdateJobFunction,
|
updateJob: UpdateJobFunction,
|
||||||
|
parent?: TaskParent,
|
||||||
): TIsInline extends true ? RunInlineTaskFunction : RunTaskFunctions => {
|
): TIsInline extends true ? RunInlineTaskFunction : RunTaskFunctions => {
|
||||||
const runTask: <TTaskSlug extends string>(
|
const runTask: <TTaskSlug extends string>(
|
||||||
taskSlug: TTaskSlug,
|
taskSlug: TTaskSlug,
|
||||||
@@ -240,6 +249,7 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
|
|||||||
completedAt: new Date().toISOString(),
|
completedAt: new Date().toISOString(),
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
executedAt: executedAt.toISOString(),
|
executedAt: executedAt.toISOString(),
|
||||||
|
parent: req?.payload?.config?.jobs?.addParentToTaskLog ? parent : undefined,
|
||||||
state: 'failed',
|
state: 'failed',
|
||||||
taskID,
|
taskID,
|
||||||
taskSlug,
|
taskSlug,
|
||||||
@@ -269,9 +279,17 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const runnerOutput = await runner({
|
const runnerOutput = await runner({
|
||||||
|
inlineTask: getRunTaskFunction(state, job, workflowConfig, req, true, updateJob, {
|
||||||
|
taskID,
|
||||||
|
taskSlug,
|
||||||
|
}),
|
||||||
input,
|
input,
|
||||||
job: job as unknown as RunningJob<WorkflowTypes>, // TODO: Type this better
|
job: job as unknown as RunningJob<WorkflowTypes>, // TODO: Type this better
|
||||||
req,
|
req,
|
||||||
|
tasks: getRunTaskFunction(state, job, workflowConfig, req, false, updateJob, {
|
||||||
|
taskID,
|
||||||
|
taskSlug,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (runnerOutput.state === 'failed') {
|
if (runnerOutput.state === 'failed') {
|
||||||
@@ -281,6 +299,7 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
|
|||||||
job,
|
job,
|
||||||
maxRetries,
|
maxRetries,
|
||||||
output,
|
output,
|
||||||
|
parent,
|
||||||
req,
|
req,
|
||||||
retriesConfig: finalRetriesConfig,
|
retriesConfig: finalRetriesConfig,
|
||||||
runnerOutput,
|
runnerOutput,
|
||||||
@@ -303,6 +322,7 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
|
|||||||
job,
|
job,
|
||||||
maxRetries,
|
maxRetries,
|
||||||
output,
|
output,
|
||||||
|
parent,
|
||||||
req,
|
req,
|
||||||
retriesConfig: finalRetriesConfig,
|
retriesConfig: finalRetriesConfig,
|
||||||
state,
|
state,
|
||||||
@@ -327,6 +347,7 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
|
|||||||
executedAt: executedAt.toISOString(),
|
executedAt: executedAt.toISOString(),
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
|
parent: req?.payload?.config?.jobs?.addParentToTaskLog ? parent : undefined,
|
||||||
state: 'succeeded',
|
state: 'succeeded',
|
||||||
taskID,
|
taskID,
|
||||||
taskSlug,
|
taskSlug,
|
||||||
|
|||||||
@@ -846,6 +846,142 @@ export default buildConfigWithDefaults({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
} as WorkflowConfig<'retriesBackoffTest'>,
|
} as WorkflowConfig<'retriesBackoffTest'>,
|
||||||
|
{
|
||||||
|
slug: 'subTask',
|
||||||
|
inputSchema: [
|
||||||
|
{
|
||||||
|
name: 'message',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
handler: async ({ job, inlineTask }) => {
|
||||||
|
await inlineTask('create two docs', {
|
||||||
|
task: async ({ input, inlineTask }) => {
|
||||||
|
const { newSimple } = await inlineTask('create doc 1', {
|
||||||
|
task: async ({ req }) => {
|
||||||
|
const newSimple = await req.payload.create({
|
||||||
|
collection: 'simple',
|
||||||
|
req,
|
||||||
|
data: {
|
||||||
|
title: input.message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
output: {
|
||||||
|
newSimple,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { newSimple2 } = await inlineTask('create doc 2', {
|
||||||
|
task: async ({ req }) => {
|
||||||
|
const newSimple2 = await req.payload.create({
|
||||||
|
collection: 'simple',
|
||||||
|
req,
|
||||||
|
data: {
|
||||||
|
title: input.message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
output: {
|
||||||
|
newSimple2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
output: {
|
||||||
|
simpleID1: newSimple.id,
|
||||||
|
simpleID2: newSimple2.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
message: job.input.message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} as WorkflowConfig<'subTask'>,
|
||||||
|
{
|
||||||
|
slug: 'subTaskFails',
|
||||||
|
inputSchema: [
|
||||||
|
{
|
||||||
|
name: 'message',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
retries: 3,
|
||||||
|
handler: async ({ job, inlineTask }) => {
|
||||||
|
await inlineTask('create two docs', {
|
||||||
|
task: async ({ input, inlineTask }) => {
|
||||||
|
const { newSimple } = await inlineTask('create doc 1 - succeeds', {
|
||||||
|
task: async ({ req }) => {
|
||||||
|
const newSimple = await req.payload.create({
|
||||||
|
collection: 'simple',
|
||||||
|
req,
|
||||||
|
data: {
|
||||||
|
title: input.message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await req.payload.update({
|
||||||
|
collection: 'payload-jobs',
|
||||||
|
data: {
|
||||||
|
input: {
|
||||||
|
...job.input,
|
||||||
|
amountTask1Retried:
|
||||||
|
// @ts-expect-error amountRetried is new arbitrary data and not in the type
|
||||||
|
job.input.amountTask1Retried !== undefined
|
||||||
|
? // @ts-expect-error
|
||||||
|
job.input.amountTask1Retried + 1
|
||||||
|
: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: job.id,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
output: {
|
||||||
|
newSimple,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await inlineTask('create doc 2 - fails', {
|
||||||
|
task: async ({ req }) => {
|
||||||
|
await req.payload.update({
|
||||||
|
collection: 'payload-jobs',
|
||||||
|
data: {
|
||||||
|
input: {
|
||||||
|
...job.input,
|
||||||
|
amountTask2Retried:
|
||||||
|
// @ts-expect-error amountRetried is new arbitrary data and not in the type
|
||||||
|
job.input.amountTask2Retried !== undefined
|
||||||
|
? // @ts-expect-error
|
||||||
|
job.input.amountTask2Retried + 1
|
||||||
|
: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: job.id,
|
||||||
|
})
|
||||||
|
throw new Error('Failed on purpose')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
output: {
|
||||||
|
simpleID1: newSimple.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
message: job.input.message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} as WorkflowConfig<'subTaskFails'>,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
editor: lexicalEditor(),
|
editor: lexicalEditor(),
|
||||||
|
|||||||
@@ -1051,4 +1051,80 @@ describe('Queues', () => {
|
|||||||
expect(allCompletedJobs.totalDocs).toBe(1)
|
expect(allCompletedJobs.totalDocs).toBe(1)
|
||||||
expect((allCompletedJobs.docs[0].input as any).message).toBe('from single task 2')
|
expect((allCompletedJobs.docs[0].input as any).message).toBe('from single task 2')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('can run sub-tasks', async () => {
|
||||||
|
payload.config.jobs.deleteJobOnComplete = false
|
||||||
|
const job = await payload.jobs.queue({
|
||||||
|
workflow: 'subTask',
|
||||||
|
input: {
|
||||||
|
message: 'hello!',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await payload.jobs.run()
|
||||||
|
|
||||||
|
const allSimples = await payload.find({
|
||||||
|
collection: 'simple',
|
||||||
|
limit: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(allSimples.totalDocs).toBe(2)
|
||||||
|
expect(allSimples.docs[0].title).toBe('hello!')
|
||||||
|
expect(allSimples.docs[1].title).toBe('hello!')
|
||||||
|
|
||||||
|
const jobAfterRun = await payload.findByID({
|
||||||
|
collection: 'payload-jobs',
|
||||||
|
id: job.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(jobAfterRun.log[0].taskID).toBe('create doc 1')
|
||||||
|
//expect(jobAfterRun.log[0].parent.taskID).toBe('create two docs')
|
||||||
|
// jobAfterRun.log[0].parent should not exist
|
||||||
|
expect(jobAfterRun.log[0].parent).toBeUndefined()
|
||||||
|
|
||||||
|
expect(jobAfterRun.log[1].taskID).toBe('create doc 2')
|
||||||
|
//expect(jobAfterRun.log[1].parent.taskID).toBe('create two docs')
|
||||||
|
expect(jobAfterRun.log[1].parent).toBeUndefined()
|
||||||
|
|
||||||
|
expect(jobAfterRun.log[2].taskID).toBe('create two docs')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ensure successful sub-tasks are not retried', async () => {
|
||||||
|
payload.config.jobs.deleteJobOnComplete = false
|
||||||
|
|
||||||
|
const job = await payload.jobs.queue({
|
||||||
|
workflow: 'subTaskFails',
|
||||||
|
input: {
|
||||||
|
message: 'hello!',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let hasJobsRemaining = true
|
||||||
|
|
||||||
|
while (hasJobsRemaining) {
|
||||||
|
const response = await payload.jobs.run()
|
||||||
|
|
||||||
|
if (response.noJobsRemaining) {
|
||||||
|
hasJobsRemaining = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allSimples = await payload.find({
|
||||||
|
collection: 'simple',
|
||||||
|
limit: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(allSimples.totalDocs).toBe(1)
|
||||||
|
expect(allSimples.docs[0].title).toBe('hello!')
|
||||||
|
|
||||||
|
const jobAfterRun = await payload.findByID({
|
||||||
|
collection: 'payload-jobs',
|
||||||
|
id: job.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(jobAfterRun.input.amountTask2Retried).toBe(3)
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(jobAfterRun.input.amountTask1Retried).toBe(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ export interface Config {
|
|||||||
inlineTaskTest: WorkflowInlineTaskTest;
|
inlineTaskTest: WorkflowInlineTaskTest;
|
||||||
externalWorkflow: WorkflowExternalWorkflow;
|
externalWorkflow: WorkflowExternalWorkflow;
|
||||||
retriesBackoffTest: WorkflowRetriesBackoffTest;
|
retriesBackoffTest: WorkflowRetriesBackoffTest;
|
||||||
|
subTask: WorkflowSubTask;
|
||||||
|
subTaskFails: WorkflowSubTaskFails;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -221,6 +223,21 @@ export interface PayloadJob {
|
|||||||
| number
|
| number
|
||||||
| boolean
|
| boolean
|
||||||
| null;
|
| null;
|
||||||
|
parent?: {
|
||||||
|
taskSlug?:
|
||||||
|
| (
|
||||||
|
| 'inline'
|
||||||
|
| 'UpdatePost'
|
||||||
|
| 'UpdatePostStep2'
|
||||||
|
| 'CreateSimple'
|
||||||
|
| 'CreateSimpleRetriesUndefined'
|
||||||
|
| 'CreateSimpleRetries0'
|
||||||
|
| 'CreateSimpleWithDuplicateMessage'
|
||||||
|
| 'ExternalTask'
|
||||||
|
)
|
||||||
|
| null;
|
||||||
|
taskID?: string | null;
|
||||||
|
};
|
||||||
state: 'failed' | 'succeeded';
|
state: 'failed' | 'succeeded';
|
||||||
error?:
|
error?:
|
||||||
| {
|
| {
|
||||||
@@ -249,6 +266,8 @@ export interface PayloadJob {
|
|||||||
| 'inlineTaskTest'
|
| 'inlineTaskTest'
|
||||||
| 'externalWorkflow'
|
| 'externalWorkflow'
|
||||||
| 'retriesBackoffTest'
|
| 'retriesBackoffTest'
|
||||||
|
| 'subTask'
|
||||||
|
| 'subTaskFails'
|
||||||
)
|
)
|
||||||
| null;
|
| null;
|
||||||
taskSlug?:
|
taskSlug?:
|
||||||
@@ -390,6 +409,12 @@ export interface PayloadJobsSelect<T extends boolean = true> {
|
|||||||
taskID?: T;
|
taskID?: T;
|
||||||
input?: T;
|
input?: T;
|
||||||
output?: T;
|
output?: T;
|
||||||
|
parent?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
taskSlug?: T;
|
||||||
|
taskID?: T;
|
||||||
|
};
|
||||||
state?: T;
|
state?: T;
|
||||||
error?: T;
|
error?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
@@ -641,6 +666,24 @@ export interface WorkflowRetriesBackoffTest {
|
|||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "WorkflowSubTask".
|
||||||
|
*/
|
||||||
|
export interface WorkflowSubTask {
|
||||||
|
input: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "WorkflowSubTaskFails".
|
||||||
|
*/
|
||||||
|
export interface WorkflowSubTaskFails {
|
||||||
|
input: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "auth".
|
* via the `definition` "auth".
|
||||||
|
|||||||
Reference in New Issue
Block a user