From c3200aaf77543b200f225c40a1bf90dd3d870866 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Sat, 19 Sep 2026 00:08:45 +0200 Subject: [PATCH] feat(tasks): cancel queued and suspending tasks (#2270) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users (and admins) can now cancel tasks that have not started yet. - queue: StatusQueued/StatusSuspending -> StatusCanceled transitions; workers skip canceled tasks at pickup; Queue.CancelTask cancels via the in-memory registry - workflow: POST /workflow/:id/cancel — owner/admin gated; registry miss falls back to a persisted status update so restarted queues won't resume it; running remote downloads route to CancelDownload - task detail: cancel action on queued/suspending tasks - tests: registry cancel, persisted fallback, status/owner gates Authored By: TDvorak Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- frontend/src/api/api.ts | 16 +++ .../src/component/Pages/Tasks/TaskDetail.tsx | 25 ++++- inventory/task.go | 9 ++ pkg/queue/queue.go | 25 +++++ pkg/queue/task.go | 13 +++ routers/controllers/workflow.go | 13 +++ routers/router.go | 6 ++ service/explorer/workflows.go | 49 +++++++++ service/explorer/workflows_retry_test.go | 101 +++++++++++++++++- 9 files changed, 254 insertions(+), 3 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 1d920060..3a0216b6 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -919,6 +919,22 @@ export function getTasks(req: ListTaskService): ThunkResponse }; } +export function sendCancelTask(id: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/workflow/${id}/cancel`, + { + method: "POST", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + export function sendRetryTask(id: string): ThunkResponse { return async (dispatch, _getState) => { return await dispatch( diff --git a/frontend/src/component/Pages/Tasks/TaskDetail.tsx b/frontend/src/component/Pages/Tasks/TaskDetail.tsx index 9979ae15..1012b813 100644 --- a/frontend/src/component/Pages/Tasks/TaskDetail.tsx +++ b/frontend/src/component/Pages/Tasks/TaskDetail.tsx @@ -12,7 +12,7 @@ import { } from "@mui/material"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { sendRetryTask } from "../../../api/api.ts"; +import { sendCancelTask, sendRetryTask } from "../../../api/api.ts"; import { TaskResponse, TaskStatus } from "../../../api/workflow.ts"; import { useAppDispatch } from "../../../redux/hooks.ts"; import { SecondaryLoadingButton, StyledTableContainerPaper } from "../../Common/StyledComponents.tsx"; @@ -30,6 +30,7 @@ const TaskDetail = ({ task, downloading, onRetried }: TaskDetailProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const [retrying, setRetrying] = useState(false); + const [canceling, setCanceling] = useState(false); const retry = () => { setRetrying(true); @@ -38,6 +39,16 @@ const TaskDetail = ({ task, downloading, onRetried }: TaskDetailProps) => { .catch(() => {}) .finally(() => setRetrying(false)); }; + + const cancel = () => { + setCanceling(true); + dispatch(sendCancelTask(task.id)) + .then(() => onRetried?.()) + .catch(() => {}) + .finally(() => setCanceling(false)); + }; + + const cancelable = task.status == TaskStatus.queued || task.status == TaskStatus.suspending; return ( @@ -60,6 +71,18 @@ const TaskDetail = ({ task, downloading, onRetried }: TaskDetailProps) => { })} )} + {cancelable && ( + + {t("common:cancel")} + + )} {task.status == TaskStatus.error && (