feat(v2): expose execution control and event APIs
This commit is contained in:
@@ -11,6 +11,7 @@ from backup_tool.db.models import Execution, Job
|
||||
|
||||
ACTIVE_STATES = frozenset({"queued", "preparing", "running", "verifying", "cancelling"})
|
||||
TERMINAL_STATES = frozenset({"committed", "cancelled", "failed"})
|
||||
TRANSIENT_REASONS = frozenset({"worker_lost", "timeout", "unavailable", "transient_io"})
|
||||
|
||||
_ALLOWED: Mapping[str, frozenset[str]] = {
|
||||
"queued": frozenset({"preparing", "cancelled", "failed"}),
|
||||
@@ -107,6 +108,71 @@ async def heartbeat(
|
||||
return getattr(result, "rowcount", 0) == 1
|
||||
|
||||
|
||||
async def request_cancellation(db: AsyncSession, execution_id: str) -> Execution | None:
|
||||
result = await db.execute(
|
||||
update(Execution)
|
||||
.where(
|
||||
Execution.id == execution_id,
|
||||
Execution.state.in_({"queued", "preparing", "running"}),
|
||||
)
|
||||
.values(state="cancelling", reason_code="cancellation_requested")
|
||||
)
|
||||
if getattr(result, "rowcount", 0) != 1:
|
||||
await db.rollback()
|
||||
return None
|
||||
await db.commit()
|
||||
return await db.get(Execution, execution_id)
|
||||
|
||||
|
||||
async def complete_cancellation(db: AsyncSession, execution_id: str, owner: str) -> bool:
|
||||
result = await db.execute(
|
||||
update(Execution)
|
||||
.where(
|
||||
Execution.id == execution_id,
|
||||
Execution.lease_owner == owner,
|
||||
Execution.state == "cancelling",
|
||||
)
|
||||
.values(
|
||||
state="cancelled",
|
||||
completed_at=datetime.now(UTC),
|
||||
lease_owner=None,
|
||||
lease_expires_at=None,
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
return getattr(result, "rowcount", 0) == 1
|
||||
|
||||
|
||||
async def retry(db: AsyncSession, execution_id: str) -> Execution | None:
|
||||
execution = await db.get(Execution, execution_id)
|
||||
if execution is None:
|
||||
return None
|
||||
if execution.state != "failed" or execution.reason_code not in TRANSIENT_REASONS:
|
||||
raise EnqueueError("retry_not_allowed", "Execution failure is not retryable.")
|
||||
execution.state = "queued"
|
||||
execution.attempt += 1
|
||||
execution.lease_owner = None
|
||||
execution.lease_expires_at = None
|
||||
execution.heartbeat_at = None
|
||||
execution.reason_code = None
|
||||
execution.operator_message = None
|
||||
await db.commit()
|
||||
await db.refresh(execution)
|
||||
return execution
|
||||
|
||||
|
||||
def public_event(execution: Execution) -> dict[str, object]:
|
||||
"""Return redacted progress suitable for polling or SSE."""
|
||||
progress = {k: v for k, v in execution.progress.items() if k not in {"path", "secret", "token"}}
|
||||
return {
|
||||
"id": execution.id,
|
||||
"state": execution.state,
|
||||
"attempt": execution.attempt,
|
||||
"reason_code": execution.reason_code,
|
||||
"progress": progress,
|
||||
}
|
||||
|
||||
|
||||
async def recover_stale(db: AsyncSession) -> int:
|
||||
now = datetime.now(UTC)
|
||||
result = await db.execute(
|
||||
|
||||
Reference in New Issue
Block a user