fix(import-admin): retry-failed + clear-stuck — same UPDATE…WHERE pattern as the maintenance sweep, so neither endpoint can hit psycopg's 65535-parameter ceiling once accumulated row counts exceed ~65k

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 11:47:40 -04:00
parent 110c1c0e51
commit b0bfbc585a
+21 -18
View File
@@ -103,17 +103,22 @@ async def list_tasks():
@import_admin_bp.route("/retry-failed", methods=["POST"]) @import_admin_bp.route("/retry-failed", methods=["POST"])
async def retry_failed(): 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: async with get_session() as session:
failed_ids = ( result = await session.execute(
await session.execute(select(ImportTask.id).where(ImportTask.status == "failed")) update(ImportTask)
).scalars().all() .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: if not failed_ids:
return jsonify({"retried": 0}) 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() await session.commit()
from ..tasks.import_file import import_media_file from ..tasks.import_file import import_media_file
@@ -138,17 +143,14 @@ async def clear_stuck():
autoretry-looped for 2 days after a corrupt-data PIL OSError. autoretry-looped for 2 days after a corrupt-data PIL OSError.
""" """
async with get_session() as session: async with get_session() as session:
stuck_ids = ( # Fold SELECT into UPDATE…WHERE — see /retry-failed for the
await session.execute( # 65535-parameter ceiling rationale. rowcount is enough here
select(ImportTask.id).where( # because we don't need the ids afterward (no .delay()).
clear_result = await session.execute(
update(ImportTask)
.where(
ImportTask.status.in_(["pending", "queued", "processing"]) ImportTask.status.in_(["pending", "queued", "processing"])
) )
)
).scalars().all()
if stuck_ids:
await session.execute(
update(ImportTask)
.where(ImportTask.id.in_(stuck_ids))
.values( .values(
status="failed", status="failed",
finished_at=datetime.now(UTC), finished_at=datetime.now(UTC),
@@ -160,6 +162,7 @@ async def clear_stuck():
), ),
) )
) )
tasks_failed = clear_result.rowcount or 0
# Finalize any 'running' ImportBatch that no longer has any # Finalize any 'running' ImportBatch that no longer has any
# active children. The "Scanning..." banner is driven by # active children. The "Scanning..." banner is driven by
@@ -195,7 +198,7 @@ async def clear_stuck():
await session.commit() await session.commit()
return jsonify({ return jsonify({
"tasks_failed": len(stuck_ids), "tasks_failed": tasks_failed,
"batches_finalized": finalized_batches, "batches_finalized": finalized_batches,
}) })