From 94c60c0af20d42d0b75c31b1d988079df071090f Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 25 May 2026 00:40:03 -0400 Subject: [PATCH] =?UTF-8?q?fc3k:=20admin=20Celery=20tasks=20=E2=80=94=20de?= =?UTF-8?q?lete=5Fartist=5Fcascade=5Ftask=20+=20bulk=5Fdelete=5Fimages=5Ft?= =?UTF-8?q?ask=20on=20maintenance=20queue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/tasks/admin.py | 57 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 backend/app/tasks/admin.py diff --git a/backend/app/tasks/admin.py b/backend/app/tasks/admin.py new file mode 100644 index 0000000..fec90c8 --- /dev/null +++ b/backend/app/tasks/admin.py @@ -0,0 +1,57 @@ +"""FC-3k: admin destructive Celery tasks. + +Two long-running ops on the maintenance queue. task_run lifecycle is +captured automatically by FC-3i signals — these tasks just return +their summary dict so it lands in task_run.metadata (via Celery's +result backend) for the dashboard to surface. + +Soft/hard time limits inherit the FC-3i recovery sweep: a runaway +task gets killed and flipped to status='timeout' by +recover_stalled_task_runs. +""" +from __future__ import annotations + +import logging +from pathlib import Path + +from sqlalchemy.exc import DBAPIError, OperationalError + +from ..celery_app import celery +from ..services import cleanup_service +from ._sync_engine import sync_session_factory as _sync_session_factory + +log = logging.getLogger(__name__) +IMAGES_ROOT = Path("/images") + + +@celery.task( + name="backend.app.tasks.admin.delete_artist_cascade_task", + bind=True, + autoretry_for=(OperationalError, DBAPIError), + retry_backoff=15, retry_backoff_max=180, max_retries=1, + soft_time_limit=1800, time_limit=2400, # 30 min / 40 min +) +def delete_artist_cascade_task(self, *, artist_id: int) -> dict: + """Wraps cleanup_service.delete_artist_cascade. Returns the + service's summary dict for FC-3i task_run.metadata capture.""" + SessionLocal = _sync_session_factory() + with SessionLocal() as session: + return cleanup_service.delete_artist_cascade( + session, artist_id=artist_id, images_root=IMAGES_ROOT, + ) + + +@celery.task( + name="backend.app.tasks.admin.bulk_delete_images_task", + bind=True, + autoretry_for=(OperationalError, DBAPIError), + retry_backoff=15, retry_backoff_max=180, max_retries=1, + soft_time_limit=900, time_limit=1200, # 15 min / 20 min +) +def bulk_delete_images_task(self, *, image_ids: list[int]) -> dict: + """Wraps cleanup_service.delete_images.""" + SessionLocal = _sync_session_factory() + with SessionLocal() as session: + return cleanup_service.delete_images( + session, image_ids=image_ids, images_root=IMAGES_ROOT, + )