Skip to content

Commit 2609389

Browse files
authored
v3: Fixes for using (batch)triggerAndWait with idempotency keys (#1043)
* Fixes various issues with triggerAndWait and batchTriggerAndWait When using idempotency keys, triggerAndWait and batchTriggerAndWait will still work even if the existing runs have already been completed (or even partially completed, in the case of batchTriggerAndWait) - TaskRunExecutionResult.id is now the run friendlyId, not the attempt friendlyId - A single TaskRun can now have many batchItems, in the case of batchTriggerAndWait while using idempotency keys - A run’s idempotencyKey is now added to the ctx as well as the TaskEvent and displayed in the span view - When resolving batchTriggerAndWait, the runtimes no longer reject promises, leading to an error in the parent task * Remove the default queue concurrency limit as we now have env and org concurrency limits * Use the run friendlyId in the completion result id * Added some error logging
1 parent e7bd1ee commit 2609389

File tree

47 files changed

+719
-143
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+719
-143
lines changed

.changeset/sharp-emus-compare.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"trigger.dev": patch
4+
"@trigger.dev/core": patch
5+
---
6+
7+
When using idempotency keys, triggerAndWait and batchTriggerAndWait will still work even if the existing runs have already been completed (or even partially completed, in the case of batchTriggerAndWait)
8+
9+
- TaskRunExecutionResult.id is now the run friendlyId, not the attempt friendlyId
10+
- A single TaskRun can now have many batchItems, in the case of batchTriggerAndWait while using idempotency keys
11+
- A run’s idempotencyKey is now added to the ctx as well as the TaskEvent and displayed in the span view
12+
- When resolving batchTriggerAndWait, the runtimes no longer reject promises, leading to an error in the parent task
13+

apps/webapp/app/components/runs/RunFilters.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
import { TimeFrameFilter } from "./TimeFrameFilter";
3333
import { Button } from "../primitives/Buttons";
3434
import { useCallback } from "react";
35+
import assertNever from "assert-never";
3536

3637
export function RunsFilters() {
3738
const navigate = useNavigate();
@@ -182,8 +183,7 @@ export function FilterStatusIcon({
182183
case "FAILED":
183184
return <XCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
184185
default: {
185-
const _exhaustiveCheck: never = status;
186-
throw new Error(`Non-exhaustive match for value: ${status}`);
186+
assertNever(status);
187187
}
188188
}
189189
}
@@ -205,8 +205,7 @@ export function filterStatusTitle(status: FilterableStatus): string {
205205
case "TIMEDOUT":
206206
return "Timed out";
207207
default: {
208-
const _exhaustiveCheck: never = status;
209-
throw new Error(`Non-exhaustive match for value: ${status}`);
208+
assertNever(status);
210209
}
211210
}
212211
}
@@ -228,8 +227,7 @@ export function filterStatusClassNameColor(status: FilterableStatus): string {
228227
case "TIMEDOUT":
229228
return "text-amber-300";
230229
default: {
231-
const _exhaustiveCheck: never = status;
232-
throw new Error(`Non-exhaustive match for value: ${status}`);
230+
assertNever(status);
233231
}
234232
}
235233
}

apps/webapp/app/components/runs/RunStatuses.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { JobRunStatus } from "@trigger.dev/database";
1111
import { cn } from "~/utils/cn";
1212
import { Spinner } from "../primitives/Spinner";
1313
import { z } from "zod";
14+
import assertNever from "assert-never";
1415

1516
export function RunStatus({ status }: { status: JobRunStatus }) {
1617
return (
@@ -51,8 +52,7 @@ export function RunStatusIcon({ status, className }: { status: JobRunStatus; cla
5152
case "CANCELED":
5253
return <NoSymbolIcon className={cn(runStatusClassNameColor(status), className)} />;
5354
default: {
54-
const _exhaustiveCheck: never = status;
55-
throw new Error(`Non-exhaustive match for value: ${status}`);
55+
assertNever(status);
5656
}
5757
}
5858
}
@@ -89,8 +89,7 @@ export function runStatusTitle(status: JobRunStatus): string {
8989
case "INVALID_PAYLOAD":
9090
return "Invalid payload";
9191
default: {
92-
const _exhaustiveCheck: never = status;
93-
throw new Error(`Non-exhaustive match for value: ${status}`);
92+
assertNever(status);
9493
}
9594
}
9695
}
@@ -123,8 +122,7 @@ export function runStatusClassNameColor(status: JobRunStatus): string {
123122
case "CANCELED":
124123
return "text-charcoal-500";
125124
default: {
126-
const _exhaustiveCheck: never = status;
127-
throw new Error(`Non-exhaustive match for value: ${status}`);
125+
assertNever(status);
128126
}
129127
}
130128
}

