Canceling runs

This commit is contained in:
Matt Aitken
2023-07-11 19:50:06 +01:00
parent 4e4e7b705d
commit 4661d2019c
8 changed files with 161 additions and 3 deletions
+49 -1
View File
@@ -1,11 +1,19 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { BoltIcon, ForwardIcon } from "@heroicons/react/24/solid";
import { Form, Outlet, useActionData, useNavigate } from "@remix-run/react";
import {
Form,
Outlet,
useActionData,
useLocation,
useNavigate,
useNavigation,
} from "@remix-run/react";
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
import { useMemo } from "react";
import { usePathName } from "~/hooks/usePathName";
import { Run } from "~/presenters/RunPresenter.server";
import { cancelSchema } from "~/routes/resources.runs.$runId.cancel";
import { schema } from "~/routes/resources.runs.$runId.rerun";
import { formatDuration } from "~/utils";
import { cn } from "~/utils/cn";
@@ -123,6 +131,7 @@ export function RunOverview({
status={basicStatus}
/>
)}
{!hasFinished(run.status) && <CancelRun runId={run.id} />}
</PageButtons>
</PageTitleRow>
<PageInfoRow>
@@ -419,3 +428,42 @@ function RerunPopover({
</Popover>
);
}
export function CancelRun({ runId }: { runId: string }) {
const lastSubmission = useActionData();
const location = useLocation();
const navigation = useNavigation();
const [form, { redirectUrl }] = useForm({
id: "cancel-run",
lastSubmission,
onValidate({ formData }) {
return parse(formData, { schema: cancelSchema });
},
});
const isLoading =
navigation.state === "submitting" && navigation.formData !== undefined;
return (
<Form
method="post"
action={`/resources/runs/${runId}/cancel`}
{...form.props}
>
<input
{...conform.input(redirectUrl, { type: "hidden" })}
defaultValue={location.pathname}
/>
<Button
type="submit"
LeadingIcon={isLoading ? "spinner-white" : "stop"}
leadingIconClassName="text-white"
variant="danger/small"
disabled={isLoading}
>
{isLoading ? "Canceling" : "Cancel run"}
</Button>
</Form>
);
}
@@ -3,18 +3,21 @@ import {
CheckCircleIcon,
ClockIcon,
ExclamationTriangleIcon,
StopIcon,
WrenchIcon,
XCircleIcon,
} from "@heroicons/react/24/solid";
import { cn } from "~/utils/cn";
import { Spinner } from "../primitives/Spinner";
import { HandRaisedIcon, NoSymbolIcon } from "@heroicons/react/20/solid";
export function hasFinished(status: JobRunStatus): boolean {
return (
status === "SUCCESS" ||
status === "FAILURE" ||
status === "ABORTED" ||
status === "TIMED_OUT"
status === "TIMED_OUT" ||
status === "CANCELED"
);
}
@@ -89,6 +92,12 @@ export function RunStatusIcon({
return (
<Spinner className={cn(runStatusClassNameColor(status), className)} />
);
case "CANCELED":
return (
<NoSymbolIcon
className={cn(runStatusClassNameColor(status), className)}
/>
);
}
}
@@ -119,6 +128,8 @@ export function runBasicStatus(status: JobRunStatus): RunBasicStatus {
return "FAILED";
case "PREPROCESSING":
return "PENDING";
case "CANCELED":
return "FAILED";
}
}
@@ -142,6 +153,8 @@ export function runStatusTitle(status: JobRunStatus): string {
return "Aborted";
case "PREPROCESSING":
return "Preprocessing";
case "CANCELED":
return "Canceled";
}
}
@@ -165,5 +178,7 @@ export function runStatusClassNameColor(status: JobRunStatus): string {
return "text-rose-500";
case "PREPROCESSING":
return "text-blue-500";
case "CANCELED":
return "text-slate-500";
}
}
@@ -192,6 +192,18 @@ export class RunTaskService {
return existingTask;
}
const run = await tx.jobRun.findUnique({
where: {
id: runId,
},
select: {
status: true,
},
});
if (!run) throw new Error("Run not found");
if (run.status === "CANCELED") return;
// If task.delayUntil is set and is in the future, we'll set the task's status to "WAITING", else set it to RUNNING
const status =
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) ||
@@ -0,0 +1,37 @@
import { parse } from "@conform-to/zod";
import { ActionFunction, json } from "@remix-run/node";
import { z } from "zod";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { CancelRunService } from "~/services/runs/cancelRun.server";
export const cancelSchema = z.object({
redirectUrl: z.string(),
});
const ParamSchema = z.object({
runId: z.string(),
});
export const action: ActionFunction = async ({ request, params }) => {
const { runId } = ParamSchema.parse(params);
const formData = await request.formData();
const submission = parse(formData, { schema: cancelSchema });
if (!submission.value) {
return json(submission);
}
try {
const cancelRunService = new CancelRunService();
await cancelRunService.call({ runId });
return redirectWithSuccessMessage(
submission.value.redirectUrl,
request,
`Canceled run. Any pending tasks will be canceled.`
);
} catch (error: any) {
return json({ errors: { body: error.message } }, { status: 400 });
}
};
@@ -0,0 +1,43 @@
import { PrismaClient, prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
export class CancelRunService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({ runId }: { runId: string }) {
try {
return await this.#prismaClient.$transaction(async (tx) => {
const run = await tx.jobRun.update({
select: {
queueId: true,
},
where: { id: runId },
data: {
status: "CANCELED",
queue: {
update: {
jobCount: {
decrement: 1,
},
},
},
},
});
await workerQueue.enqueue(
"startQueuedRuns",
{
id: run.queueId,
},
{ tx }
);
});
} catch (error) {
throw error;
}
}
}
@@ -2,7 +2,7 @@ import { EXECUTE_JOB_RETRY_LIMIT } from "~/consts";
import { $transaction, Prisma, PrismaClient, prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
const RESUMABLE_STATUSES = ["FAILURE", "TIMED_OUT", "ABORTED"];
const RESUMABLE_STATUSES = ["FAILURE", "TIMED_OUT", "ABORTED", "CANCELED"];
export class ContinueRunService {
#prismaClient: PrismaClient;
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "JobRunStatus" ADD VALUE 'CANCELED';
+1
View File
@@ -709,6 +709,7 @@ enum JobRunStatus {
FAILURE
TIMED_OUT
ABORTED
CANCELED
}
model JobRunExecution {