Source code for app.orchestrator.resume
"""Resume a previously failed/cancelled Job.
The resume contract:
- Tasks in `(failed, cancelled, cancelled_dirty)` are reset to
`pending`. Their `cache_key` is preserved so the same upstream
cache hits replay.
- Tasks in `succeeded` are kept as-is (free cache).
- The Job itself transitions back to `pending`, cancel flags
cleared.
- If `inline=True` (or `SFMAPI_INLINE_TASKS=true`), reset tasks are
re-run synchronously here. Otherwise they're re-enqueued via ARQ.
- For mapping tasks specifically, the worker reads
`jobs/{job_id}/checkpoints/` (see `app.storage.mapping_input`) to
resume from the latest `MappingInput.save` write, avoiding a
full restart.
"""
from __future__ import annotations
import contextlib
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import get_settings
from app.core.errors import NotFoundError
from app.db.models import Job, Task
[docs]
async def resume_job(
session: AsyncSession, *, tenant_id: str, job_id: str, inline: bool | None = None
) -> Job:
j = (
await session.execute(select(Job).where(Job.tenant_id == tenant_id, Job.job_id == job_id))
).scalar_one_or_none()
if j is None:
raise NotFoundError(f"Job {job_id} not found")
# Reset only failed/cancelled tasks; keep succeeded ones to leverage cache.
tasks = (await session.execute(select(Task).where(Task.job_id == job_id))).scalars().all()
for t in tasks:
if t.status in ("failed", "cancelled", "cancelled_dirty"):
t.status = "pending"
t.error_class = None
t.error_message = None
t.lease_expires_at = None
t.worker_id = None
j.cancel_requested = False
j.cancel_force = False
j.status = "pending"
j.error_class = None
j.error_message = None
await session.flush()
settings = get_settings()
use_inline = settings.inline_tasks if inline is None else inline
reset_ids = [t.task_id for t in tasks if t.status == "pending"]
if reset_ids:
from app.orchestrator.queue import InlineQueue, get_queue
queue = InlineQueue(settings) if use_inline else get_queue(settings)
if use_inline:
# Commit + close so SQLite writer lock is free for the
# worker's session inside `run_task`.
await session.commit()
await session.close()
try:
for tid in reset_ids:
if use_inline:
await queue.enqueue(tid)
else:
with contextlib.suppress(Exception):
await queue.enqueue(tid)
finally:
await queue.close()
return j