Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tasks and their lifecycle

A task is one unit of work: a payload plus the rules for scheduling it. A pool is a set of worker slots that runs tasks. An executor (in-process) or a remote worker (server mode) runs one attempt of a task at a time; a task may need several attempts.

Task fields

FieldDefaultMeaning
payloadrequiredThe work. A Rust value, a Python object, or bytes on the wire.
pool"default"Which pool runs it.
kind"default"A label for metrics and for learning typical durations.
priorityNormalCritical, High, Normal or Low. See Priorities.
keynoneUnique name: deduplicates submissions and can be referenced by dependents.
deadlinenoneSoft "done by" time. See Deadlines.
fixed_startnoneStart time, tolerance and miss policy. See Fixed starts.
duration_hintlearnedExpected run time; used for deadlines and reservations.
timeoutnoneMaximum duration of each attempt.
retrypool defaultRetry policy. See Reliability.
depsnoneDependencies with edge conditions. See Dependencies.
resourcesnoneUnits of named resources needed while running. See Resources.
tagsnoneFree-form key/value labels passed to the executor (e.g. traceparent).
labelsnoneWorker labels the task requires; it runs only on workers that carry them. See Worker labels.
tenantnoneTenant the task belongs to, for fair sharing of a pool between tenants. See Fair sharing.

Every task gets a numeric id assigned by the engine. Ids increase monotonically; in server mode the top bits carry the leader's fencing epoch, so ids stay unique across failovers.

Lifecycle

                 ┌── dependencies unmet ──┐
  submit ──▶  Blocked                      │
     │           │ all edges satisfied      │
     ├──────▶ Scheduled ── start time ──┐   │
     │        (fixed start, in future)   ▼   ▼
     └────────────────────────────▶  Ready ◀──────── backoff elapsed ──── Retrying
                                       │ dispatched                        ▲
                                       ▼                                   │ retryable failure,
                                    Running ──────────────────────────────┘ timeout, lease lost
                                       │
      ┌──────────────┬────────────────┼──────────────┬───────────────────────┐
  Succeeded        Failed       DeadLettered     Cancelled        Skipped · Missed · Shed
                 (permanent)   (retries spent)  (by request)   (edge unmet · start missed · overload)
StatusTerminalMeaning
BlockedWaiting for dependencies.
ScheduledWaiting for its fixed start time.
ReadyEligible and queued (or waiting for a resource).
RunningAn attempt is executing.
RetryingWaiting for a retry backoff to elapse.
Succeeded✔The last attempt returned an output.
Failed✔Permanent failure, or a missed fixed start with MissPolicy::Fail.
DeadLettered✔Retryable failures used up every attempt.
Cancelled✔Cancelled by a client.
Skipped✔A dependency edge can no longer be satisfied, an awaited key never appeared, or a recurring occurrence was skipped by the overlap policy Skip.
Missed✔Fixed start missed with MissPolicy::Skip.
Shed✔Evicted to make room under overload.

Terminal statuses are retained for the retention window (1 hour, up to 1,000,000 entries by default). During that window the task can still be looked up, keys stay reserved, and new tasks may depend on it.

Results

  • Rust: submit returns a TaskHandle<O>; awaiting it yields Ok(Arc<O>) on success or Err(TaskFailure) with the terminal status and message.
  • Python: awaiting a TaskHandle returns the executor's return value or raises cronoq.TaskFailed.
  • Server: Wait / GET /v1/tasks/{id}/result return the output bytes (and JSON, when the bytes are JSON). Outputs are kept in a bounded in-memory cache and, with a durable store (SQLite or Postgres), in the store.

Handles are not required: enqueue submits without one, which is cheaper.

Events

Every terminal transition is published as a StatusEvent { id, pool, key, status, attempt, at, message }. With emit_transitions enabled, every intermediate transition is published as well. Subscribe with Scheduler::events() in Rust, sched.events() in Python, or Tasks/Watch over gRPC (with all_transitions for intermediate ones). Slow subscribers lose the oldest events rather than slowing the scheduler down.