apps/webapp/app/components/runs/v3/DeploymentStatus.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
XCircleIcon,
66
} from "@heroicons/react/20/solid";
77
import { WorkerDeploymentStatus } from "@trigger.dev/database";
8+
import assertNever from "assert-never";
89
import { Spinner } from "~/components/primitives/Spinner";
910
import { cn } from "~/utils/cn";
1011

@@ -54,8 +55,7 @@ export function DeploymentStatusIcon({
5455
/>
5556
);
5657
default: {
57-
const _exhaustiveCheck: never = status;
58-
throw new Error(`Non-exhaustive match for value: ${status}`);
58+
assertNever(status);
5959
}
6060
}
6161
}
@@ -74,8 +74,7 @@ export function deploymentStatusClassNameColor(status: WorkerDeploymentStatus):
7474
case "FAILED":
7575
return "text-error";
7676
default: {
77-
const _exhaustiveCheck: never = status;
78-
throw new Error(`Non-exhaustive match for value: ${status}`);
77+
assertNever(status);
7978
}
8079
}
8180
}
@@ -97,8 +96,7 @@ export function deploymentStatusTitle(status: WorkerDeploymentStatus): string {
9796
case "FAILED":
9897
return "Failed";
9998
default: {
100-
const _exhaustiveCheck: never = status;
101-
throw new Error(`Non-exhaustive match for value: ${status}`);
99+
assertNever(status);
102100
}
103101
}
104102
}

apps/webapp/app/components/runs/v3/TaskRunAttemptStatus.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "@heroicons/react/20/solid";
99
import type { TaskRunAttemptStatus as TaskRunAttemptStatusType } from "@trigger.dev/database";
1010
import { TaskRunAttemptStatus } from "@trigger.dev/database";
11+
import assertNever from "assert-never";
1112
import { SnowflakeIcon } from "lucide-react";
1213
import { Spinner } from "~/components/primitives/Spinner";
1314
import { cn } from "~/utils/cn";
@@ -72,8 +73,7 @@ export function TaskRunAttemptStatusIcon({
7273
case "COMPLETED":
7374
return <CheckCircleIcon className={cn(runAttemptStatusClassNameColor(status), className)} />;
7475
default: {
75-
const _exhaustiveCheck: never = status;
76-
throw new Error(`Non-exhaustive match for value: ${status}`);
76+
assertNever(status);
7777
}
7878
}
7979
}
@@ -99,8 +99,7 @@ export function runAttemptStatusClassNameColor(status: ExtendedTaskAttemptStatus
9999
case "COMPLETED":
100100
return "text-success";
101101
default: {
102-
const _exhaustiveCheck: never = status;
103-
throw new Error(`Non-exhaustive match for value: ${status}`);
102+
assertNever(status);
104103
}
105104
}
106105
}
@@ -126,8 +125,7 @@ export function runAttemptStatusTitle(status: ExtendedTaskAttemptStatus | null):
126125
case "COMPLETED":
127126
return "Completed";
128127
default: {
129-
const _exhaustiveCheck: never = status;
130-
throw new Error(`Non-exhaustive match for value: ${status}`);
128+
assertNever(status);
131129
}
132130
}
133131
}

