feat(sdk): replace onStart lifecycle hook with onStartAttempt (#2515)
* fix(sdk): prevent uncaught errors thrown onSuccess, onComplete, and onFailure hooks to fail attempts & in some cases runs * Add onStartAttempt hook and deprecate onSuccess * Add onStartAttempt hook and deprecate onStart hook * Fix onStartAttempt overload types * Update lifecycle functions diagram
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
---
|
||||
"@trigger.dev/sdk": minor
|
||||
---
|
||||
|
||||
Prevent uncaught errors in the `onSuccess`, `onComplete`, and `onFailure` lifecycle hooks from failing attempts/runs.
|
||||
|
||||
Deprecated the `onStart` lifecycle hook (which only fires before the `run` function on the first attempt). Replaced with `onStartAttempt` that fires before the run function on every attempt:
|
||||
|
||||
```ts
|
||||
export const taskWithOnStartAttempt = task({
|
||||
id: "task-with-on-start-attempt",
|
||||
onStartAttempt: async ({ payload, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
|
||||
// Default a global lifecycle hook using tasks
|
||||
tasks.onStartAttempt(({ ctx, payload, task }) => {
|
||||
console.log(
|
||||
`Run ${ctx.run.id} started on task ${task} attempt ${ctx.run.attempt.number}`,
|
||||
ctx.run
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
If you want to execute code before just the first attempt, you can use the `onStartAttempt` function and check `ctx.run.attempt.number === 1`:
|
||||
|
||||
```ts /trigger/on-start-attempt.ts
|
||||
export const taskWithOnStartAttempt = task({
|
||||
id: "task-with-on-start-attempt",
|
||||
onStartAttempt: async ({ payload, ctx }) => {
|
||||
if (ctx.run.attempt.number === 1) {
|
||||
console.log("Run started on attempt 1", ctx.run);
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -98,6 +98,7 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
|
||||
return <RunFunctionIcon className={cn(className, "text-text-dimmed")} />;
|
||||
case "task-hook-init":
|
||||
case "task-hook-onStart":
|
||||
case "task-hook-onStartAttempt":
|
||||
case "task-hook-onSuccess":
|
||||
case "task-hook-onWait":
|
||||
case "task-hook-onResume":
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 28 KiB |
+174
-84
@@ -174,63 +174,14 @@ tasks.onStart(({ ctx, payload, task }) => {
|
||||
|
||||

|
||||
|
||||
### `init` function
|
||||
|
||||
This function is called before a run attempt:
|
||||
|
||||
```ts /trigger/init.ts
|
||||
export const taskWithInit = task({
|
||||
id: "task-with-init",
|
||||
init: async ({ payload, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also return data from the `init` function that will be available in the params of the `run`, `cleanup`, `onSuccess`, and `onFailure` functions.
|
||||
|
||||
```ts /trigger/init-return.ts
|
||||
export const taskWithInitReturn = task({
|
||||
id: "task-with-init-return",
|
||||
init: async ({ payload, ctx }) => {
|
||||
return { someData: "someValue" };
|
||||
},
|
||||
run: async (payload: any, { ctx, init }) => {
|
||||
console.log(init.someData); // "someValue"
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `init` function are ignored.</Info>
|
||||
|
||||
### `cleanup` function
|
||||
|
||||
This function is called after the `run` function is executed, regardless of whether the run was successful or not. It's useful for cleaning up resources, logging, or other side effects.
|
||||
|
||||
```ts /trigger/cleanup.ts
|
||||
export const taskWithCleanup = task({
|
||||
id: "task-with-cleanup",
|
||||
cleanup: async ({ payload, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `cleanup` function will fail the attempt.</Info>
|
||||
|
||||
### `middleware` and `locals` functions
|
||||
|
||||
Our task middleware system runs at the top level, executing before and after all lifecycle hooks. This allows you to wrap the entire task execution lifecycle with custom logic.
|
||||
|
||||
<Info>
|
||||
An error thrown in `middleware` is just like an uncaught error in the run function: it will
|
||||
propagate through to `catchError()` function and then will fail the attempt (causing a retry).
|
||||
propagate through to `catchError()` function and then will fail the attempt (either causing a
|
||||
retry or failing the run).
|
||||
</Info>
|
||||
|
||||
The `locals` API allows you to share data between middleware and hooks.
|
||||
@@ -296,14 +247,16 @@ export const myTask = task({
|
||||
});
|
||||
```
|
||||
|
||||
### `onStart` function
|
||||
### `onStartAttempt` function
|
||||
|
||||
When a task run starts, the `onStart` function is called. It's useful for sending notifications, logging, and other side effects. This function will only be called one per run (not per retry). If you want to run code before each retry, use the `init` function.
|
||||
<Info>The `onStartAttempt` function was introduced in v4.1.0</Info>
|
||||
|
||||
Before a task run attempt starts, the `onStartAttempt` function is called. It's useful for sending notifications, logging, and other side effects.
|
||||
|
||||
```ts /trigger/on-start.ts
|
||||
export const taskWithOnStart = task({
|
||||
id: "task-with-on-start",
|
||||
onStart: async ({ payload, ctx }) => {
|
||||
export const taskWithOnStartAttempt = task({
|
||||
id: "task-with-on-start-attempt",
|
||||
onStartAttempt: async ({ payload, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
@@ -312,20 +265,33 @@ export const taskWithOnStart = task({
|
||||
});
|
||||
```
|
||||
|
||||
You can also define an `onStart` function in your `trigger.config.ts` file to get notified when any task starts.
|
||||
You can also define a global `onStartAttempt` function using `tasks.onStartAttempt()`.
|
||||
|
||||
```ts trigger.config.ts
|
||||
import { defineConfig } from "@trigger.dev/sdk";
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
export default defineConfig({
|
||||
project: "proj_1234",
|
||||
onStart: async ({ payload, ctx }) => {
|
||||
console.log("Task started", ctx.task.id);
|
||||
},
|
||||
tasks.onStartAttempt(({ ctx, payload, task }) => {
|
||||
console.log(
|
||||
`Run ${ctx.run.id} started on task ${task} attempt ${ctx.run.attempt.number}`,
|
||||
ctx.run
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `onStart` function are ignored.</Info>
|
||||
<Info>Errors thrown in the `onStartAttempt` function will cause the attempt to fail.</Info>
|
||||
|
||||
If you want to execute code before just the first attempt, you can use the `onStartAttempt` function and check `ctx.run.attempt.number === 1`:
|
||||
|
||||
```ts /trigger/on-start-attempt.ts
|
||||
export const taskWithOnStartAttempt = task({
|
||||
id: "task-with-on-start-attempt",
|
||||
onStartAttempt: async ({ payload, ctx }) => {
|
||||
if (ctx.run.attempt.number === 1) {
|
||||
console.log("Run started on attempt 1", ctx.run);
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `onWait` and `onResume` functions
|
||||
|
||||
@@ -350,6 +316,20 @@ export const myTask = task({
|
||||
});
|
||||
```
|
||||
|
||||
You can also define global `onWait` and `onResume` functions using `tasks.onWait()` and `tasks.onResume()`:
|
||||
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
tasks.onWait(({ ctx, payload, wait, task }) => {
|
||||
console.log("Run paused", ctx.run, wait);
|
||||
});
|
||||
|
||||
tasks.onResume(({ ctx, payload, wait, task }) => {
|
||||
console.log("Run resumed", ctx.run, wait);
|
||||
});
|
||||
```
|
||||
|
||||
### `onSuccess` function
|
||||
|
||||
When a task run succeeds, the `onSuccess` function is called. It's useful for sending notifications, logging, syncing state to your database, or other side effects.
|
||||
@@ -366,20 +346,20 @@ export const taskWithOnSuccess = task({
|
||||
});
|
||||
```
|
||||
|
||||
You can also define an `onSuccess` function in your `trigger.config.ts` file to get notified when any task succeeds.
|
||||
You can also define a global `onSuccess` function using `tasks.onSuccess()`.
|
||||
|
||||
```ts trigger.config.ts
|
||||
import { defineConfig } from "@trigger.dev/sdk";
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
export default defineConfig({
|
||||
project: "proj_1234",
|
||||
onSuccess: async ({ payload, output, ctx }) => {
|
||||
console.log("Task succeeded", ctx.task.id);
|
||||
},
|
||||
tasks.onSuccess(({ ctx, payload, output }) => {
|
||||
console.log("Task succeeded", ctx.task.id);
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `onSuccess` function are ignored.</Info>
|
||||
<Info>
|
||||
Errors thrown in the `onSuccess` function will be ignored, but you will still be able to see them
|
||||
in the dashboard.
|
||||
</Info>
|
||||
|
||||
### `onComplete` function
|
||||
|
||||
@@ -397,6 +377,21 @@ export const taskWithOnComplete = task({
|
||||
});
|
||||
```
|
||||
|
||||
You can also define a global `onComplete` function using `tasks.onComplete()`.
|
||||
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
tasks.onComplete(({ ctx, payload, output }) => {
|
||||
console.log("Task completed", ctx.task.id);
|
||||
});
|
||||
```
|
||||
|
||||
<Info>
|
||||
Errors thrown in the `onComplete` function will be ignored, but you will still be able to see them
|
||||
in the dashboard.
|
||||
</Info>
|
||||
|
||||
### `onFailure` function
|
||||
|
||||
When a task run fails, the `onFailure` function is called. It's useful for sending notifications, logging, or other side effects. It will only be executed once the task run has exhausted all its retries.
|
||||
@@ -413,20 +408,20 @@ export const taskWithOnFailure = task({
|
||||
});
|
||||
```
|
||||
|
||||
You can also define an `onFailure` function in your `trigger.config.ts` file to get notified when any task fails.
|
||||
You can also define a global `onFailure` function using `tasks.onFailure()`.
|
||||
|
||||
```ts trigger.config.ts
|
||||
import { defineConfig } from "@trigger.dev/sdk";
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
export default defineConfig({
|
||||
project: "proj_1234",
|
||||
onFailure: async ({ payload, error, ctx }) => {
|
||||
console.log("Task failed", ctx.task.id);
|
||||
},
|
||||
tasks.onFailure(({ ctx, payload, error }) => {
|
||||
console.log("Task failed", ctx.task.id);
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `onFailure` function are ignored.</Info>
|
||||
<Info>
|
||||
Errors thrown in the `onFailure` function will be ignored, but you will still be able to see them
|
||||
in the dashboard.
|
||||
</Info>
|
||||
|
||||
<Note>
|
||||
`onFailure` doesn’t fire for some of the run statuses like `Crashed`, `System failures`, and
|
||||
@@ -441,7 +436,7 @@ Read more about `catchError` in our [Errors and Retrying guide](/errors-retrying
|
||||
|
||||
<Info>Uncaught errors will throw a special internal error of the type `HANDLE_ERROR_ERROR`.</Info>
|
||||
|
||||
### onCancel
|
||||
### `onCancel` function
|
||||
|
||||
You can define an `onCancel` hook that is called when a run is cancelled. This is useful if you want to clean up any resources that were allocated for the run.
|
||||
|
||||
@@ -540,6 +535,101 @@ export const cancelExampleTask = task({
|
||||
point the process will be killed.
|
||||
</Note>
|
||||
|
||||
### `onStart` function (deprecated)
|
||||
|
||||
<Info>The `onStart` function was deprecated in v4.1.0. Use `onStartAttempt` instead.</Info>
|
||||
|
||||
When a task run starts, the `onStart` function is called. It's useful for sending notifications, logging, and other side effects.
|
||||
|
||||
<Warning>
|
||||
This function will only be called once per run (not per attempt). If you want to run code before
|
||||
each attempt, use a middleware function or the `onStartAttempt` function.
|
||||
</Warning>
|
||||
|
||||
```ts /trigger/on-start.ts
|
||||
export const taskWithOnStart = task({
|
||||
id: "task-with-on-start",
|
||||
onStart: async ({ payload, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also define a global `onStart` function using `tasks.onStart()`.
|
||||
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
tasks.onStart(({ ctx, payload, task }) => {
|
||||
console.log(`Run ${ctx.run.id} started on task ${task}`, ctx.run);
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `onStart` function will cause the attempt to fail.</Info>
|
||||
|
||||
### `init` function (deprecated)
|
||||
|
||||
<Warning>
|
||||
The `init` hook is deprecated and will be removed in the future. Use
|
||||
[middleware](/tasks/overview#middleware-and-locals-functions) instead.
|
||||
</Warning>
|
||||
|
||||
This function is called before a run attempt:
|
||||
|
||||
```ts /trigger/init.ts
|
||||
export const taskWithInit = task({
|
||||
id: "task-with-init",
|
||||
init: async ({ payload, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also return data from the `init` function that will be available in the params of the `run`, `cleanup`, `onSuccess`, and `onFailure` functions.
|
||||
|
||||
```ts /trigger/init-return.ts
|
||||
export const taskWithInitReturn = task({
|
||||
id: "task-with-init-return",
|
||||
init: async ({ payload, ctx }) => {
|
||||
return { someData: "someValue" };
|
||||
},
|
||||
run: async (payload: any, { ctx, init }) => {
|
||||
console.log(init.someData); // "someValue"
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `init` function will cause the attempt to fail.</Info>
|
||||
|
||||
### `cleanup` function (deprecated)
|
||||
|
||||
<Warning>
|
||||
The `cleanup` hook is deprecated and will be removed in the future. Use
|
||||
[middleware](/tasks/overview#middleware-and-locals-functions) instead.
|
||||
</Warning>
|
||||
|
||||
This function is called after the `run` function is executed, regardless of whether the run was successful or not. It's useful for cleaning up resources, logging, or other side effects.
|
||||
|
||||
```ts /trigger/cleanup.ts
|
||||
export const taskWithCleanup = task({
|
||||
id: "task-with-cleanup",
|
||||
cleanup: async ({ payload, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `cleanup` function will cause the attempt to fail.</Info>
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup>
|
||||
|
||||
@@ -35,4 +35,5 @@ export type {
|
||||
TaskCancelHookParams,
|
||||
OnCancelHookFunction,
|
||||
AnyOnCancelHookFunction,
|
||||
AnyOnStartAttemptHookFunction,
|
||||
} from "./lifecycleHooks/types.js";
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
RegisterHookFunctionParams,
|
||||
TaskWait,
|
||||
type LifecycleHooksManager,
|
||||
AnyOnStartAttemptHookFunction,
|
||||
} from "./types.js";
|
||||
|
||||
const NOOP_LIFECYCLE_HOOKS_MANAGER = new NoopLifecycleHooksManager();
|
||||
@@ -81,6 +82,27 @@ export class LifecycleHooksAPI {
|
||||
return this.#getManager().getGlobalStartHooks();
|
||||
}
|
||||
|
||||
public registerTaskStartAttemptHook(
|
||||
taskId: string,
|
||||
hook: RegisterHookFunctionParams<AnyOnStartAttemptHookFunction>
|
||||
): void {
|
||||
this.#getManager().registerTaskStartAttemptHook(taskId, hook);
|
||||
}
|
||||
|
||||
public registerGlobalStartAttemptHook(
|
||||
hook: RegisterHookFunctionParams<AnyOnStartAttemptHookFunction>
|
||||
): void {
|
||||
this.#getManager().registerGlobalStartAttemptHook(hook);
|
||||
}
|
||||
|
||||
public getTaskStartAttemptHook(taskId: string): AnyOnStartAttemptHookFunction | undefined {
|
||||
return this.#getManager().getTaskStartAttemptHook(taskId);
|
||||
}
|
||||
|
||||
public getGlobalStartAttemptHooks(): RegisteredHookFunction<AnyOnStartAttemptHookFunction>[] {
|
||||
return this.#getManager().getGlobalStartAttemptHooks();
|
||||
}
|
||||
|
||||
public registerGlobalFailureHook(
|
||||
hook: RegisterHookFunctionParams<AnyOnFailureHookFunction>
|
||||
): void {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
AnyOnCleanupHookFunction,
|
||||
TaskWait,
|
||||
AnyOnCancelHookFunction,
|
||||
AnyOnStartAttemptHookFunction,
|
||||
} from "./types.js";
|
||||
|
||||
export class StandardLifecycleHooksManager implements LifecycleHooksManager {
|
||||
@@ -23,6 +24,15 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager {
|
||||
private globalStartHooks: Map<string, RegisteredHookFunction<AnyOnStartHookFunction>> = new Map();
|
||||
private taskStartHooks: Map<string, RegisteredHookFunction<AnyOnStartHookFunction>> = new Map();
|
||||
|
||||
private globalStartAttemptHooks: Map<
|
||||
string,
|
||||
RegisteredHookFunction<AnyOnStartAttemptHookFunction>
|
||||
> = new Map();
|
||||
private taskStartAttemptHooks: Map<
|
||||
string,
|
||||
RegisteredHookFunction<AnyOnStartAttemptHookFunction>
|
||||
> = new Map();
|
||||
|
||||
private globalFailureHooks: Map<string, RegisteredHookFunction<AnyOnFailureHookFunction>> =
|
||||
new Map();
|
||||
private taskFailureHooks: Map<string, RegisteredHookFunction<AnyOnFailureHookFunction>> =
|
||||
@@ -129,6 +139,37 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager {
|
||||
return Array.from(this.globalStartHooks.values());
|
||||
}
|
||||
|
||||
registerGlobalStartAttemptHook(
|
||||
hook: RegisterHookFunctionParams<AnyOnStartAttemptHookFunction>
|
||||
): void {
|
||||
const id = generateHookId(hook);
|
||||
this.globalStartAttemptHooks.set(id, {
|
||||
id,
|
||||
name: hook.id,
|
||||
fn: hook.fn,
|
||||
});
|
||||
}
|
||||
|
||||
registerTaskStartAttemptHook(
|
||||
taskId: string,
|
||||
hook: RegisterHookFunctionParams<AnyOnStartAttemptHookFunction>
|
||||
): void {
|
||||
const id = generateHookId(hook);
|
||||
this.taskStartAttemptHooks.set(taskId, {
|
||||
id,
|
||||
name: hook.id,
|
||||
fn: hook.fn,
|
||||
});
|
||||
}
|
||||
|
||||
getTaskStartAttemptHook(taskId: string): AnyOnStartAttemptHookFunction | undefined {
|
||||
return this.taskStartAttemptHooks.get(taskId)?.fn;
|
||||
}
|
||||
|
||||
getGlobalStartAttemptHooks(): RegisteredHookFunction<AnyOnStartAttemptHookFunction>[] {
|
||||
return Array.from(this.globalStartAttemptHooks.values());
|
||||
}
|
||||
|
||||
registerGlobalInitHook(hook: RegisterHookFunctionParams<AnyOnInitHookFunction>): void {
|
||||
// if there is no id, lets generate one based on the contents of the function
|
||||
const id = generateHookId(hook);
|
||||
@@ -527,6 +568,22 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager {
|
||||
return [];
|
||||
}
|
||||
|
||||
registerGlobalStartAttemptHook(): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
registerTaskStartAttemptHook(): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
getTaskStartAttemptHook(): undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getGlobalStartAttemptHooks(): RegisteredHookFunction<AnyOnStartAttemptHookFunction>[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
registerGlobalFailureHook(hook: RegisterHookFunctionParams<AnyOnFailureHookFunction>): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
@@ -33,6 +33,19 @@ export type OnStartHookFunction<TPayload, TInitOutput extends TaskInitOutput = T
|
||||
|
||||
export type AnyOnStartHookFunction = OnStartHookFunction<unknown, TaskInitOutput>;
|
||||
|
||||
export type TaskStartAttemptHookParams<TPayload = unknown> = {
|
||||
ctx: TaskRunContext;
|
||||
payload: TPayload;
|
||||
task: string;
|
||||
signal: AbortSignal;
|
||||
};
|
||||
|
||||
export type OnStartAttemptHookFunction<TPayload> = (
|
||||
params: TaskStartAttemptHookParams<TPayload>
|
||||
) => undefined | void | Promise<undefined | void>;
|
||||
|
||||
export type AnyOnStartAttemptHookFunction = OnStartAttemptHookFunction<unknown>;
|
||||
|
||||
export type TaskWait =
|
||||
| {
|
||||
type: "duration";
|
||||
@@ -268,6 +281,17 @@ export interface LifecycleHooksManager {
|
||||
): void;
|
||||
getTaskStartHook(taskId: string): AnyOnStartHookFunction | undefined;
|
||||
getGlobalStartHooks(): RegisteredHookFunction<AnyOnStartHookFunction>[];
|
||||
|
||||
registerGlobalStartAttemptHook(
|
||||
hook: RegisterHookFunctionParams<AnyOnStartAttemptHookFunction>
|
||||
): void;
|
||||
registerTaskStartAttemptHook(
|
||||
taskId: string,
|
||||
hook: RegisterHookFunctionParams<AnyOnStartAttemptHookFunction>
|
||||
): void;
|
||||
getTaskStartAttemptHook(taskId: string): AnyOnStartAttemptHookFunction | undefined;
|
||||
getGlobalStartAttemptHooks(): RegisteredHookFunction<AnyOnStartAttemptHookFunction>[];
|
||||
|
||||
registerGlobalFailureHook(hook: RegisterHookFunctionParams<AnyOnFailureHookFunction>): void;
|
||||
registerTaskFailureHook(
|
||||
taskId: string,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
OnSuccessHookFunction,
|
||||
OnWaitHookFunction,
|
||||
OnCancelHookFunction,
|
||||
OnStartAttemptHookFunction,
|
||||
} from "../lifecycleHooks/types.js";
|
||||
import { RunTags } from "../schemas/api.js";
|
||||
import {
|
||||
@@ -114,6 +115,13 @@ export type StartFnParams = Prettify<{
|
||||
signal: AbortSignal;
|
||||
}>;
|
||||
|
||||
export type StartAttemptFnParams = Prettify<{
|
||||
ctx: Context;
|
||||
init?: InitOutput;
|
||||
/** Abort signal that is aborted when a task run exceeds it's maxDuration or if the task run is cancelled. Can be used to automatically cancel downstream requests */
|
||||
signal: AbortSignal;
|
||||
}>;
|
||||
|
||||
export type CancelFnParams = Prettify<{
|
||||
ctx: Context;
|
||||
/** Abort signal that is aborted when a task run exceeds it's maxDuration or if the task run is cancelled. Can be used to automatically cancel downstream requests */
|
||||
@@ -328,9 +336,18 @@ type CommonTaskOptions<
|
||||
|
||||
/**
|
||||
* onStart is called the first time a task is executed in a run (not before every retry)
|
||||
*
|
||||
* @deprecated Use onStartAttempt instead
|
||||
*/
|
||||
onStart?: OnStartHookFunction<TPayload, TInitOutput>;
|
||||
|
||||
/**
|
||||
* onStartAttempt is called before each attempt of a task is executed.
|
||||
*
|
||||
* You can detect the first attempt by checking `ctx.attempt.number === 1`.
|
||||
*/
|
||||
onStartAttempt?: OnStartAttemptHookFunction<TPayload>;
|
||||
|
||||
/**
|
||||
* onSuccess is called after the run function has successfully completed.
|
||||
*/
|
||||
@@ -913,6 +930,7 @@ export type TaskMetadataWithFunctions = TaskMetadata & {
|
||||
onSuccess?: (payload: any, output: any, params: SuccessFnParams<any>) => Promise<void>;
|
||||
onFailure?: (payload: any, error: unknown, params: FailureFnParams<any>) => Promise<void>;
|
||||
onStart?: (payload: any, params: StartFnParams) => Promise<void>;
|
||||
onStartAttempt?: (payload: any, params: StartAttemptFnParams) => Promise<void>;
|
||||
parsePayload?: AnySchemaParseFn;
|
||||
};
|
||||
schema?: TaskSchema;
|
||||
|
||||
@@ -182,6 +182,8 @@ export class TaskExecutor {
|
||||
await this.#callOnStartFunctions(payload, ctx, initOutput, signal);
|
||||
}
|
||||
|
||||
await this.#callOnStartAttemptFunctions(payload, ctx, signal);
|
||||
|
||||
try {
|
||||
return await this.#callRun(payload, ctx, initOutput, signal);
|
||||
} catch (error) {
|
||||
@@ -777,7 +779,7 @@ export class TaskExecutor {
|
||||
|
||||
return await runTimelineMetrics.measureMetric("trigger.dev/execution", "success", async () => {
|
||||
for (const hook of globalSuccessHooks) {
|
||||
const [hookError] = await tryCatch(
|
||||
await tryCatch(
|
||||
this._tracer.startActiveSpan(
|
||||
"onSuccess()",
|
||||
async (span) => {
|
||||
@@ -799,14 +801,10 @@ export class TaskExecutor {
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (hookError) {
|
||||
throw hookError;
|
||||
}
|
||||
}
|
||||
|
||||
if (taskSuccessHook) {
|
||||
const [hookError] = await tryCatch(
|
||||
await tryCatch(
|
||||
this._tracer.startActiveSpan(
|
||||
"onSuccess()",
|
||||
async (span) => {
|
||||
@@ -828,10 +826,6 @@ export class TaskExecutor {
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (hookError) {
|
||||
throw hookError;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -852,7 +846,7 @@ export class TaskExecutor {
|
||||
|
||||
return await runTimelineMetrics.measureMetric("trigger.dev/execution", "failure", async () => {
|
||||
for (const hook of globalFailureHooks) {
|
||||
const [hookError] = await tryCatch(
|
||||
await tryCatch(
|
||||
this._tracer.startActiveSpan(
|
||||
"onFailure()",
|
||||
async (span) => {
|
||||
@@ -874,14 +868,10 @@ export class TaskExecutor {
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (hookError) {
|
||||
throw hookError;
|
||||
}
|
||||
}
|
||||
|
||||
if (taskFailureHook) {
|
||||
const [hookError] = await tryCatch(
|
||||
await tryCatch(
|
||||
this._tracer.startActiveSpan(
|
||||
"onFailure()",
|
||||
async (span) => {
|
||||
@@ -903,10 +893,6 @@ export class TaskExecutor {
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (hookError) {
|
||||
throw hookError;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -989,6 +975,70 @@ export class TaskExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
async #callOnStartAttemptFunctions(payload: unknown, ctx: TaskRunContext, signal: AbortSignal) {
|
||||
const globalStartHooks = lifecycleHooks.getGlobalStartAttemptHooks();
|
||||
const taskStartHook = lifecycleHooks.getTaskStartAttemptHook(this.task.id);
|
||||
|
||||
if (globalStartHooks.length === 0 && !taskStartHook) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await runTimelineMetrics.measureMetric(
|
||||
"trigger.dev/execution",
|
||||
"startAttempt",
|
||||
async () => {
|
||||
for (const hook of globalStartHooks) {
|
||||
const [hookError] = await tryCatch(
|
||||
this._tracer.startActiveSpan(
|
||||
"onStartAttempt()",
|
||||
async (span) => {
|
||||
await hook.fn({ payload, ctx, signal, task: this.task.id });
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
[SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStartAttempt",
|
||||
[SemanticInternalAttributes.COLLAPSED]: true,
|
||||
...this.#lifecycleHookAccessoryAttributes(hook.name),
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (hookError) {
|
||||
throw hookError;
|
||||
}
|
||||
}
|
||||
|
||||
if (taskStartHook) {
|
||||
const [hookError] = await tryCatch(
|
||||
this._tracer.startActiveSpan(
|
||||
"onStartAttempt()",
|
||||
async (span) => {
|
||||
await taskStartHook({
|
||||
payload,
|
||||
ctx,
|
||||
signal,
|
||||
task: this.task.id,
|
||||
});
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
[SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStartAttempt",
|
||||
[SemanticInternalAttributes.COLLAPSED]: true,
|
||||
...this.#lifecycleHookAccessoryAttributes("task"),
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (hookError) {
|
||||
throw hookError;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async #cleanupAndWaitUntil(
|
||||
payload: unknown,
|
||||
ctx: TaskRunContext,
|
||||
@@ -1297,7 +1347,7 @@ export class TaskExecutor {
|
||||
|
||||
return await runTimelineMetrics.measureMetric("trigger.dev/execution", "complete", async () => {
|
||||
for (const hook of globalCompleteHooks) {
|
||||
const [hookError] = await tryCatch(
|
||||
await tryCatch(
|
||||
this._tracer.startActiveSpan(
|
||||
"onComplete()",
|
||||
async (span) => {
|
||||
@@ -1319,14 +1369,10 @@ export class TaskExecutor {
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (hookError) {
|
||||
throw hookError;
|
||||
}
|
||||
}
|
||||
|
||||
if (taskCompleteHook) {
|
||||
const [hookError] = await tryCatch(
|
||||
await tryCatch(
|
||||
this._tracer.startActiveSpan(
|
||||
"onComplete()",
|
||||
async (span) => {
|
||||
@@ -1348,10 +1394,6 @@ export class TaskExecutor {
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (hookError) {
|
||||
throw hookError;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -269,6 +269,91 @@ describe("TaskExecutor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("should call onStartAttempt hooks in correct order with proper data", async () => {
|
||||
const globalStartOrder: string[] = [];
|
||||
const startPayloads: any[] = [];
|
||||
|
||||
// Register global init hook to provide init data
|
||||
lifecycleHooks.registerGlobalInitHook({
|
||||
id: "test-init",
|
||||
fn: async () => {
|
||||
return {
|
||||
foo: "bar",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Register two global start hooks
|
||||
lifecycleHooks.registerGlobalStartAttemptHook({
|
||||
id: "global-start-1",
|
||||
fn: async ({ payload, ctx }) => {
|
||||
console.log("Executing global start hook 1");
|
||||
globalStartOrder.push("global-1");
|
||||
startPayloads.push(payload);
|
||||
},
|
||||
});
|
||||
|
||||
lifecycleHooks.registerGlobalStartAttemptHook({
|
||||
id: "global-start-2",
|
||||
fn: async ({ payload, ctx }) => {
|
||||
console.log("Executing global start hook 2");
|
||||
globalStartOrder.push("global-2");
|
||||
startPayloads.push(payload);
|
||||
},
|
||||
});
|
||||
|
||||
// Register task-specific start hook
|
||||
lifecycleHooks.registerTaskStartAttemptHook("test-task", {
|
||||
id: "task-start",
|
||||
fn: async ({ payload, ctx }) => {
|
||||
console.log("Executing task start hook");
|
||||
globalStartOrder.push("task");
|
||||
startPayloads.push(payload);
|
||||
},
|
||||
});
|
||||
|
||||
// Verify hooks are registered
|
||||
const globalHooks = lifecycleHooks.getGlobalStartAttemptHooks();
|
||||
console.log(
|
||||
"Registered global hooks:",
|
||||
globalHooks.map((h) => h.id)
|
||||
);
|
||||
const taskHook = lifecycleHooks.getTaskStartAttemptHook("test-task");
|
||||
console.log("Registered task hook:", taskHook ? "yes" : "no");
|
||||
|
||||
const task = {
|
||||
id: "test-task",
|
||||
fns: {
|
||||
run: async (payload: any, params: RunFnParams<any>) => {
|
||||
return {
|
||||
output: "test-output",
|
||||
init: params.init,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await executeTask(task, { test: "data" }, undefined);
|
||||
|
||||
// Verify hooks were called in correct order
|
||||
expect(globalStartOrder).toEqual(["global-1", "global-2", "task"]);
|
||||
|
||||
// Verify each hook received the correct payload
|
||||
startPayloads.forEach((payload) => {
|
||||
expect(payload).toEqual({ test: "data" });
|
||||
});
|
||||
|
||||
// Verify the final result
|
||||
expect(result).toEqual({
|
||||
result: {
|
||||
ok: true,
|
||||
id: "test-run-id",
|
||||
output: '{"json":{"output":"test-output","init":{"foo":"bar"}}}',
|
||||
outputType: "application/super+json",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should call onFailure hooks with error when task fails", async () => {
|
||||
const globalFailureOrder: string[] = [];
|
||||
const failurePayloads: any[] = [];
|
||||
@@ -1246,6 +1331,48 @@ describe("TaskExecutor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("should NOT propagate errors from onSuccess hooks", async () => {
|
||||
const executionOrder: string[] = [];
|
||||
const expectedError = new Error("On success hook error");
|
||||
|
||||
// Register global on success hook that throws an error
|
||||
lifecycleHooks.registerGlobalSuccessHook({
|
||||
id: "global-success",
|
||||
fn: async () => {
|
||||
executionOrder.push("global-success");
|
||||
throw expectedError;
|
||||
},
|
||||
});
|
||||
|
||||
const task = {
|
||||
id: "test-task",
|
||||
fns: {
|
||||
run: async (payload: any, params: RunFnParams<any>) => {
|
||||
executionOrder.push("run");
|
||||
return {
|
||||
output: "test-output",
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Expect that this does not throw an error
|
||||
const result = await executeTask(task, { test: "data" }, undefined);
|
||||
|
||||
// Verify that run was called and on success hook was called
|
||||
expect(executionOrder).toEqual(["run", "global-success"]);
|
||||
|
||||
// Verify the error result
|
||||
expect(result).toEqual({
|
||||
result: {
|
||||
ok: true,
|
||||
id: "test-run-id",
|
||||
output: '{"json":{"output":"test-output"}}',
|
||||
outputType: "application/super+json",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should call cleanup hooks in correct order after other hooks but before middleware completion", async () => {
|
||||
const executionOrder: string[] = [];
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type AnyOnCatchErrorHookFunction,
|
||||
type AnyOnMiddlewareHookFunction,
|
||||
type AnyOnCancelHookFunction,
|
||||
type AnyOnStartAttemptHookFunction,
|
||||
} from "@trigger.dev/core/v3";
|
||||
|
||||
export type {
|
||||
@@ -41,6 +42,18 @@ export function onStart(
|
||||
});
|
||||
}
|
||||
|
||||
export function onStartAttempt(name: string, fn: AnyOnStartAttemptHookFunction): void;
|
||||
export function onStartAttempt(fn: AnyOnStartAttemptHookFunction): void;
|
||||
export function onStartAttempt(
|
||||
fnOrName: string | AnyOnStartAttemptHookFunction,
|
||||
fn?: AnyOnStartAttemptHookFunction
|
||||
): void {
|
||||
lifecycleHooks.registerGlobalStartAttemptHook({
|
||||
id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined,
|
||||
fn: typeof fnOrName === "function" ? fnOrName : fn!,
|
||||
});
|
||||
}
|
||||
|
||||
export function onFailure(name: string, fn: AnyOnFailureHookFunction): void;
|
||||
export function onFailure(fn: AnyOnFailureHookFunction): void;
|
||||
export function onFailure(
|
||||
|
||||
@@ -90,6 +90,7 @@ import type {
|
||||
TriggerAndWaitOptions,
|
||||
TriggerApiRequestOptions,
|
||||
TriggerOptions,
|
||||
AnyOnStartAttemptHookFunction,
|
||||
} from "@trigger.dev/core/v3";
|
||||
|
||||
export type {
|
||||
@@ -1588,6 +1589,12 @@ function registerTaskLifecycleHooks<
|
||||
});
|
||||
}
|
||||
|
||||
if (params.onStartAttempt) {
|
||||
lifecycleHooks.registerTaskStartAttemptHook(taskId, {
|
||||
fn: params.onStartAttempt as AnyOnStartAttemptHookFunction,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.onFailure) {
|
||||
lifecycleHooks.registerTaskFailureHook(taskId, {
|
||||
fn: params.onFailure as AnyOnFailureHookFunction,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
onStart,
|
||||
onStartAttempt,
|
||||
onFailure,
|
||||
onSuccess,
|
||||
onComplete,
|
||||
@@ -88,7 +89,9 @@ export const tasks = {
|
||||
batchTrigger,
|
||||
triggerAndWait,
|
||||
batchTriggerAndWait,
|
||||
/** @deprecated Use onStartAttempt instead */
|
||||
onStart,
|
||||
onStartAttempt,
|
||||
onFailure,
|
||||
onSuccess,
|
||||
onComplete,
|
||||
|
||||
@@ -439,3 +439,93 @@ export const lotsOfLogsTask = task({
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const throwErrorInOnSuccessHookTask = task({
|
||||
id: "throw-error-in-on-success-hook",
|
||||
run: async (payload: { message: string }, { ctx }) => {
|
||||
logger.info("Hello, world from the throw error in on success hook task", {
|
||||
message: payload.message,
|
||||
});
|
||||
},
|
||||
onSuccess: async ({ payload, output, ctx }) => {
|
||||
logger.info("Hello, world from the on success hook", { payload, output });
|
||||
throw new Error("Forced error to cause a retry");
|
||||
},
|
||||
});
|
||||
|
||||
export const throwErrorInOnStartHookTask = task({
|
||||
id: "throw-error-in-on-start-hook",
|
||||
run: async (payload: { message: string }, { ctx }) => {
|
||||
logger.info("Hello, world from the throw error in on start hook task", {
|
||||
message: payload.message,
|
||||
});
|
||||
},
|
||||
onStart: async ({ payload, ctx }) => {
|
||||
logger.info("Hello, world from the on start hook", { payload });
|
||||
throw new Error("Forced error to cause a retry");
|
||||
},
|
||||
});
|
||||
|
||||
export const throwErrorInOnCompleteHookTask = task({
|
||||
id: "throw-error-in-on-complete-hook",
|
||||
run: async (payload: { message: string }, { ctx }) => {
|
||||
logger.info("Hello, world from the throw error in on complete hook task", {
|
||||
message: payload.message,
|
||||
});
|
||||
},
|
||||
onComplete: async ({ payload, result, ctx }) => {
|
||||
logger.info("Hello, world from the on complete hook", { payload, result });
|
||||
throw new Error("Forced error to cause a retry");
|
||||
},
|
||||
});
|
||||
|
||||
export const throwErrorInOnFailureHookTask = task({
|
||||
id: "throw-error-in-on-failure-hook",
|
||||
retry: {
|
||||
maxAttempts: 1,
|
||||
},
|
||||
run: async (payload: { message: string }, { ctx }) => {
|
||||
logger.info("Hello, world from the throw error in on failure hook task", {
|
||||
message: payload.message,
|
||||
});
|
||||
throw new Error("Forced error to cause a retry");
|
||||
},
|
||||
onFailure: async ({ payload, error, ctx }) => {
|
||||
logger.info("Hello, world from the on failure hook", { payload, error });
|
||||
throw new Error("Forced error to cause a retry in on failure hook");
|
||||
},
|
||||
});
|
||||
|
||||
export const throwErrorInInitHookTask = task({
|
||||
id: "throw-error-in-init-hook",
|
||||
run: async (payload: { message: string }, { ctx }) => {
|
||||
logger.info("Hello, world from the throw error in init hook task", {
|
||||
message: payload.message,
|
||||
});
|
||||
},
|
||||
init: async ({ payload, ctx }) => {
|
||||
logger.info("Hello, world from the init hook", { payload });
|
||||
throw new Error("Forced error to cause a retry");
|
||||
},
|
||||
});
|
||||
|
||||
export const testStartAttemptHookTask = task({
|
||||
id: "test-start-attempt-hook",
|
||||
retry: {
|
||||
maxAttempts: 3,
|
||||
},
|
||||
run: async (payload: { message: string }, { ctx }) => {
|
||||
logger.info("Hello, world from the test start attempt hook task", { message: payload.message });
|
||||
|
||||
if (ctx.attempt.number === 1) {
|
||||
throw new Error("Forced error to cause a retry so we can test the onStartAttempt hook");
|
||||
}
|
||||
},
|
||||
onStartAttempt: async ({ payload, ctx }) => {
|
||||
console.log(`onStartAttempt hook called ${ctx.attempt.number}`);
|
||||
},
|
||||
});
|
||||
|
||||
tasks.onStartAttempt(({ payload, ctx }) => {
|
||||
console.log(`global onStartAttempt hook called ${ctx.attempt.number}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user