Skip to content

Commit 464902d

Browse files
committed
add try and resume action for in-progress batches
1 parent f7bf7bc commit 464902d

File tree

5 files changed

+217
-15
lines changed

5 files changed

+217
-15
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { DialogClose } from "@radix-ui/react-dialog";
2+
import { Form, useNavigation } from "@remix-run/react";
3+
import { Button } from "~/components/primitives/Buttons";
4+
import { DialogContent, DialogHeader } from "~/components/primitives/Dialog";
5+
import { FormButtons } from "~/components/primitives/FormButtons";
6+
import { Paragraph } from "~/components/primitives/Paragraph";
7+
8+
type CheckBatchCompletionDialogProps = {
9+
batchId: string;
10+
redirectPath: string;
11+
};
12+
13+
export function CheckBatchCompletionDialog({
14+
batchId,
15+
redirectPath,
16+
}: CheckBatchCompletionDialogProps) {
17+
const navigation = useNavigation();
18+
19+
const formAction = `/resources/batches/${batchId}/check-completion`;
20+
const isLoading = navigation.formAction === formAction;
21+
22+
return (
23+
<DialogContent key="check-completion">
24+
<DialogHeader>Try and resume batch</DialogHeader>
25+
<div className="flex flex-col gap-3 pt-3">
26+
<Paragraph>
27+
In rare cases, parent runs don't continue after child runs have completed.
28+
</Paragraph>
29+
<Paragraph>
30+
If this doesn't help, please get in touch. We are working on a permanent fix for this.
31+
</Paragraph>
32+
<FormButtons
33+
confirmButton={
34+
<Form action={`/resources/batches/${batchId}/check-completion`} method="post">
35+
<Button
36+
type="submit"
37+
name="redirectUrl"
38+
value={redirectPath}
39+
variant="primary/medium"
40+
LeadingIcon={isLoading ? "spinner-white" : undefined}
41+
disabled={isLoading}
42+
shortcut={{ modifiers: ["meta"], key: "enter" }}
43+
>
44+
{isLoading ? "Attempting resume..." : "Attempt resume"}
45+
</Button>
46+
</Form>
47+
}
48+
cancelButton={
49+
<DialogClose asChild>
50+
<Button variant={"tertiary/medium"}>Cancel</Button>
51+
</DialogClose>
52+
}
53+
/>
54+
</div>
55+
</DialogContent>
56+
);
57+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.batches/route.tsx

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
1-
import { ExclamationCircleIcon } from "@heroicons/react/20/solid";
1+
import { ArrowPathRoundedSquareIcon, ArrowRightIcon, CheckIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid";
22
import { BookOpenIcon } from "@heroicons/react/24/solid";
3-
import { useNavigation } from "@remix-run/react";
3+
import { useLocation, useNavigation } from "@remix-run/react";
44
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
55
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
66
import { typedjson, useTypedLoaderData } from "remix-typedjson";
77
import { ListPagination } from "~/components/ListPagination";
88
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
99
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
1010
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
11-
import { LinkButton } from "~/components/primitives/Buttons";
11+
import { Button, LinkButton } from "~/components/primitives/Buttons";
1212
import { DateTime } from "~/components/primitives/DateTime";
13+
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
1314
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
1415
import { Paragraph } from "~/components/primitives/Paragraph";
16+
import { PopoverMenuItem } from "~/components/primitives/Popover";
1517
import { Spinner } from "~/components/primitives/Spinner";
1618
import {
1719
Table,
1820
TableBlankRow,
1921
TableBody,
2022
TableCell,
23+
TableCellMenu,
2124
TableHeader,
2225
TableHeaderCell,
2326
TableRow,
@@ -29,12 +32,17 @@ import {
2932
BatchStatusCombo,
3033
descriptionForBatchStatus,
3134
} from "~/components/runs/v3/BatchStatus";
35+
import { CheckBatchCompletionDialog } from "~/components/runs/v3/CheckBatchCompletionDialog";
3236
import { LiveTimer } from "~/components/runs/v3/LiveTimer";
3337
import { useOrganization } from "~/hooks/useOrganizations";
3438
import { useProject } from "~/hooks/useProject";
3539
import { redirectWithErrorMessage } from "~/models/message.server";
3640
import { findProjectBySlug } from "~/models/project.server";
37-
import { BatchList, BatchListPresenter } from "~/presenters/v3/BatchListPresenter.server";
41+
import {
42+
BatchList,
43+
BatchListItem,
44+
BatchListPresenter,
45+
} from "~/presenters/v3/BatchListPresenter.server";
3846
import { requireUserId } from "~/services/session.server";
3947
import { docsPath, ProjectParamSchema, v3BatchRunsPath } from "~/utils/pathBuilder";
4048

@@ -150,19 +158,22 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
150158
<TableHeaderCell>Duration</TableHeaderCell>
151159
<TableHeaderCell>Created</TableHeaderCell>
152160
<TableHeaderCell>Finished</TableHeaderCell>
161+
<TableHeaderCell>
162+
<span className="sr-only">Go to batch</span>
163+
</TableHeaderCell>
153164
</TableRow>
154165
</TableHeader>
155166
<TableBody>
156167
{batches.length === 0 && !hasFilters ? (
157-
<TableBlankRow colSpan={7}>
168+
<TableBlankRow colSpan={8}>
158169
{!isLoading && (
159170
<div className="flex items-center justify-center">
160171
<Paragraph className="w-auto">No batches</Paragraph>
161172
</div>
162173
)}
163174
</TableBlankRow>
164175
) : batches.length === 0 ? (
165-
<TableBlankRow colSpan={7}>
176+
<TableBlankRow colSpan={8}>
166177
<div className="flex items-center justify-center">
167178
<Paragraph className="w-auto">No batches match these filters</Paragraph>
168179
</div>
@@ -215,13 +226,14 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
215226
<TableCell to={path}>
216227
{batch.finishedAt ? <DateTime date={batch.finishedAt} /> : "–"}
217228
</TableCell>
229+
<BatchActionsCell batch={batch} path={path} />
218230
</TableRow>
219231
);
220232
})
221233
)}
222234
{isLoading && (
223235
<TableBlankRow
224-
colSpan={7}
236+
colSpan={8}
225237
className="absolute left-0 top-0 flex h-full w-full items-center justify-center gap-2 bg-charcoal-900/90"
226238
>
227239
<Spinner /> <span className="text-text-dimmed">Loading…</span>
@@ -231,3 +243,50 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
231243
</Table>
232244
);
233245
}
246+
247+
function BatchActionsCell({ batch, path }: { batch: BatchListItem; path: string }) {
248+
const location = useLocation();
249+
250+
const isPending = batch.status === "PENDING";
251+
252+
if (!isPending) return <TableCell to={path}>{""}</TableCell>;
253+
254+
return (
255+
<TableCellMenu
256+
isSticky
257+
popoverContent={
258+
<>
259+
<PopoverMenuItem
260+
to={path}
261+
icon={ArrowRightIcon}
262+
leadingIconClassName="text-blue-500"
263+
title="View batch"
264+
/>
265+
{isPending && (
266+
<Dialog>
267+
<DialogTrigger
268+
asChild
269+
className="size-6 rounded-sm p-1 text-text-dimmed transition hover:bg-charcoal-700 hover:text-text-bright"
270+
>
271+
<Button
272+
variant="small-menu-item"
273+
LeadingIcon={ArrowPathRoundedSquareIcon}
274+
leadingIconClassName="text-success"
275+
fullWidth
276+
textAlignLeft
277+
className="w-full px-1.5 py-[0.9rem]"
278+
>
279+
Try and resume
280+
</Button>
281+
</DialogTrigger>
282+
<CheckBatchCompletionDialog
283+
batchId={batch.id}
284+
redirectPath={`${location.pathname}${location.search}`}
285+
/>
286+
</Dialog>
287+
)}
288+
</>
289+
}
290+
/>
291+
);
292+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { parse } from "@conform-to/zod";
2+
import { ActionFunction, json } from "@remix-run/node";
3+
import { assertExhaustive } from "@trigger.dev/core";
4+
import { z } from "zod";
5+
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
6+
import { logger } from "~/services/logger.server";
7+
import { ResumeBatchRunService } from "~/v3/services/resumeBatchRun.server";
8+
9+
export const checkCompletionSchema = z.object({
10+
redirectUrl: z.string(),
11+
});
12+
13+
const ParamSchema = z.object({
14+
batchId: z.string(),
15+
});
16+
17+
export const action: ActionFunction = async ({ request, params }) => {
18+
const { batchId } = ParamSchema.parse(params);
19+
20+
const formData = await request.formData();
21+
const submission = parse(formData, { schema: checkCompletionSchema });
22+
23+
if (!submission.value) {
24+
return json(submission);
25+
}
26+
27+
try {
28+
const resumeBatchRunService = new ResumeBatchRunService();
29+
const resumeResult = await resumeBatchRunService.call(batchId);
30+
31+
let message: string | undefined;
32+
33+
switch (resumeResult) {
34+
case "ERROR": {
35+
throw "Unknown error during batch completion check";
36+
}
37+
case "ALREADY_COMPLETED": {
38+
message = "Batch already completed.";
39+
break;
40+
}
41+
case "COMPLETED": {
42+
message = "Batch completed and parent tasks resumed.";
43+
break;
44+
}
45+
case "PENDING": {
46+
message = "Child runs still in progress. Please try again later.";
47+
break;
48+
}
49+
default: {
50+
assertExhaustive(resumeResult);
51+
}
52+
}
53+
54+
return redirectWithSuccessMessage(submission.value.redirectUrl, request, message);
55+
} catch (error) {
56+
if (error instanceof Error) {
57+
logger.error("Failed to check batch completion", {
58+
error: {
59+
name: error.name,
60+
message: error.message,
61+
stack: error.stack,
62+
},
63+
});
64+
return redirectWithErrorMessage(submission.value.redirectUrl, request, error.message);
65+
} else {
66+
logger.error("Failed to check batch completion", { error });
67+
return redirectWithErrorMessage(submission.value.redirectUrl, request, "Unknown error");
68+
}
69+
}
70+
};

apps/webapp/app/services/worker.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ function getWorkerQueue() {
563563
handler: async (payload, job) => {
564564
const service = new ResumeBatchRunService();
565565

566-
return await service.call(payload.batchRunId);
566+
await service.call(payload.batchRunId);
567567
},
568568
},
569569
"v3.resumeTaskDependency": {

0 commit comments

Comments
 (0)