apps/webapp/app/components/runs/v3/TaskRunStatus.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
XCircleIcon,
1111
} from "@heroicons/react/20/solid";
1212
import { TaskRunStatus } from "@trigger.dev/database";
13+
import assertNever from "assert-never";
1314
import { SnowflakeIcon } from "lucide-react";
1415
import { Spinner } from "~/components/primitives/Spinner";
1516
import { cn } from "~/utils/cn";
@@ -88,8 +89,7 @@ export function TaskRunStatusIcon({
8889
return <FireIcon className={cn(runStatusClassNameColor(status), className)} />;
8990

9091
default: {
91-
const _exhaustiveCheck: never = status;
92-
throw new Error(`Non-exhaustive match for value: ${status}`);
92+
assertNever(status);
9393
}
9494
}
9595
}
@@ -120,8 +120,7 @@ export function runStatusClassNameColor(status: TaskRunStatus): string {
120120
case "CRASHED":
121121
return "text-error";
122122
default: {
123-
const _exhaustiveCheck: never = status;
124-
throw new Error(`Non-exhaustive match for value: ${status}`);
123+
assertNever(status);
125124
}
126125
}
127126
}
@@ -153,8 +152,7 @@ export function runStatusTitle(status: TaskRunStatus): string {
153152
case "CRASHED":
154153
return "Crashed";
155154
default: {
156-
const _exhaustiveCheck: never = status;
157-
throw new Error(`Non-exhaustive match for value: ${status}`);
155+
assertNever(status);
158156
}
159157
}
160158
}

