Skip to content

Commit 0e77e7e

Browse files
authored
v3: Delayed runs and run ttl expiration (#1193)
* v3: Trigger delayed runs and reschedule them * Create a `@trigger.dev/core/v3/schemas` export * fixed the `@trigger.dev/core/v3/schemas` export * Small docs tweak * Add ttl option when triggering tasks, expire runs after ttl Dev runs expire in 10m by default
1 parent 76a5c62 commit 0e77e7e

34 files changed

+930
-90
lines changed

.changeset/modern-stingrays-end.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
v3: Trigger delayed runs and reschedule them

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import {
33
BoltSlashIcon,
44
BugAntIcon,
55
CheckCircleIcon,
6+
ClockIcon,
67
FireIcon,
78
NoSymbolIcon,
89
PauseCircleIcon,
910
RectangleStackIcon,
11+
TrashIcon,
1012
XCircleIcon,
1113
} from "@heroicons/react/20/solid";
1214
import { TaskRunStatus } from "@trigger.dev/database";
@@ -16,6 +18,7 @@ import { Spinner } from "~/components/primitives/Spinner";
1618
import { cn } from "~/utils/cn";
1719

1820
export const allTaskRunStatuses = [
21+
"DELAYED",
1922
"WAITING_FOR_DEPLOY",
2023
"PENDING",
2124
"EXECUTING",
@@ -28,10 +31,12 @@ export const allTaskRunStatuses = [
2831
"PAUSED",
2932
"INTERRUPTED",
3033
"SYSTEM_FAILURE",
34+
"EXPIRED",
3135
] as const satisfies Readonly<Array<TaskRunStatus>>;
3236

3337
export const filterableTaskRunStatuses = [
3438
"WAITING_FOR_DEPLOY",
39+
"DELAYED",
3540
"PENDING",
3641
"EXECUTING",
3742
"RETRYING_AFTER_FAILURE",
@@ -42,9 +47,11 @@ export const filterableTaskRunStatuses = [
4247
"CRASHED",
4348
"INTERRUPTED",
4449
"SYSTEM_FAILURE",
50+
"EXPIRED",
4551
] as const satisfies Readonly<Array<TaskRunStatus>>;
4652

4753
const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
54+
DELAYED: "Task has been delayed and is waiting to be executed",
4855
PENDING: "Task is waiting to be executed",
4956
WAITING_FOR_DEPLOY: "Task needs to be deployed first to start executing",
5057
EXECUTING: "Task is currently being executed",
@@ -57,9 +64,10 @@ const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
5764
SYSTEM_FAILURE: "Task has failed due to a system failure",
5865
PAUSED: "Task has been paused by the user",
5966
CRASHED: "Task has crashed and won't be retried",
67+
EXPIRED: "Task has surpassed its ttl and won't be executed",
6068
};
6169

62-
export const QUEUED_STATUSES: TaskRunStatus[] = ["PENDING", "WAITING_FOR_DEPLOY"];
70+
export const QUEUED_STATUSES: TaskRunStatus[] = ["PENDING", "WAITING_FOR_DEPLOY", "DELAYED"];
6371

6472
export const RUNNING_STATUSES: TaskRunStatus[] = [
6573
"EXECUTING",
@@ -74,6 +82,7 @@ export const FINISHED_STATUSES: TaskRunStatus[] = [
7482
"INTERRUPTED",
7583
"SYSTEM_FAILURE",
7684
"CRASHED",
85+
"EXPIRED",
7786
];
7887

7988
export function descriptionForTaskRunStatus(status: TaskRunStatus): string {
@@ -109,6 +118,8 @@ export function TaskRunStatusIcon({
109118
className: string;
110119
}) {
111120
switch (status) {
121+
case "DELAYED":
122+
return <ClockIcon className={cn(runStatusClassNameColor(status), className)} />;
112123
case "PENDING":
113124
return <RectangleStackIcon className={cn(runStatusClassNameColor(status), className)} />;
114125
case "WAITING_FOR_DEPLOY":
@@ -133,6 +144,8 @@ export function TaskRunStatusIcon({
133144
return <BugAntIcon className={cn(runStatusClassNameColor(status), className)} />;
134145
case "CRASHED":
135146
return <FireIcon className={cn(runStatusClassNameColor(status), className)} />;
147+
case "EXPIRED":
148+
return <TrashIcon className={cn(runStatusClassNameColor(status), className)} />;
136149

137150
default: {
138151
assertNever(status);
@@ -143,6 +156,7 @@ export function TaskRunStatusIcon({
143156
export function runStatusClassNameColor(status: TaskRunStatus): string {
144157
switch (status) {
145158
case "PENDING":
159+
case "DELAYED":
146160
return "text-charcoal-500";
147161
case "WAITING_FOR_DEPLOY":
148162
return "text-amber-500";
@@ -154,6 +168,7 @@ export function runStatusClassNameColor(status: TaskRunStatus): string {
154168
case "PAUSED":
155169
return "text-amber-300";
156170
case "CANCELED":
171+
case "EXPIRED":
157172
return "text-charcoal-500";
158173
case "INTERRUPTED":
159174
return "text-error";
@@ -173,6 +188,8 @@ export function runStatusClassNameColor(status: TaskRunStatus): string {
173188

174189
export function runStatusTitle(status: TaskRunStatus): string {
175190
switch (status) {
191+
case "DELAYED":
192+
return "Delayed";
176193
case "PENDING":
177194
return "Queued";
178195
case "WAITING_FOR_DEPLOY":
@@ -197,6 +214,8 @@ export function runStatusTitle(status: TaskRunStatus): string {
197214
return "System failure";
198215
case "CRASHED":
199216
return "Crashed";
217+
case "EXPIRED":
218+
return "Expired";
200219
default: {
201220
assertNever(status);
202221
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ export function TaskRunsTable({
118118
<TableHeaderCell>Duration</TableHeaderCell>
119119
<TableHeaderCell>Test</TableHeaderCell>
120120
<TableHeaderCell>Created at</TableHeaderCell>
121+
<TableHeaderCell>Delayed until</TableHeaderCell>
122+
<TableHeaderCell>TTL</TableHeaderCell>
121123
<TableHeaderCell>
122124
<span className="sr-only">Go to page</span>
123125
</TableHeaderCell>
@@ -187,6 +189,10 @@ export function TaskRunsTable({
187189
<TableCell to={path}>
188190
{run.createdAt ? <DateTime date={run.createdAt} /> : "–"}
189191
</TableCell>
192+
<TableCell to={path}>
193+
{run.delayUntil ? <DateTime date={run.delayUntil} /> : "–"}
194+
</TableCell>
195+
<TableCell to={path}>{run.ttl ?? "–"}</TableCell>
190196
<RunActionsCell run={run} path={path} />
191197
</TableRow>
192198
);

apps/webapp/app/database-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export const TaskRunStatus = {
4040
COMPLETED_WITH_ERRORS: "COMPLETED_WITH_ERRORS",
4141
SYSTEM_FAILURE: "SYSTEM_FAILURE",
4242
CRASHED: "CRASHED",
43+
DELAYED: "DELAYED",
44+
EXPIRED: "EXPIRED",
4345
} as const satisfies Record<TaskRunStatusType, TaskRunStatusType>;
4446

4547
export const JobRunStatus = {

apps/webapp/app/models/taskRun.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,15 @@ export function batchTaskRunItemStatusForRunStatus(
118118
case TaskRunStatus.COMPLETED_WITH_ERRORS:
119119
case TaskRunStatus.SYSTEM_FAILURE:
120120
case TaskRunStatus.CRASHED:
121+
case TaskRunStatus.EXPIRED:
121122
return BatchTaskRunItemStatus.FAILED;
122123
case TaskRunStatus.PENDING:
123124
case TaskRunStatus.WAITING_FOR_DEPLOY:
124125
case TaskRunStatus.WAITING_TO_RESUME:
125126
case TaskRunStatus.RETRYING_AFTER_FAILURE:
126127
case TaskRunStatus.EXECUTING:
127128
case TaskRunStatus.PAUSED:
129+
case TaskRunStatus.DELAYED:
128130
return BatchTaskRunItemStatus.PENDING;
129131
default:
130132
assertNever(status);

apps/webapp/app/presenters/v3/ApiRetrieveRunPresenter.server.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,14 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
111111
finishedAt: ApiRetrieveRunPresenter.isStatusFinished(apiStatus)
112112
? taskRun.updatedAt
113113
: undefined,
114+
delayedUntil: taskRun.delayUntil ?? undefined,
114115
payload: $payload,
115116
payloadPresignedUrl: $payloadPresignedUrl,
116117
output: $output,
117118
outputPresignedUrl: $outputPresignedUrl,
118119
isTest: taskRun.isTest,
120+
ttl: taskRun.ttl ?? undefined,
121+
expiredAt: taskRun.expiredAt ?? undefined,
119122
schedule: taskRun.schedule
120123
? {
121124
id: taskRun.schedule.friendlyId,
@@ -171,6 +174,9 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
171174

172175
static apiStatusFromRunStatus(status: TaskRunStatus): RunStatus {
173176
switch (status) {
177+
case "DELAYED": {
178+
return "DELAYED";
179+
}
174180
case "WAITING_FOR_DEPLOY": {
175181
return "WAITING_FOR_DEPLOY";
176182
}
@@ -205,14 +211,17 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
205211
case "COMPLETED_WITH_ERRORS": {
206212
return "FAILED";
207213
}
214+
case "EXPIRED": {
215+
return "EXPIRED";
216+
}
208217
default: {
209218
assertNever(status);
210219
}
211220
}
212221
}
213222

214223
static apiBooleanHelpersFromRunStatus(status: RunStatus) {
215-
const isQueued = status === "QUEUED" || status === "WAITING_FOR_DEPLOY";
224+
const isQueued = status === "QUEUED" || status === "WAITING_FOR_DEPLOY" || status === "DELAYED";
216225
const isExecuting = status === "EXECUTING" || status === "REATTEMPTING" || status === "FROZEN";
217226
const isCompleted =
218227
status === "COMPLETED" ||

apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,10 @@ export class ApiRunListPresenter extends BasePresenter {
209209
updatedAt: new Date(run.updatedAt),
210210
startedAt: run.startedAt ? new Date(run.startedAt) : undefined,
211211
finishedAt: run.finishedAt ? new Date(run.finishedAt) : undefined,
212+
delayedUntil: run.delayUntil ? new Date(run.delayUntil) : undefined,
212213
isTest: run.isTest,
214+
ttl: run.ttl ?? undefined,
215+
expiredAt: run.expiredAt ? new Date(run.expiredAt) : undefined,
213216
env: {
214217
id: run.environment.id,
215218
name: run.environment.slug,
@@ -233,6 +236,8 @@ export class ApiRunListPresenter extends BasePresenter {
233236

234237
static apiStatusToRunStatuses(status: RunStatus): TaskRunStatus[] | TaskRunStatus {
235238
switch (status) {
239+
case "DELAYED":
240+
return "DELAYED";
236241
case "WAITING_FOR_DEPLOY": {
237242
return "WAITING_FOR_DEPLOY";
238243
}
@@ -266,6 +271,9 @@ export class ApiRunListPresenter extends BasePresenter {
266271
case "FAILED": {
267272
return "COMPLETED_WITH_ERRORS";
268273
}
274+
case "EXPIRED": {
275+
return "EXPIRED";
276+
}
269277
default: {
270278
assertNever(status);
271279
}

apps/webapp/app/presenters/v3/RunListPresenter.server.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,13 @@ export class RunListPresenter extends BasePresenter {
158158
createdAt: Date;
159159
startedAt: Date | null;
160160
lockedAt: Date | null;
161+
delayUntil: Date | null;
161162
updatedAt: Date;
162163
isTest: boolean;
163164
spanId: string;
164165
idempotencyKey: string | null;
166+
ttl: string | null;
167+
expiredAt: Date | null;
165168
}[]
166169
>`
167170
SELECT
@@ -174,11 +177,14 @@ export class RunListPresenter extends BasePresenter {
174177
tr.status AS status,
175178
tr."createdAt" AS "createdAt",
176179
tr."startedAt" AS "startedAt",
180+
tr."delayUntil" AS "delayUntil",
177181
tr."lockedAt" AS "lockedAt",
178182
tr."updatedAt" AS "updatedAt",
179183
tr."isTest" AS "isTest",
180184
tr."spanId" AS "spanId",
181-
tr."idempotencyKey" AS "idempotencyKey"
185+
tr."idempotencyKey" AS "idempotencyKey",
186+
tr."ttl" AS "ttl",
187+
tr."expiredAt" AS "expiredAt"
182188
FROM
183189
${sqlDatabaseSchema}."TaskRun" tr
184190
LEFT JOIN
@@ -283,6 +289,7 @@ export class RunListPresenter extends BasePresenter {
283289
createdAt: run.createdAt.toISOString(),
284290
updatedAt: run.updatedAt.toISOString(),
285291
startedAt: startedAt ? startedAt.toISOString() : undefined,
292+
delayUntil: run.delayUntil ? run.delayUntil.toISOString() : undefined,
286293
hasFinished,
287294
finishedAt: hasFinished ? run.updatedAt.toISOString() : undefined,
288295
isTest: run.isTest,
@@ -294,6 +301,8 @@ export class RunListPresenter extends BasePresenter {
294301
isCancellable: isCancellableRunStatus(run.status),
295302
environment: displayableEnvironment(environment, userId),
296303
idempotencyKey: run.idempotencyKey ? run.idempotencyKey : undefined,
304+
ttl: run.ttl ? run.ttl : undefined,
305+
expiredAt: run.expiredAt ? run.expiredAt.toISOString() : undefined,
297306
};
298307
}),
299308
pagination: {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
2+
import { json } from "@remix-run/server-runtime";
3+
import { RescheduleRunRequestBody } from "@trigger.dev/core/v3/schemas";
4+
import { z } from "zod";
5+
import { prisma } from "~/db.server";
6+
import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
7+
import { authenticateApiRequest } from "~/services/apiAuth.server";
8+
import { ServiceValidationError } from "~/v3/services/baseService.server";
9+
import { RescheduleTaskRunService } from "~/v3/services/rescheduleTaskRun.server";
10+
11+
const ParamsSchema = z.object({
12+
runParam: z.string(),
13+
});
14+
15+
export async function action({ request, params }: ActionFunctionArgs) {
16+
// Ensure this is a POST request
17+
if (request.method.toUpperCase() !== "POST") {
18+
return { status: 405, body: "Method Not Allowed" };
19+
}
20+
21+
// Authenticate the request
22+
const authenticationResult = await authenticateApiRequest(request);
23+
24+
if (!authenticationResult) {
25+
return json({ error: "Invalid or missing API Key" }, { status: 401 });
26+
}
27+
28+
const parsed = ParamsSchema.safeParse(params);
29+
30+
if (!parsed.success) {
31+
return json({ error: "Invalid or missing run ID" }, { status: 400 });
32+
}
33+
34+
const { runParam } = parsed.data;
35+
36+
const taskRun = await prisma.taskRun.findUnique({
37+
where: {
38+
friendlyId: runParam,
39+
runtimeEnvironmentId: authenticationResult.environment.id,
40+
},
41+
});
42+
43+
if (!taskRun) {
44+
return json({ error: "Run not found" }, { status: 404 });
45+
}
46+
47+
const anyBody = await request.json();
48+
49+
const body = RescheduleRunRequestBody.safeParse(anyBody);
50+
51+
if (!body.success) {
52+
return json({ error: "Invalid request body" }, { status: 400 });
53+
}
54+
55+
const service = new RescheduleTaskRunService();
56+
57+
try {
58+
const updatedRun = await service.call(taskRun, body.data);
59+
60+
if (!updatedRun) {
61+
return json({ error: "An unknown error occurred" }, { status: 500 });
62+
}
63+
64+
const presenter = new ApiRetrieveRunPresenter();
65+
const result = await presenter.call(
66+
updatedRun.friendlyId,
67+
authenticationResult.environment,
68+
true
69+
);
70+
71+
if (!result) {
72+
return json({ error: "Run not found" }, { status: 404 });
73+
}
74+
75+
return json(result);
76+
} catch (error) {
77+
if (error instanceof ServiceValidationError) {
78+
return json({ error: error.message }, { status: 400 });
79+
} else if (error instanceof Error) {
80+
return json({ error: error.message }, { status: 500 });
81+
} else {
82+
return json({ error: "An unknown error occurred" }, { status: 500 });
83+
}
84+
}
85+
}

apps/webapp/app/routes/api.v2.runs.$runParam.cancel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
3333
const taskRun = await prisma.taskRun.findUnique({
3434
where: {
3535
friendlyId: runParam,
36+
runtimeEnvironmentId: authenticationResult.environment.id,
3637
},
3738
});
3839

0 commit comments

Comments
 (0)