Task Schedule Implementation
Operator guide: Task Schedules.
Dispatcher
The Dispatcher runs as a background goroutine started by the service at boot.
It claims due schedule rows with SELECT … FOR UPDATE SKIP LOCKED so other
instances skip locked rows.
It polls every 10 seconds and fetches up to 10 due schedules per poll by default. A schedule with no eligible scopes advances its timing without submitting tasks.
Three-phase fire
Each schedule firing is split into three phases to avoid nesting transactions (the task manager opens its own transaction when submitting a task):
- Locking phase (transaction): Lock the row, fetch scopes, run the overlap check,
advance
next_run_at(so the row is no longer “due”), commit. - Submission phase (outside transaction): Call
SubmitTaskonce per eligible scope. - Writeback phase (new transaction): Write back
last_task_idon each scope row.
If Phase 2 fails for a scope, the scope is logged and skipped — other scopes
still fire. Phase 1 committing before Phase 2 means next_run_at has already
advanced; the schedule will not fire again for that same tick even if all
submissions fail. Failed task-ID writeback can leave the overlap check using
stale state; see dispatch and failure behavior.
Advancing next_run_at
Timing advances after a normal firing, an overlap skip, or an empty scope:
Operation template
The operation_template JSONB column stores the operation type, code, and
parameters needed to reconstruct an operation.Request at fire time. The
target is not stored in the template — it is resolved from the scope rows
at fire time. This means changing the scope (via scope management RPCs) takes
effect on the very next firing without modifying the operation template.
Database schema
See the schedule migration for table definitions, indexes, and foreign keys.