From c0248f109a178aca24745e04b2f269f010433043 Mon Sep 17 00:00:00 2001 From: Aditya <60684641+0x0elliot@users.noreply.github.com> Date: Thu, 5 Oct 2023 06:27:31 +0530 Subject: [PATCH 1/2] feat: adding job run cancelling feature --- .../app/routes/api.v1.runs.$runId.cancel.ts | 146 ++++++++++++++++++ packages/trigger-sdk/src/apiClient.ts | 16 ++ packages/trigger-sdk/src/triggerClient.ts | 4 + 3 files changed, 166 insertions(+) create mode 100644 apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts new file mode 100644 index 0000000000..8d426cc13b --- /dev/null +++ b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts @@ -0,0 +1,146 @@ +// use CancelRunService to write a new route handler for the /api/v1/runs/:runId/cancel endpoint +import { parse } from "@conform-to/zod"; +import { ActionFunction, json } from "@remix-run/node"; +import { z } from "zod"; +import { prisma } from "~/db.server"; +import { redirectWithSuccessMessage } from "~/models/message.server"; +import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { apiCors } from "~/utils/apiCors"; +import { logger } from "~/services/logger.server"; +import { CancelRunService } from "~/services/runs/cancelRun.server"; +import { taskListToTree } from "~/utils/taskListToTree"; + +const ParamSchema = z.object({ + runId: z.string(), +}); + +const SearchQuerySchema = z.object({ + cursor: z.string().optional(), + take: z.coerce.number().default(20), + subtasks: z.coerce.boolean().default(false), + taskdetails: z.coerce.boolean().default(false), +}); + +export const action: ActionFunction = async ({ request, params }) => { + // Ensure this is a POST request + if (request.method.toUpperCase() != "POST") { + return { status: 405, body: "Method Not Allowed" }; + } + + const authenticationResult = await authenticateApiRequest(request, { + allowPublicKey: true, + }); + if (!authenticationResult) { + return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 })); + } + + const { runId } = ParamSchema.parse(params); + const url = new URL(request.url); + + const cancelRunService = new CancelRunService(); + const authenticatedEnv = authenticationResult.environment; + const parsedQuery = SearchQuerySchema.safeParse(Object.fromEntries(url.searchParams)); + + if (!parsedQuery.success) { + return apiCors( + request, + json({ error: "Invalid or missing query parameters" }, { status: 400 }) + ); + } + + try { + const query = parsedQuery.data; + const take = Math.min(query.take, 50); + const showTaskDetails = query.taskdetails && authenticationResult.type === "PRIVATE"; + await cancelRunService.call({ runId }); + const jobRun = await prisma.jobRun.findUnique({ + where: { + id: runId, + }, + select: { + id: true, + status: true, + startedAt: true, + updatedAt: true, + completedAt: true, + environmentId: true, + output: true, + tasks: { + select: { + id: true, + parentId: true, + displayKey: true, + status: true, + name: true, + icon: true, + startedAt: true, + completedAt: true, + params: showTaskDetails, + output: showTaskDetails, + }, + where: { + parentId: query.subtasks ? undefined : null, + }, + orderBy: { + id: "asc", + }, + take: take + 1, + cursor: query.cursor + ? { + id: query.cursor, + } + : undefined, + }, + statuses: { + select: { key: true, label: true, state: true, data: true, history: true }, + }, + }, + }); + + if (!jobRun) { + return apiCors(request, json({ message: "Run not found" }, { status: 404 })); + } + + if (jobRun.environmentId !== authenticatedEnv.id) { + return apiCors(request, json({ message: "Run not found" }, { status: 404 })); + } + + const selectedTasks = jobRun.tasks.slice(0, take); + const tasks = taskListToTree(selectedTasks, query.subtasks); + const nextTask = jobRun.tasks[take]; + + return apiCors(request, json({ + id: jobRun.id, + status: jobRun.status, + startedAt: jobRun.startedAt, + updatedAt: jobRun.updatedAt, + completedAt: jobRun.completedAt, + output: jobRun.output, + tasks: tasks.map((task) => { + const { parentId, ...rest } = task; + return { ...rest }; + }), + statuses: jobRun.statuses.map((s) => ({ + ...s, + state: s.state ?? undefined, + data: s.data ?? undefined, + history: s.history ?? undefined, + })), + nextCursor: nextTask ? nextTask.id : undefined, + })); + } catch (error) { + if (error instanceof Error) { + logger.error("Failed to cancel run", { + error: { + name: error.name, + message: error.message, + stack: error.stack, + }, + }); + return apiCors(request, json({ errors: { body: error.message } }, { status: 500 })); + } else { + logger.error("Failed to cancel run", { error }); + return apiCors(request, json({ errors: { body: "Unknown error" } }, { status: 500 })); + } + } +}; \ No newline at end of file diff --git a/packages/trigger-sdk/src/apiClient.ts b/packages/trigger-sdk/src/apiClient.ts index 42a2976a52..018aa7b919 100644 --- a/packages/trigger-sdk/src/apiClient.ts +++ b/packages/trigger-sdk/src/apiClient.ts @@ -216,6 +216,22 @@ export class ApiClient { }); } + async cancelRun(runId: string) { + const apiKey = await this.#apiKey(); + + this.#logger.debug("Cancelling run", { + runId, + }); + + return await zodfetch(ApiEventLogSchema, `${this.#apiUrl}/api/v1/runs/${runId}/cancel`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + }); + } + async updateStatus(runId: string, id: string, status: StatusUpdate) { const apiKey = await this.#apiKey(); diff --git a/packages/trigger-sdk/src/triggerClient.ts b/packages/trigger-sdk/src/triggerClient.ts index 55412d89b4..12152f998a 100644 --- a/packages/trigger-sdk/src/triggerClient.ts +++ b/packages/trigger-sdk/src/triggerClient.ts @@ -622,6 +622,10 @@ export class TriggerClient { return this.#client.cancelEvent(eventId); } + async cancelRun(runId: string) { + return this.#client.cancelRun(runId); + } + async updateStatus(runId: string, id: string, status: StatusUpdate) { return this.#client.updateStatus(runId, id, status); } From d87492f0a0c95fa93134d996053ae27dc72689e9 Mon Sep 17 00:00:00 2001 From: Aditya <60684641+0x0elliot@users.noreply.github.com> Date: Thu, 5 Oct 2023 07:20:13 +0530 Subject: [PATCH 2/2] fix: package-sdk --- apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts | 2 +- packages/trigger-sdk/src/apiClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts index 8d426cc13b..3736dd6cfc 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.cancel.ts @@ -23,7 +23,7 @@ const SearchQuerySchema = z.object({ export const action: ActionFunction = async ({ request, params }) => { // Ensure this is a POST request - if (request.method.toUpperCase() != "POST") { + if (request.method.toLowerCase() !== "post") { return { status: 405, body: "Method Not Allowed" }; } diff --git a/packages/trigger-sdk/src/apiClient.ts b/packages/trigger-sdk/src/apiClient.ts index 018aa7b919..b2e17a33e8 100644 --- a/packages/trigger-sdk/src/apiClient.ts +++ b/packages/trigger-sdk/src/apiClient.ts @@ -223,7 +223,7 @@ export class ApiClient { runId, }); - return await zodfetch(ApiEventLogSchema, `${this.#apiUrl}/api/v1/runs/${runId}/cancel`, { + return await zodfetch(z.object({}), `${this.#apiUrl}/api/v1/runs/${runId}/cancel`, { method: "POST", headers: { "Content-Type": "application/json",