apps/webapp/app/env.server.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ const EnvironmentSchema = z.object({
7676
REDIS_PASSWORD: z.string().optional(),
7777
REDIS_TLS_DISABLED: z.string().optional(),
7878

79-
DEFAULT_QUEUE_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(5),
8079
DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(10),
8180
DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(10),
8281
DEFAULT_DEV_ENV_EXECUTION_ATTEMPTS: z.coerce.number().int().positive().default(1),
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import {
2+
TaskRunError,
3+
TaskRunExecutionResult,
4+
TaskRunFailedExecutionResult,
5+
TaskRunSuccessfulExecutionResult,
6+
} from "@trigger.dev/core/v3";
7+
import {
8+
BatchTaskRunItemStatus,
9+
TaskRun,
10+
TaskRunAttempt,
11+
TaskRunAttemptStatus,
12+
TaskRunStatus,
13+
} from "@trigger.dev/database";
14+
import { assertNever } from "assert-never";
15+
import { logger } from "~/services/logger.server";
16+
17+
const SUCCESSFUL_STATUSES = [TaskRunStatus.COMPLETED_SUCCESSFULLY];
18+
const FAILURE_STATUSES = [
19+
TaskRunStatus.CANCELED,
20+
TaskRunStatus.INTERRUPTED,
21+
TaskRunStatus.COMPLETED_WITH_ERRORS,
22+
TaskRunStatus.SYSTEM_FAILURE,
23+
TaskRunStatus.CRASHED,
24+
];
25+
26+
export type TaskRunWithAttempts = TaskRun & {
27+
attempts: TaskRunAttempt[];
28+
};
29+
30+
export function executionResultForTaskRun(
31+
taskRun: TaskRunWithAttempts
32+
): TaskRunExecutionResult | undefined {
33+
if (SUCCESSFUL_STATUSES.includes(taskRun.status)) {
34+
// find the last attempt that was successful
35+
const attempt = taskRun.attempts.find((a) => a.status === TaskRunAttemptStatus.COMPLETED);
36+
37+
if (!attempt) {
38+
logger.error("Task run is successful but no successful attempt found", {
39+
taskRunId: taskRun.id,
40+
taskRunStatus: taskRun.status,
41+
taskRunAttempts: taskRun.attempts.map((a) => a.status),
42+
});
43+
44+
return undefined;
45+
}
46+
47+
return {
48+
ok: true,
49+
id: taskRun.friendlyId,
50+
output: attempt.output ?? undefined,
51+
outputType: attempt.outputType,
52+
} satisfies TaskRunSuccessfulExecutionResult;
53+
}
54+
55+
if (FAILURE_STATUSES.includes(taskRun.status)) {
56+
if (taskRun.status === TaskRunStatus.CANCELED) {
57+
return {
58+
ok: false,
59+
id: taskRun.friendlyId,
60+
error: {
61+
type: "INTERNAL_ERROR",
62+
code: "TASK_RUN_CANCELLED",
63+
},
64+
} satisfies TaskRunFailedExecutionResult;
65+
}
66+
67+
const attempt = taskRun.attempts.find((a) => a.status === TaskRunAttemptStatus.FAILED);
68+
69+
if (!attempt) {
70+
logger.error("Task run is failed but no failed attempt found", {
71+
taskRunId: taskRun.id,
72+
taskRunStatus: taskRun.status,
73+
taskRunAttempts: taskRun.attempts.map((a) => a.status),
74+
});
75+
76+
return undefined;
77+
}
78+
79+
const error = TaskRunError.safeParse(attempt.error);
80+
81+
if (!error.success) {
82+
logger.error("Failed to parse error from failed task run attempt", {
83+
taskRunId: taskRun.id,
84+
taskRunStatus: taskRun.status,
85+
taskRunAttempts: taskRun.attempts.map((a) => a.status),
86+
error: attempt.error,
87+
});
88+
89+
return {
90+
ok: false,
91+
id: taskRun.friendlyId,
92+
error: {
93+
type: "INTERNAL_ERROR",
94+
code: "CONFIGURED_INCORRECTLY",
95+
},
96+
} satisfies TaskRunFailedExecutionResult;
97+
}
98+
99+
return {
100+
ok: false,
101+
id: taskRun.friendlyId,
102+
error: error.data,
103+
} satisfies TaskRunFailedExecutionResult;
104+
}
105+
}
106+
107+
export function batchTaskRunItemStatusForRunStatus(status: TaskRunStatus): BatchTaskRunItemStatus {
108+
switch (status) {
109+
case TaskRunStatus.COMPLETED_SUCCESSFULLY:
110+
return BatchTaskRunItemStatus.COMPLETED;
111+
case TaskRunStatus.CANCELED:
112+
case TaskRunStatus.INTERRUPTED:
113+
case TaskRunStatus.COMPLETED_WITH_ERRORS:
114+
case TaskRunStatus.SYSTEM_FAILURE:
115+
case TaskRunStatus.CRASHED:
116+
case TaskRunStatus.COMPLETED_WITH_ERRORS:
117+
return BatchTaskRunItemStatus.FAILED;
118+
case TaskRunStatus.PENDING:
119+
case TaskRunStatus.WAITING_FOR_DEPLOY:
120+
case TaskRunStatus.WAITING_TO_RESUME:
121+
case TaskRunStatus.RETRYING_AFTER_FAILURE:
122+
case TaskRunStatus.EXECUTING:
123+
case TaskRunStatus.PAUSED:
124+
return BatchTaskRunItemStatus.PENDING;
125+
default:
126+
assertNever(status);
127+
}
128+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { BatchTaskRunExecutionResult } from "@trigger.dev/core/v3";
2+
import { executionResultForTaskRun } from "~/models/taskRun.server";
3+
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
4+
import { BasePresenter } from "./basePresenter.server";
5+
6+
export class ApiBatchResultsPresenter extends BasePresenter {
7+
public async call(
8+
friendlyId: string,
9+
env: AuthenticatedEnvironment
10+
): Promise<BatchTaskRunExecutionResult | undefined> {
11+
return this.traceWithEnv("call", env, async (span) => {
12+
const batchRun = await this._prisma.batchTaskRun.findUnique({
13+
where: {
14+
friendlyId,
15+
runtimeEnvironmentId: env.id,
16+
},
17+
include: {
18+
items: {
19+
include: {
20+
taskRun: {
21+
include: {
22+
attempts: {
23+
orderBy: {
24+
createdAt: "desc",
25+
},
26+
},
27+
},
28+
},
29+
},
30+
},
31+
},
32+
});
33+
34+
if (!batchRun) {
35+
return undefined;
36+
}
37+
38+
return {
39+
id: batchRun.friendlyId,
40+
items: batchRun.items
41+
.map((item) => executionResultForTaskRun(item.taskRun))
42+
.filter(Boolean),
43+
};
44+
});
45+
}
46+
}

0 commit comments

Comments
 (0)