Skip to content

Commit c963dcd

Browse files
authored
fix: use default retry settings when no catchError handlers are defined (#1852)
1 parent 0e77747 commit c963dcd

File tree

4 files changed

+103
-9
lines changed

4 files changed

+103
-9
lines changed

apps/webapp/app/runEngine/services/triggerTask.server.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ export class RunEngineTriggerTaskService extends WithRunEngine {
282282
runtimeEnvironmentId: environment.id,
283283
version: body.options?.lockToVersion,
284284
},
285+
select: {
286+
id: true,
287+
version: true,
288+
sdkVersion: true,
289+
cliVersion: true,
290+
},
285291
})
286292
: undefined;
287293

packages/core/src/v3/workers/taskExecutor.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,14 @@ export class TaskExecutor {
10271027
}
10281028
}
10291029

1030+
const defaultRetryResult =
1031+
typeof defaultDelay === "undefined"
1032+
? { status: "noop" as const }
1033+
: {
1034+
status: "retry" as const,
1035+
retry: { timestamp: Date.now() + defaultDelay, delay: defaultDelay },
1036+
};
1037+
10301038
// Check if retries are enabled in dev environment
10311039
if (
10321040
execution.environment.type === "DEVELOPMENT" &&
@@ -1040,7 +1048,7 @@ export class TaskExecutor {
10401048
const globalCatchErrorHooks = lifecycleHooks.getGlobalCatchErrorHooks();
10411049

10421050
if (globalCatchErrorHooks.length === 0 && !taskCatchErrorHook) {
1043-
return { status: "noop" };
1051+
return defaultRetryResult;
10441052
}
10451053

10461054
return this._tracer.startActiveSpan(
@@ -1085,12 +1093,7 @@ export class TaskExecutor {
10851093
}
10861094

10871095
// If no hooks handled the error, use default retry behavior
1088-
return typeof defaultDelay === "undefined"
1089-
? { status: "noop" as const }
1090-
: {
1091-
status: "retry" as const,
1092-
retry: { timestamp: Date.now() + defaultDelay, delay: defaultDelay },
1093-
};
1096+
return defaultRetryResult;
10941097
},
10951098
{
10961099
attributes: {

packages/core/test/taskExecutor.test.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, test } from "vitest";
22
import { ConsoleInterceptor } from "../src/v3/consoleInterceptor.js";
33
import {
4+
RetryOptions,
45
RunFnParams,
56
ServerBackgroundWorker,
67
TaskMetadataWithFunctions,
@@ -784,6 +785,48 @@ describe("TaskExecutor", () => {
784785
expect((result as any).result.retry.delay).toBeLessThan(30100);
785786
});
786787

788+
test("should use the default retry settings if no catch error hook is provided", async () => {
789+
const expectedError = new Error("Task failed intentionally");
790+
791+
const task = {
792+
id: "test-task",
793+
fns: {
794+
run: async (payload: any, params: RunFnParams<any>) => {
795+
throw expectedError;
796+
},
797+
},
798+
};
799+
800+
const result = await executeTask(task, { test: "data" }, undefined, {
801+
maxAttempts: 3,
802+
minTimeoutInMs: 1000,
803+
maxTimeoutInMs: 5000,
804+
factor: 2,
805+
});
806+
807+
// Verify the final result contains the specific retry timing
808+
expect(result).toEqual({
809+
result: {
810+
ok: false,
811+
id: "test-run-id",
812+
error: {
813+
type: "BUILT_IN_ERROR",
814+
message: "Task failed intentionally",
815+
name: "Error",
816+
stackTrace: expect.any(String),
817+
},
818+
retry: {
819+
timestamp: expect.any(Number),
820+
delay: expect.any(Number),
821+
},
822+
skippedRetrying: false,
823+
},
824+
});
825+
826+
expect((result as any).result.retry.delay).toBeGreaterThan(1000);
827+
expect((result as any).result.retry.delay).toBeLessThan(3000);
828+
});
829+
787830
test("should execute middleware hooks in correct order around other hooks", async () => {
788831
const executionOrder: string[] = [];
789832

@@ -1623,7 +1666,12 @@ describe("TaskExecutor", () => {
16231666
});
16241667
});
16251668

1626-
function executeTask(task: TaskMetadataWithFunctions, payload: any, signal?: AbortSignal) {
1669+
function executeTask(
1670+
task: TaskMetadataWithFunctions,
1671+
payload: any,
1672+
signal?: AbortSignal,
1673+
retrySettings?: RetryOptions
1674+
) {
16271675
const tracingSDK = new TracingSDK({
16281676
url: "http://localhost:4318",
16291677
});
@@ -1643,7 +1691,7 @@ function executeTask(task: TaskMetadataWithFunctions, payload: any, signal?: Abo
16431691
consoleInterceptor,
16441692
retries: {
16451693
enabledInDev: false,
1646-
default: {
1694+
default: retrySettings ?? {
16471695
maxAttempts: 1,
16481696
},
16491697
},
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { logger, task } from "@trigger.dev/sdk";
2+
3+
type RetryPayload = {
4+
failCount: number;
5+
};
6+
7+
export const retryTask = task({
8+
id: "retry-task",
9+
// Configure 5 retries with exponential backoff
10+
retry: {
11+
maxAttempts: 5,
12+
factor: 1.8,
13+
minTimeoutInMs: 20,
14+
maxTimeoutInMs: 100,
15+
randomize: false,
16+
},
17+
run: async (payload: RetryPayload, { ctx }) => {
18+
const currentAttempt = ctx.attempt.number;
19+
logger.info("Running retry task", {
20+
currentAttempt,
21+
desiredFailCount: payload.failCount,
22+
});
23+
24+
// If we haven't reached the desired number of failures yet, throw an error
25+
if (currentAttempt <= payload.failCount) {
26+
const error = new Error(`Intentionally failing attempt ${currentAttempt}`);
27+
logger.warn("Task failing", { error, currentAttempt });
28+
throw error;
29+
}
30+
31+
// If we've made it past the desired fail count, return success
32+
logger.info("Task succeeded", { currentAttempt });
33+
return {
34+
attemptsTaken: currentAttempt,
35+
};
36+
},
37+
});

0 commit comments

Comments
 (0)