Add functionality to reset stale RUNNING tasks to READY in task overview

This commit is contained in:
Ross
2026-05-18 14:15:21 +01:00
parent e016175d2b
commit c55797636a
3 changed files with 58 additions and 1 deletions
+22
View File
@@ -32,6 +32,8 @@ from reversion.admin import VersionAdmin
from django.forms.widgets import RadioSelect from django.forms.widgets import RadioSelect
from django.utils.html import format_html from django.utils.html import format_html
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from datetime import timedelta
from tinymce.widgets import TinyMCE from tinymce.widgets import TinyMCE
@@ -226,6 +228,24 @@ if DBTaskResult is not None:
msg += f" Skipped {blocked} running task(s)." msg += f" Skipped {blocked} running task(s)."
self.message_user(request, msg, level=messages.SUCCESS) self.message_user(request, msg, level=messages.SUCCESS)
@admin.action(description="Reset stale RUNNING tasks to READY (>30 min)")
def reset_stale_running_to_ready(self, request, queryset):
cutoff = timezone.now() - timedelta(minutes=30)
updated = queryset.filter(status="RUNNING", started_at__lt=cutoff).update(
status="READY",
started_at=None,
finished_at=None,
worker_ids="",
exception_class_path="",
traceback="",
return_value=None,
)
self.message_user(
request,
f"Reset {updated} stale RUNNING task(s) to READY (older than 30 minutes).",
level=messages.SUCCESS,
)
@admin.action(description="Delete selected COMPLETED tasks") @admin.action(description="Delete selected COMPLETED tasks")
def delete_selected_completed(self, request, queryset): def delete_selected_completed(self, request, queryset):
completed_qs = queryset.filter(status="SUCCESSFUL") completed_qs = queryset.filter(status="SUCCESSFUL")
@@ -278,6 +298,7 @@ if DBTaskResult is not None:
admin_cls.actions = ( admin_cls.actions = (
"reset_failed_to_ready", "reset_failed_to_ready",
"reset_selected_to_ready", "reset_selected_to_ready",
"reset_stale_running_to_ready",
"delete_selected_completed", "delete_selected_completed",
"delete_selected_failed", "delete_selected_failed",
) )
@@ -339,6 +360,7 @@ if DBTaskResult is not None:
admin_cls.traceback_pretty = traceback_pretty admin_cls.traceback_pretty = traceback_pretty
admin_cls.reset_failed_to_ready = reset_failed_to_ready admin_cls.reset_failed_to_ready = reset_failed_to_ready
admin_cls.reset_selected_to_ready = reset_selected_to_ready admin_cls.reset_selected_to_ready = reset_selected_to_ready
admin_cls.reset_stale_running_to_ready = reset_stale_running_to_ready
admin_cls.delete_selected_completed = delete_selected_completed admin_cls.delete_selected_completed = delete_selected_completed
admin_cls.delete_selected_failed = delete_selected_failed admin_cls.delete_selected_failed = delete_selected_failed
admin_cls.has_add_permission = has_add_permission admin_cls.has_add_permission = has_add_permission
+12
View File
@@ -82,10 +82,22 @@
<option value="">Choose task action...</option> <option value="">Choose task action...</option>
<option value="reset_failed">Reset FAILED to READY</option> <option value="reset_failed">Reset FAILED to READY</option>
<option value="reset_selected">Reset selected to READY (skip RUNNING)</option> <option value="reset_selected">Reset selected to READY (skip RUNNING)</option>
<option value="reset_stale_running">Reset stale RUNNING to READY</option>
<option value="delete_completed">Delete selected COMPLETED</option> <option value="delete_completed">Delete selected COMPLETED</option>
<option value="delete_failed">Delete selected FAILED</option> <option value="delete_failed">Delete selected FAILED</option>
<option value="delete_selected_all">Delete ALL selected (including RUNNING)</option> <option value="delete_selected_all">Delete ALL selected (including RUNNING)</option>
</select> </select>
<input
class="form-control form-control-sm"
type="number"
name="stale_minutes"
min="1"
step="1"
value="30"
style="max-width: 150px;"
title="Stale cutoff in minutes for reset stale RUNNING action"
aria-label="Stale minutes"
>
<button type="submit" class="btn btn-sm btn-primary">Apply to selected</button> <button type="submit" class="btn btn-sm btn-primary">Apply to selected</button>
<div class="form-check ms-2"> <div class="form-check ms-2">
<input class="form-check-input" type="checkbox" id="select-all-tasks"> <input class="form-check-input" type="checkbox" id="select-all-tasks">
+24 -1
View File
@@ -4,6 +4,7 @@ import csv
import random import random
import copy import copy
import io import io
from datetime import timedelta
from typing import Any from typing import Any
from dal import autocomplete from dal import autocomplete
from django.db.models.base import Model as Model from django.db.models.base import Model as Model
@@ -25,6 +26,7 @@ from django.template.defaultfilters import filesizeformat
# from django.contrib.auth.models import User # from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib import messages
from django.core.exceptions import ( from django.core.exceptions import (
FieldError, FieldError,
ObjectDoesNotExist, ObjectDoesNotExist,
@@ -882,7 +884,7 @@ def series_reconstruct_status_htmx(request, series_id, task_id):
@login_required @login_required
@user_is_atlas_editor @user_is_atlas_editor
def task_overview(request): def task_overview(request):
"""Atlas admin-style task overview page with active/completed/failed task summaries.""" """Atlas editor task control page for inspecting and recovering queued/background tasks."""
if DBTaskResult is None: if DBTaskResult is None:
return render( return render(
request, request,
@@ -930,6 +932,27 @@ def task_overview(request):
if blocked: if blocked:
msg += f" Skipped {blocked} running task(s)." msg += f" Skipped {blocked} running task(s)."
messages.success(request, msg) messages.success(request, msg)
elif action == "reset_stale_running":
stale_minutes_raw = (request.POST.get("stale_minutes") or "30").strip()
try:
stale_minutes = max(int(stale_minutes_raw), 1)
except ValueError:
stale_minutes = 30
cutoff = timezone.now() - timedelta(minutes=stale_minutes)
updated = selected_qs.filter(status="RUNNING", started_at__lt=cutoff).update(
status="READY",
started_at=None,
finished_at=None,
worker_ids="",
exception_class_path="",
traceback="",
return_value=None,
)
messages.success(
request,
f"Reset {updated} stale RUNNING task(s) to READY (older than {stale_minutes} minute(s)).",
)
elif action == "delete_completed": elif action == "delete_completed":
deleted, _ = selected_qs.filter(status="SUCCESSFUL").delete() deleted, _ = selected_qs.filter(status="SUCCESSFUL").delete()
messages.success(request, f"Deleted {deleted} completed task record(s).") messages.success(request, f"Deleted {deleted} completed task record(s).")