Skip to content

Commit 194a8c1

Browse files
authored
feat: add shouldRestore config to job queue tasks (#10059)
By default, if a task has passed previously and a workflow is re-run, the task will not be re-run. Instead, the output from the previous task run will be returned. This is to prevent unnecessary re-runs of tasks that have already passed. This PR allows you to configure this behavior through the `retries.shouldRestore` property. This property accepts a boolean or a function for more complex restore behaviors.
1 parent 1446fe4 commit 194a8c1

File tree

3 files changed

+133
-21
lines changed

3 files changed

+133
-21
lines changed

docs/jobs-queue/tasks.mdx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,65 @@ export const createPostHandler: TaskHandler<'createPost'> = async ({ input, job,
141141
}
142142
}
143143
```
144+
145+
### Configuring task restoration
146+
147+
By default, if a task has passed previously and a workflow is re-run, the task will not be re-run. Instead, the output from the previous task run will be returned. This is to prevent unnecessary re-runs of tasks that have already passed.
148+
149+
You can configure this behavior through the `retries.shouldRestore` property. This property accepts a boolean or a function.
150+
151+
If `shouldRestore` is set to true, the task will only be re-run if it previously failed. This is the default behavior.
152+
153+
If `shouldRestore` this is set to false, the task will be re-run even if it previously succeeded, ignoring the maximum number of retries.
154+
155+
If `shouldRestore` is a function, the return value of the function will determine whether the task should be re-run. This can be used for more complex restore logic, e.g you may want to re-run a task up to X amount of times and then restore it for consecutive runs, or only re-run a task if the input has changed.
156+
157+
Example:
158+
159+
```ts
160+
export default buildConfig({
161+
// ...
162+
jobs: {
163+
tasks: [
164+
{
165+
slug: 'myTask',
166+
retries: {
167+
shouldRestore: false,
168+
}
169+
// ...
170+
} as TaskConfig<'myTask'>,
171+
]
172+
}
173+
})
174+
```
175+
176+
Example - determine whether a task should be restored based on the input data:
177+
178+
```ts
179+
export default buildConfig({
180+
// ...
181+
jobs: {
182+
tasks: [
183+
{
184+
slug: 'myTask',
185+
inputSchema: [
186+
{
187+
name: 'someDate',
188+
type: 'date',
189+
required: true,
190+
},
191+
],
192+
retries: {
193+
shouldRestore: ({ input }) => {
194+
if(new Date(input.someDate) > new Date()) {
195+
return false
196+
}
197+
return true
198+
},
199+
}
200+
// ...
201+
} as TaskConfig<'myTask'>,
202+
]
203+
}
204+
})
205+
```

packages/payload/src/queues/config/types/taskTypes.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Field, PayloadRequest, StringKeyOf, TypedJobs } from '../../../index.js'
2-
import type { RunningJob, RunningJobSimple } from './workflowTypes.js'
2+
import type { BaseJob, RunningJob, RunningJobSimple, SingleTaskStatus } from './workflowTypes.js'
33

44
export type TaskInputOutput = {
55
input: object
@@ -101,8 +101,23 @@ export type RunInlineTaskFunction = <TTaskInput extends object, TTaskOutput exte
101101
},
102102
) => Promise<TTaskOutput>
103103

104+
export type ShouldRestoreFn = (args: {
105+
/**
106+
* Input data passed to the task
107+
*/
108+
input: object
109+
job: BaseJob
110+
req: PayloadRequest
111+
taskStatus: SingleTaskStatus<string>
112+
}) => boolean | Promise<boolean>
113+
104114
export type RetryConfig = {
105-
attempts: number
115+
/**
116+
* This controls how many times the task should be retried if it fails.
117+
*
118+
* @default undefined - attempts are either inherited from the workflow retry config or set to 0.
119+
*/
120+
attempts?: number
106121
/**
107122
* The backoff strategy to use when retrying the task. This determines how long to wait before retrying the task.
108123
*
@@ -137,6 +152,19 @@ export type RetryConfig = {
137152
*/
138153
type: 'exponential' | 'fixed'
139154
}
155+
/**
156+
* This controls whether the task output should be restored if the task previously succeeded and the workflow is being retried.
157+
*
158+
* If this is set to false, the task will be re-run even if it previously succeeded, ignoring the maximum number of retries.
159+
*
160+
* If this is set to true, the task will only be re-run if it previously failed.
161+
*
162+
* If this is a function, the return value of the function will determine whether the task should be re-run. This can be used for more complex restore logic,
163+
* e.g you may want to re-run a task up until a certain point and then restore it, or only re-run a task if the input has changed.
164+
*
165+
* @default true - the task output will be restored if the task previously succeeded.
166+
*/
167+
shouldRestore?: boolean | ShouldRestoreFn
140168
}
141169

142170
export type TaskConfig<

packages/payload/src/queues/operations/runJobs/runJob/getRunTaskFunction.ts

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -170,39 +170,47 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
170170
inlineRunner = task
171171
}
172172

173-
let retriesConfig: number | RetryConfig = retries
174173
let taskConfig: TaskConfig<string>
175174
if (!isInline) {
176175
taskConfig = req.payload.config.jobs.tasks.find((t) => t.slug === taskSlug)
177-
if (!retriesConfig) {
178-
retriesConfig = taskConfig.retries
179-
}
180176

181177
if (!taskConfig) {
182178
throw new Error(`Task ${taskSlug} not found in workflow ${job.workflowSlug}`)
183179
}
184180
}
185-
let maxRetries: number =
186-
typeof retriesConfig === 'object' ? retriesConfig?.attempts : retriesConfig
187181

188-
if (maxRetries === undefined || maxRetries === null) {
189-
// Inherit retries from workflow config, if they are undefined and the workflow config has retries configured
190-
if (workflowConfig.retries !== undefined && workflowConfig.retries !== null) {
191-
maxRetries =
192-
typeof workflowConfig.retries === 'object'
193-
? workflowConfig.retries.attempts
194-
: workflowConfig.retries
195-
} else {
196-
maxRetries = 0
197-
}
182+
const retriesConfigFromPropsNormalized =
183+
retries == undefined || retries == null
184+
? {}
185+
: typeof retries === 'number'
186+
? { attempts: retries }
187+
: retries
188+
const retriesConfigFromTaskConfigNormalized = taskConfig
189+
? typeof taskConfig.retries === 'number'
190+
? { attempts: taskConfig.retries }
191+
: taskConfig.retries
192+
: {}
193+
194+
const finalRetriesConfig: RetryConfig = {
195+
...retriesConfigFromTaskConfigNormalized,
196+
...retriesConfigFromPropsNormalized, // Retry config from props takes precedence
198197
}
199198

200199
const taskStatus: null | SingleTaskStatus<string> = job?.taskStatus?.[taskSlug]
201200
? job.taskStatus[taskSlug][taskID]
202201
: null
203202

203+
// Handle restoration of task if it succeeded in a previous run
204204
if (taskStatus && taskStatus.complete === true) {
205-
return taskStatus.output
205+
let shouldRestore = true
206+
if (finalRetriesConfig?.shouldRestore === false) {
207+
shouldRestore = false
208+
} else if (typeof finalRetriesConfig?.shouldRestore === 'function') {
209+
shouldRestore = await finalRetriesConfig.shouldRestore({ input, job, req, taskStatus })
210+
}
211+
if (shouldRestore) {
212+
return taskStatus.output
213+
}
206214
}
207215

208216
let runner: TaskHandler<TaskType>
@@ -245,6 +253,20 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
245253

246254
let output: object = {}
247255

256+
let maxRetries: number | undefined = finalRetriesConfig?.attempts
257+
258+
if (maxRetries === undefined || maxRetries === null) {
259+
// Inherit retries from workflow config, if they are undefined and the workflow config has retries configured
260+
if (workflowConfig.retries !== undefined && workflowConfig.retries !== null) {
261+
maxRetries =
262+
typeof workflowConfig.retries === 'object'
263+
? workflowConfig.retries.attempts
264+
: workflowConfig.retries
265+
} else {
266+
maxRetries = 0
267+
}
268+
}
269+
248270
try {
249271
const runnerOutput = await runner({
250272
input,
@@ -260,7 +282,7 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
260282
maxRetries,
261283
output,
262284
req,
263-
retriesConfig,
285+
retriesConfig: finalRetriesConfig,
264286
runnerOutput,
265287
state,
266288
taskConfig,
@@ -282,7 +304,7 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
282304
maxRetries,
283305
output,
284306
req,
285-
retriesConfig,
307+
retriesConfig: finalRetriesConfig,
286308
state,
287309
taskConfig,
288310
taskID,

0 commit comments

Comments
 (0)