Concepts
Fencing and settles
Attempt IDs, typed settle races, and handler return values.
Every claim creates a server-generated attempt_id. Heartbeat and settle verbs CAS on (job_id, attempt_id).
Settle results
| Result | Meaning |
|---|---|
ok | Transition applied |
already_settled | Same verb already finished this attempt — treat as success on retry |
settle_conflict | Different verb against an already-settled attempt |
lost | Fence lost (another attempt owns the job) |
retry_scheduled | Failure accepted; job re-queued with backoff |
dead | Terminal failure |
A dropped HTTP response after a successful settle is no longer indistinguishable from theft: retrying the same verb returns already_settled.
Handler return values
Handlers return typed results; the worker maps them to SQL:
| Return | Fields | Burns attempts? |
|---|---|---|
Complete | result: dict, optional followups | no |
Snooze | delay_seconds: int, optional progress, reason | no |
Cancel | reason: str | no |
Retry | optional after_seconds, error, progress | yes |
NonRetryable | error: str, optional progress | terminal |
Non-empty
Complete.followups are rejected by the 0.1 worker/SQL (followup_rejected). The field is reserved for 0.2.from taskq import Complete, Snooze
async def scrape(payload: Input) -> Complete | Snooze:
if quota_hit():
return Snooze(delay_seconds=3600, reason="provider_quota")
data = await do_scrape(payload)
return Complete(result={"n": len(data)})