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 |
Followup (via Complete.followups) | typed child graph declared on the parent Task | 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 |
from taskq import Complete, Followup, 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)},
followups=(
Followup(
step="enrich",
job_type="listing.enrich",
queue="enrichment",
payload={"listing_id": payload.listing_id},
),
),
)
Parent settlement and every declared child insert commit together. Declare followup_targets on the parent Task — see Follow-ups.