OutlabsTaskq
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

ResultMeaning
okTransition applied
already_settledSame verb already finished this attempt — treat as success on retry
settle_conflictDifferent verb against an already-settled attempt
lostFence lost (another attempt owns the job)
retry_scheduledFailure accepted; job re-queued with backoff
deadTerminal 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:

ReturnFieldsBurns attempts?
Completeresult: dict, optional followupsno
Followup (via Complete.followups)typed child graph declared on the parent Taskno
Snoozedelay_seconds: int, optional progress, reasonno
Cancelreason: strno
Retryoptional after_seconds, error, progressyes
NonRetryableerror: str, optional progressterminal
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.