Skip to content

Commit 29d107d

Browse files
authored
Feat: Add button to try and resume pending batches (#1529)
* add try and resume action for in-progress batches * don't reinvent the wheel
1 parent cf7dc8d commit 29d107d

File tree

5 files changed

+219
-15
lines changed

5 files changed

+219
-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: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
1-
import { ExclamationCircleIcon } from "@heroicons/react/20/solid";
1+
import {
2+
ArrowPathRoundedSquareIcon,
3+
ArrowRightIcon,
4+
ExclamationCircleIcon,
5+
} from "@heroicons/react/20/solid";
26
import { BookOpenIcon } from "@heroicons/react/24/solid";
3-
import { useNavigation } from "@remix-run/react";
7+
import { useLocation, useNavigation } from "@remix-run/react";
48
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
59
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
610
import { typedjson, useTypedLoaderData } from "remix-typedjson";
711
import { ListPagination } from "~/components/ListPagination";
812
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
913
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
1014
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
11-
import { LinkButton } from "~/components/primitives/Buttons";
15+
import { Button, LinkButton } from "~/components/primitives/Buttons";
1216
import { DateTime } from "~/components/primitives/DateTime";
17+
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
1318
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
1419
import { Paragraph } from "~/components/primitives/Paragraph";
20+
import { PopoverMenuItem } from "~/components/primitives/Popover";
1521
import { Spinner } from "~/components/primitives/Spinner";
1622
import {
1723
Table,
1824
TableBlankRow,
1925
TableBody,
2026
TableCell,
27+
TableCellMenu,
2128
TableHeader,
2229
TableHeaderCell,
2330
TableRow,
@@ -29,12 +36,17 @@ import {
2936
BatchStatusCombo,
3037
descriptionForBatchStatus,
3138
} from "~/components/runs/v3/BatchStatus";
39+
import { CheckBatchCompletionDialog } from "~/components/runs/v3/CheckBatchCompletionDialog";
3240
import { LiveTimer } from "~/components/runs/v3/LiveTimer";
3341
import { useOrganization } from "~/hooks/useOrganizations";
3442
import { useProject } from "~/hooks/useProject";
3543
import { redirectWithErrorMessage } from "~/models/message.server";
3644
import { findProjectBySlug } from "~/models/project.server";
37-
import { BatchList, BatchListPresenter } from "~/presenters/v3/BatchListPresenter.server";
45+
import {
46+
BatchList,
47+
BatchListItem,
48+
BatchListPresenter,
49+
} from "~/presenters/v3/BatchListPresenter.server";
3850
import { requireUserId } from "~/services/session.server";
3951
import { docsPath, ProjectParamSchema, v3BatchRunsPath } from "~/utils/pathBuilder";
4052

@@ -150,19 +162,22 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
150162
<TableHeaderCell>Duration</TableHeaderCell>
151163
<TableHeaderCell>Created</TableHeaderCell>
152164
<TableHeaderCell>Finished</TableHeaderCell>
165+
<TableHeaderCell>
166+
<span className="sr-only">Go to batch</span>
167+
</TableHeaderCell>
153168
</TableRow>
154169
</TableHeader>
155170
<TableBody>
156171
{batches.length === 0 && !hasFilters ? (
157-
<TableBlankRow colSpan={7}>
172+
<TableBlankRow colSpan={8}>
158173
{!isLoading && (
159174
<div className="flex items-center justify-center">
160175
<Paragraph className="w-auto">No batches</Paragraph>
161176
</div>
162177
)}
163178
</TableBlankRow>
164179
) : batches.length === 0 ? (
165-
<TableBlankRow colSpan={7}>
180+
<TableBlankRow colSpan={8}>
166181
<div className="flex items-center justify-center">
167182
<Paragraph className="w-auto">No batches match these filters</Paragraph>
168183
</div>
@@ -215,13 +230,14 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
215230
<TableCell to={path}>
216231
{batch.finishedAt ? <DateTime date={batch.finishedAt} /> : "–"}
217232
</TableCell>
233+
<BatchActionsCell batch={batch} path={path} />
218234
</TableRow>
219235
);
220236
})
221237
)}
222238
{isLoading && (
223239
<TableBlankRow
224-
colSpan={7}
240+
colSpan={8}
225241
className="absolute left-0 top-0 flex h-full w-full items-center justify-center gap-2 bg-charcoal-900/90"
226242
>
227243
<Spinner /> <span className="text-text-dimmed">Loading…</span>
@@ -231,3 +247,48 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
231247
</Table>
232248
);
233249
}
250+
251+
function BatchActionsCell({ batch, path }: { batch: BatchListItem; path: string }) {
252+
const location = useLocation();
253+
254+
if (batch.hasFinished) return <TableCell to={path}>{""}</TableCell>;
255+
256+
return (
257+
<TableCellMenu
258+
isSticky
259+
popoverContent={
260+
<>
261+
<PopoverMenuItem
262+
to={path}
263+
icon={ArrowRightIcon}
264+
leadingIconClassName="text-blue-500"
265+
title="View batch"
266+
/>
267+
{!batch.hasFinished && (
268+
<Dialog>
269+
<DialogTrigger
270+
asChild
271+
className="size-6 rounded-sm p-1 text-text-dimmed transition hover:bg-charcoal-700 hover:text-text-bright"
272+
>
273+
<Button
274+
variant="small-menu-item"
275+
LeadingIcon={ArrowPathRoundedSquareIcon}
276+
leadingIconClassName="text-success"
277+
fullWidth
278+
textAlignLeft
279+
className="w-full px-1.5 py-[0.9rem]"
280+
>
281+
Try and resume
282+
</Button>
283+
</DialogTrigger>
284+
<CheckBatchCompletionDialog
285+
batchId={batch.id}
286+
redirectPath={`${location.pathname}${location.search}`}
287+
/>
288+
</Dialog>
289+
)}
290+
</>
291+
}
292+
/>
293+
);
294+
}
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)