diff --git a/backend/app/api/import_admin.py b/backend/app/api/import_admin.py index 367305b..02f65d2 100644 --- a/backend/app/api/import_admin.py +++ b/backend/app/api/import_admin.py @@ -103,17 +103,22 @@ async def list_tasks(): @import_admin_bp.route("/retry-failed", methods=["POST"]) async def retry_failed(): + # Fold SELECT into UPDATE…WHERE…RETURNING — the prior SELECT-then- + # UPDATE-WHERE-id-IN pattern blew past psycopg's 65535-parameter + # ceiling once failed_ids exceeded ~65k rows. async with get_session() as session: - failed_ids = ( - await session.execute(select(ImportTask.id).where(ImportTask.status == "failed")) - ).scalars().all() + result = await session.execute( + update(ImportTask) + .where(ImportTask.status == "failed") + .values( + status="queued", error=None, + started_at=None, finished_at=None, + ) + .returning(ImportTask.id) + ) + failed_ids = [row[0] for row in result.all()] if not failed_ids: return jsonify({"retried": 0}) - await session.execute( - update(ImportTask) - .where(ImportTask.id.in_(failed_ids)) - .values(status="queued", error=None, started_at=None, finished_at=None) - ) await session.commit() from ..tasks.import_file import import_media_file @@ -138,28 +143,26 @@ async def clear_stuck(): autoretry-looped for 2 days after a corrupt-data PIL OSError. """ async with get_session() as session: - stuck_ids = ( - await session.execute( - select(ImportTask.id).where( - ImportTask.status.in_(["pending", "queued", "processing"]) - ) + # Fold SELECT into UPDATE…WHERE — see /retry-failed for the + # 65535-parameter ceiling rationale. rowcount is enough here + # because we don't need the ids afterward (no .delay()). + clear_result = await session.execute( + update(ImportTask) + .where( + ImportTask.status.in_(["pending", "queued", "processing"]) ) - ).scalars().all() - if stuck_ids: - await session.execute( - update(ImportTask) - .where(ImportTask.id.in_(stuck_ids)) - .values( - status="failed", - finished_at=datetime.now(UTC), - error=( - "manually cleared via /api/import/clear-stuck " - "— stuck in non-terminal state; retry once " - "underlying cause (corrupt file, missing model, " - "etc.) is resolved" - ), - ) + .values( + status="failed", + finished_at=datetime.now(UTC), + error=( + "manually cleared via /api/import/clear-stuck " + "— stuck in non-terminal state; retry once " + "underlying cause (corrupt file, missing model, " + "etc.) is resolved" + ), ) + ) + tasks_failed = clear_result.rowcount or 0 # Finalize any 'running' ImportBatch that no longer has any # active children. The "Scanning..." banner is driven by @@ -195,7 +198,7 @@ async def clear_stuck(): await session.commit() return jsonify({ - "tasks_failed": len(stuck_ids), + "tasks_failed": tasks_failed, "batches_finalized": finalized_batches, })