diff --git a/task_handlers.go b/task_handlers.go index ccb0bd7..4d3f2de 100644 --- a/task_handlers.go +++ b/task_handlers.go @@ -229,7 +229,7 @@ func newDeleteAllDeadTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFu // request body used for all batch delete tasks endpoints. type batchDeleteTasksRequest struct { - taskKeys []string `json:"task_keys"` + TaskKeys []string `json:"task_keys"` } // Note: Redis does not have any rollback mechanism, so it's possible @@ -238,10 +238,10 @@ type batchDeleteTasksRequest struct { // and a list of failed keys. type batchDeleteTasksResponse struct { // task keys that were successfully deleted. - deletedKeys []string `json:"deleted_keys"` + DeletedKeys []string `json:"deleted_keys"` // task keys that were not deleted. - failedKeys []string `json:"failed_keys"` + FailedKeys []string `json:"failed_keys"` } // Maximum request body size in bytes. @@ -261,13 +261,17 @@ func newBatchDeleteDeadTasksHandlerFunc(inspector *asynq.Inspector) http.Handler } qname := mux.Vars(r)["qname"] - var resp batchDeleteTasksResponse - for _, key := range req.taskKeys { + resp := batchDeleteTasksResponse{ + // avoid null in the json response + DeletedKeys: make([]string, 0), + FailedKeys: make([]string, 0), + } + for _, key := range req.TaskKeys { if err := inspector.DeleteTaskByKey(qname, key); err != nil { log.Printf("error: could not delete task with key %q: %v", key, err) - resp.failedKeys = append(resp.failedKeys, key) + resp.FailedKeys = append(resp.FailedKeys, key) } else { - resp.deletedKeys = append(resp.deletedKeys, key) + resp.DeletedKeys = append(resp.DeletedKeys, key) } } if err := json.NewEncoder(w).Encode(resp); err != nil { diff --git a/ui/src/actions/tasksActions.ts b/ui/src/actions/tasksActions.ts index 38cd205..4aff61f 100644 --- a/ui/src/actions/tasksActions.ts +++ b/ui/src/actions/tasksActions.ts @@ -1,4 +1,6 @@ import { + batchDeleteDeadTasks, + BatchDeleteTasksResponse, cancelActiveTask, deleteDeadTask, deleteRetryTask, @@ -45,6 +47,10 @@ export const DELETE_RETRY_TASK_ERROR = "DELETE_RETRY_TASK_ERROR"; export const DELETE_DEAD_TASK_BEGIN = "DELETE_DEAD_TASK_BEGIN"; export const DELETE_DEAD_TASK_SUCCESS = "DELETE_DEAD_TASK_SUCCESS"; export const DELETE_DEAD_TASK_ERROR = "DELETE_DEAD_TASK_ERROR"; +export const BATCH_DELETE_DEAD_TASKS_BEGIN = "BATCH_DELETE_DEAD_TASKS_BEGIN"; +export const BATCH_DELETE_DEAD_TASKS_SUCCESS = + "BATCH_DELETE_DEAD_TASKS_SUCCESS"; +export const BATCH_DELETE_DEAD_TASKS_ERROR = "BATCH_DELETE_DEAD_TASKS_ERROR"; interface ListActiveTasksBeginAction { type: typeof LIST_ACTIVE_TASKS_BEGIN; @@ -207,6 +213,25 @@ interface DeleteDeadTaskErrorAction { error: string; } +interface BatchDeleteDeadTasksBeginAction { + type: typeof BATCH_DELETE_DEAD_TASKS_BEGIN; + queue: string; + taskKeys: string[]; +} + +interface BatchDeleteDeadTasksSuccessAction { + type: typeof BATCH_DELETE_DEAD_TASKS_SUCCESS; + queue: string; + payload: BatchDeleteTasksResponse; +} + +interface BatchDeleteDeadTasksErrorAction { + type: typeof BATCH_DELETE_DEAD_TASKS_ERROR; + queue: string; + taskKeys: string[]; + error: string; +} + // Union of all tasks related action types. export type TasksActionTypes = | ListActiveTasksBeginAction @@ -235,7 +260,10 @@ export type TasksActionTypes = | DeleteRetryTaskErrorAction | DeleteDeadTaskBeginAction | DeleteDeadTaskSuccessAction - | DeleteDeadTaskErrorAction; + | DeleteDeadTaskErrorAction + | BatchDeleteDeadTasksBeginAction + | BatchDeleteDeadTasksSuccessAction + | BatchDeleteDeadTasksErrorAction; export function listActiveTasksAsync( qname: string, @@ -422,3 +450,25 @@ export function deleteDeadTaskAsync(queue: string, taskKey: string) { } }; } + +export function batchDeleteDeadTasksAsync(queue: string, taskKeys: string[]) { + return async (dispatch: Dispatch) => { + dispatch({ type: BATCH_DELETE_DEAD_TASKS_BEGIN, queue, taskKeys }); + try { + const response = await batchDeleteDeadTasks(queue, taskKeys); + dispatch({ + type: BATCH_DELETE_DEAD_TASKS_SUCCESS, + queue: queue, + payload: response, + }); + } catch (error) { + console.error("batchDeleteDeadTasksAsync: ", error); + dispatch({ + type: BATCH_DELETE_DEAD_TASKS_ERROR, + error: `Could not batch delete tasks: ${taskKeys}`, + queue, + taskKeys, + }); + } + }; +} diff --git a/ui/src/api.ts b/ui/src/api.ts index 053e47c..7395eef 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -41,6 +41,11 @@ export interface ListSchedulerEntriesResponse { entries: SchedulerEntry[]; } +export interface BatchDeleteTasksResponse { + deleted_keys: string[]; + failed_keys: string[]; +} + export interface Queue { queue: string; paused: boolean; @@ -273,6 +278,21 @@ export async function deleteDeadTask( }); } +export async function batchDeleteDeadTasks( + qname: string, + taskKeys: string[] +): Promise { + const resp = await axios({ + method: "post", + url: `${BASE_URL}/queues/${qname}/dead_tasks:batch_delete`, + data: { + task_keys: taskKeys, + }, + }); + console.log("debug: response:", resp); + return resp.data; +} + export async function listSchedulerEntries(): Promise { const resp = await axios({ method: "get", diff --git a/ui/src/components/DeadTasksTable.tsx b/ui/src/components/DeadTasksTable.tsx index 122e1a1..397766f 100644 --- a/ui/src/components/DeadTasksTable.tsx +++ b/ui/src/components/DeadTasksTable.tsx @@ -28,6 +28,7 @@ import { AppState } from "../store"; import { listDeadTasksAsync, deleteDeadTaskAsync, + batchDeleteDeadTasksAsync, } from "../actions/tasksActions"; import TablePaginationActions, { defaultPageSize, @@ -61,11 +62,16 @@ function mapStateToProps(state: AppState) { return { loading: state.tasks.deadTasks.loading, tasks: state.tasks.deadTasks.data, + batchActionPending: state.tasks.deadTasks.batchActionPending, pollInterval: state.settings.pollInterval, }; } -const mapDispatchToProps = { listDeadTasksAsync, deleteDeadTaskAsync }; +const mapDispatchToProps = { + listDeadTasksAsync, + deleteDeadTaskAsync, + batchDeleteDeadTasksAsync, +}; const connector = connect(mapStateToProps, mapDispatchToProps); @@ -81,7 +87,7 @@ function DeadTasksTable(props: Props & ReduxProps) { const classes = useStyles(); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(defaultPageSize); - const [selected, setSelected] = useState([]); + const [selectedKeys, setSelectedKeys] = useState([]); const handleChangePage = ( event: React.MouseEvent | null, @@ -99,10 +105,10 @@ function DeadTasksTable(props: Props & ReduxProps) { const handleSelectAllClick = (event: React.ChangeEvent) => { if (event.target.checked) { - const newSelected = props.tasks.map((t) => t.id); - setSelected(newSelected); + const newSelected = props.tasks.map((t) => t.key); + setSelectedKeys(newSelected); } else { - setSelected([]); + setSelectedKeys([]); } }; @@ -132,7 +138,7 @@ function DeadTasksTable(props: Props & ReduxProps) { ]; const rowCount = props.tasks.length; - const numSelected = selected.length; + const numSelected = selectedKeys.length; return (
@@ -147,7 +153,16 @@ function DeadTasksTable(props: Props & ReduxProps) { > - + )}
@@ -176,14 +191,16 @@ function DeadTasksTable(props: Props & ReduxProps) { {props.tasks.map((task) => ( { if (checked) { - setSelected(selected.concat(task.id)); + setSelectedKeys(selectedKeys.concat(task.key)); } else { - setSelected(selected.filter((id) => id !== task.id)); + setSelectedKeys( + selectedKeys.filter((key) => key !== task.key) + ); } }} onDeleteClick={() => { diff --git a/ui/src/reducers/queuesReducer.ts b/ui/src/reducers/queuesReducer.ts index fa9bbd3..da689cc 100644 --- a/ui/src/reducers/queuesReducer.ts +++ b/ui/src/reducers/queuesReducer.ts @@ -14,6 +14,7 @@ import { DELETE_QUEUE_SUCCESS, } from "../actions/queuesActions"; import { + BATCH_DELETE_DEAD_TASKS_SUCCESS, DELETE_DEAD_TASK_SUCCESS, DELETE_RETRY_TASK_SUCCESS, DELETE_SCHEDULED_TASK_SUCCESS, @@ -198,6 +199,23 @@ function queuesReducer( return { ...state, data: newData }; } + case BATCH_DELETE_DEAD_TASKS_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + dead: + queueInfo.currentStats.dead - action.payload.deleted_keys.length, + }, + }; + }); + return { ...state, data: newData }; + } + default: return state; } diff --git a/ui/src/reducers/snackbarReducer.ts b/ui/src/reducers/snackbarReducer.ts index f8285b8..601e1a3 100644 --- a/ui/src/reducers/snackbarReducer.ts +++ b/ui/src/reducers/snackbarReducer.ts @@ -3,6 +3,7 @@ import { SnackbarActionTypes, } from "../actions/snackbarActions"; import { + BATCH_DELETE_DEAD_TASKS_SUCCESS, DELETE_DEAD_TASK_SUCCESS, DELETE_RETRY_TASK_SUCCESS, DELETE_SCHEDULED_TASK_SUCCESS, @@ -53,6 +54,14 @@ function snackbarReducer( message: `Dead task ${action.taskKey} deleted`, }; + case BATCH_DELETE_DEAD_TASKS_SUCCESS: { + const n = action.payload.deleted_keys.length; + return { + isOpen: true, + message: `${n} Dead ${n === 1 ? "task" : "tasks"} deleted`, + }; + } + default: return state; } diff --git a/ui/src/reducers/tasksReducer.ts b/ui/src/reducers/tasksReducer.ts index b0190f1..f010831 100644 --- a/ui/src/reducers/tasksReducer.ts +++ b/ui/src/reducers/tasksReducer.ts @@ -27,6 +27,9 @@ import { DELETE_DEAD_TASK_BEGIN, DELETE_DEAD_TASK_SUCCESS, DELETE_DEAD_TASK_ERROR, + BATCH_DELETE_DEAD_TASKS_BEGIN, + BATCH_DELETE_DEAD_TASKS_SUCCESS, + BATCH_DELETE_DEAD_TASKS_ERROR, } from "../actions/tasksActions"; import { ActiveTask, @@ -87,6 +90,7 @@ interface TasksState { }; deadTasks: { loading: boolean; + batchActionPending: boolean; error: string; data: DeadTaskExtended[]; }; @@ -115,6 +119,7 @@ const initialState: TasksState = { }, deadTasks: { loading: false, + batchActionPending: false, error: "", data: [], }, @@ -269,6 +274,7 @@ function tasksReducer( return { ...state, deadTasks: { + ...state.deadTasks, loading: false, error: "", data: action.payload.tasks.map((task) => ({ @@ -452,6 +458,38 @@ function tasksReducer( }, }; + case BATCH_DELETE_DEAD_TASKS_BEGIN: + return { + ...state, + deadTasks: { + ...state.deadTasks, + batchActionPending: true, + }, + }; + + case BATCH_DELETE_DEAD_TASKS_SUCCESS: { + const newData = state.deadTasks.data.filter( + (task) => !action.payload.deleted_keys.includes(task.key) + ); + return { + ...state, + deadTasks: { + ...state.deadTasks, + batchActionPending: false, + data: newData, + }, + }; + } + + case BATCH_DELETE_DEAD_TASKS_ERROR: + return { + ...state, + deadTasks: { + ...state.deadTasks, + batchActionPending: false, + }, + }; + default: return state; }