Scheduler — production-компонент, который превращает правила времени в надёжные сигналы: «запусти один раз в 15:00», «каждый час», «через 20 минут», «по будням», «после задержки», «к дедлайну».
№42 Events & Triggers определяет смысл события и реакцию; scheduler создаёт time-originated event/occurrence. №57 Queues & Workers хранит и исполняет созданную работу; scheduler не владеет execution concurrency. №59 Broker / Message Bus транспортирует сообщения; scheduler может publish через broker, но не владеет pub-sub topology. №63 Retry/Circuit Breakers владеет общей resilience policy; scheduler отвечает только за delivery/misfire semantics time occurrence. №68 Durable Workflow владеет workflow timers внутри многошагового процесса; №58 — общий scheduler независимых scheduled jobs. №64 Budgets может ограничить количество/стоимость запланированных задач.
Prerequisites: №10 State, №42 Events, №46 Observability, №50 Contracts, №57 Queues & Workers. Forward references: №59 Broker, №63 Retry/Circuit Breakers, №64 Budgets, №68 Durable Workflow, №69 Distributed Reliability.
REQUEST-TIME: create/update/cancel schedule may happen synchronously. CONTROL PLANE: schedule definitions, timezone, cadence, owner, status, next_run, misfire policy. DATA PLANE: emitted occurrences/jobs/events. OFFLINE: cleanup, reconciliation, calendar-rule tests, load/failure tests.
Success: each intended schedule occurrence becomes exactly one logical occurrence within allowed lateness. Retryable: transient DB/queue/broker outage before durable handoff. Permanent: invalid recurrence/timezone/expired schedule/forbidden owner. Idempotency: occurrence key = schedule_id + scheduled_for. Persist: rule/version/timezone/next_run/last_run/status/misfire policy/occurrence ids. Trace: due→claim→emit→handoff→acknowledged. Security: schedule owner/tenant and target capability bound by host.
№58 не владеет worker execution, business event semantics, workflow state machine, generic message transport, retry/circuit policy или user calendar product. Она владеет TIME RULE → DUE OCCURRENCE → DURABLE HANDOFF.
«Запустить 5 сентября в 14:00 Europe/Riga».
«Через 20 минут», «через 3 дня после события».
Каждые 10 минут/6 часов. Семантика может быть fixed-rate или fixed-delay.
По будням в 09:00, первого числа, каждый понедельник.
Escalate/expire/notify when deadline arrives.
Например, запускать в течение business hours, но не ночью.
{
"contract": "schedule.v1",
"schedule_id": "SCH-...",
"tenant_id": "tenant_A",
"owner_ref": "user://...",
"target": {
"type": "JOB",
"job_type": "source.sync",
"payload_ref": "state://sync-source-42"
},
"timing": {
"kind": "RECURRENCE",
"rule": "FREQ=DAILY;BYHOUR=9;BYMINUTE=0",
"timezone": "Europe/Riga"
},
"misfire": "SKIP|FIRE_ONCE|CATCH_UP",
"max_lateness_s": 900,
"status": "ACTIVE",
"version": 3,
"next_run_at": "...",
"created_at": "...",
"updated_at": "..."
}Natural-language request можно преобразовать в этот contract, но execution использует structured rule.
{
"occurrence_id": "OCC-...",
"schedule_id": "SCH-...",
"schedule_version": 3,
"scheduled_for": "2026-09-01T09:00:00+03:00",
"emitted_at": "2026-09-01T09:00:02+03:00",
"idempotency_key":
"schedule:SCH-...:2026-09-01T09:00:00+03:00",
"target_ref": "job://...",
"status": "EMITTED"
}Отдельная occurrence позволяет:
Видит next_run_at ≤ now().
Claim schedule row или insert occurrence по unique (schedule_id, scheduled_for).
Получает conflict/no-op и не создаёт duplicate logical occurrence.
Запуски рассчитаны от календарной сетки: 10:00, 11:00, 12:00 независимо от длительности предыдущего run.
Подходит для polling/freshness schedules.
Следующий запуск через N после завершения предыдущего: finished 10:17 → next 11:17.
Это уже требует знания completion state и ближе к workflow/job coordination.
Computed next_run/current timestamps удобно хранить как absolute UTC instants.
Для «каждый день в 09:00 по Риге» хранить zone id вроде Europe/Riga, а не fixed +03:00.
Следующий occurrence вычисляется по timezone rules, учитывая DST/history changes.
+02:00 не знает будущие переходы DST; Europe/Riga знает календарную зону.| Case | Problem | Need explicit policy |
|---|---|---|
| Spring forward | Например, 02:30 может не существовать. | Skip / shift to next valid time / provider-defined behavior. |
| Fall back | 02:30 может произойти дважды. | Fire once per local slot or per absolute instant. |
| Timezone rule changes | Government changes DST/offset rules. | Use maintained IANA timezone database. |
| User changes timezone | Calendar intent changes. | Version schedule and recompute future runs. |
Каждый occurrence независим; workers могут выполнять одновременно.
Не создавать/не исполнять новый occurrence, если предыдущий active.
Occurrence создаётся, но job ждёт previous completion.
Несколько missed/overlapping occurrences превращаются в один current refresh.
Calculates due time and creates unique occurrence.
Durably stores executable job and absorbs backlog.
Executes under concurrency, retry, permission and resource controls.
«Каждые 15 минут синхронизировать source 42» → enqueue source.sync.
«Наступил дедлайн договора» → emit domain event contract.deadline_reached; №42 decides which reactions/triggers follow.
Recurring reports, source sync, tenant maintenance, standalone reminder.
«Подождать 48 часов после отправки письма; если ответа нет — эскалировать». Timer is part of durable workflow history/state.
| Race | Required design |
|---|---|
| Schedule edited while due scan runs | Optimistic version / row lock; occurrence stores schedule_version. |
| Schedule paused after occurrence emitted | Define whether already-emitted job remains valid; cancellation may target job separately. |
| Schedule deleted while worker runs | Deletion stops future occurrences; existing execution follows explicit cancellation policy. |
| Timezone changed | Version schedule; recompute future next_run; old emitted occurrences retain old version. |
| Payload changed | Occurrence binds payload/snapshot/reference version so execution is auditable. |
Normal scheduled state.
Definition retained; resume policy determines next run.
No future runs after end condition/date/count.
No future runs; historical occurrences remain auditable.
Owner/principal authorized to create target schedule for tenant/resource.
Scheduler can create occurrence but not automatically inherit all target credentials.
Worker/tool evaluates current permission/policy where action can have external effect.
Leader election гарантирует, что один instance вычисляет due jobs; standby takeover при failure.
Несколько instances atomically claim due rows/insert unique occurrences. Проще, если DB locking/unique constraints достаточны.
FOR UPDATE SKIP LOCKED + unique occurrence, чем строить отдельный leader-election subsystem.loop every 1s:
begin transaction
due = SELECT schedules
WHERE status='ACTIVE'
AND next_run_at <= now()
ORDER BY next_run_at
FOR UPDATE SKIP LOCKED
LIMIT 100
for schedule in due:
scheduled_for = schedule.next_run_at
INSERT occurrence(
schedule_id,
schedule_version,
scheduled_for,
idempotency_key
)
ON CONFLICT(schedule_id, scheduled_for)
DO NOTHING
if inserted:
enqueue_or_publish(occurrence)
schedule.next_run_at =
compute_next(schedule.rule, schedule.timezone)
commit
sleep
Advance next_run, crash before queue publish → occurrence lost.
Publish job, crash before advance → same occurrence may publish again.
Create unique occurrence transactionally; downstream emission is replayable/idempotent until acknowledged.
| Class | Typical lateness target | Examples |
|---|---|---|
| HUMAN REMINDER | Seconds–minute depending product promise. | User reminders/notifications. |
| BUSINESS BATCH | Minutes. | Daily report, sync, digest. |
| MAINTENANCE | Minutes–hours. | Cleanup, reindex, cache refresh. |
| WORKFLOW DEADLINE | Domain-specific; often minutes. | SLA escalation, approval timeout. |
| REAL-TIME CONTROL | Sub-second requirements. | Usually not general application scheduler; use specialized realtime system. |
emitted_at - scheduled_for p50/p95/p99.
Количество active schedules с next_run_at сильно в прошлом.
Skipped/caught-up/expired occurrences.
Conflicting occurrence insert/publish attempts.
Occurrence created but queue/event handoff pending/failed.
Invalid/uncomputable recurrence/timezone.
Active schedules/occurrences per tenant/type.
Unexpected skew between scheduler nodes/time source where relevant.
Before due = no occurrence; at/after due = one occurrence.
Daily/weekly/monthly rules across boundaries.
Spring/fall transitions for supported zones.
Advance fake clock by hours/days; verify skip/fire-once/catch-up.
Two schedulers race; one logical occurrence created.
Crash before/after occurrence/publish; reconciliation recovers safely.
Edit/pause/cancel while due and verify bound semantics.
Thousands due simultaneously; jitter/backpressure/queue remain healthy.
scheduled_for → occurrence emitted.
% intended occurrences handled via misfire policy.
Active schedules whose next_run is late beyond SLA.
Multiple downstream effects for one schedule slot. Target: zero.
Occurrences waiting/failed before queue/event delivery.
Incorrect calendar fires due to zone/rule handling.
Count per tenant/type/timezone.
Jobs emitted after outage and resulting pressure.
schedules( schedule_id uuid primary key, tenant_id text, owner_ref text, target_json jsonb, timing_kind text, timing_rule text, timezone text, misfire_policy text, max_lateness_s int, status text, version int, next_run_at timestamptz, last_run_at timestamptz, starts_at timestamptz, ends_at timestamptz, created_at timestamptz, updated_at timestamptz ) occurrences( occurrence_id uuid primary key, schedule_id uuid, schedule_version int, scheduled_for timestamptz, emitted_at timestamptz, handoff_status text, target_ref text, unique(schedule_id, scheduled_for) ) scheduler process: scan due rows claim atomically insert occurrence handoff to DB queue compute/persist next_run
Dedicated timer service нужен позже, если millions of timers, sub-second precision, multi-region scale или DB scanning становится bottleneck.
| Signal | Potential upgrade |
|---|---|
| Millions of active schedules | Partitioned timing buckets / specialized scheduler service. |
| Sub-second timer precision | Specialized timing infrastructure rather than periodic DB scans. |
| Multi-region active-active requirements | Distributed ownership/consensus/region affinity — see №69. |
| Complex workflow-local waits | Durable Workflow engine — see №68. |
| Huge fan-out at schedule boundaries | Queue/broker capacity, jitter, admission control — №57/59/64. |
| Вопрос | Ответ |
|---|---|
| Стоит ли реализовывать? | Да, если time-based work является частью продукта/операций. Для пары системных periodic tasks обычного cron может хватить. |
| Separate Component? | YES. Time calculation and durable occurrence emission — отдельная production responsibility. |
| Минимум 80% ценности? | Schedule registry, timezone-aware recurrence, next_run, unique occurrence, misfire policy, queue handoff, pause/update/cancel, metrics. |
| Когда overkill? | Distributed scheduler cluster для одного nightly maintenance script. |
| Trigger? | Recurring tasks, reminders, delayed execution, deadlines, user-configurable schedules or time-triggered business events. |
| Как измерить uplift? | Schedule lag, missed/duplicate occurrences, recovery after downtime, manual cron incidents, execution timeliness, operational simplicity. |
| Можно ли rule/tool/code вместо LLM-agent? | Да, полностью. Natural language may help author a schedule, but canonical time semantics and emission must be deterministic code. |
Execution belongs to queue/worker/workflow.
Restart не должен стирать future timers.
Каждый time slot имеет durable unique logical identity.
Calendar intent хранит IANA zone, не только UTC offset.
Skip/fire-once/catch-up/expire — business choice.
Crash between due and queue must not lose occurrence.
Occurrence remembers schedule definition that produced it.
Old schedule does not freeze permissions forever.
Technical jobs can stagger; exact human-time commitments cannot.
USER / SYSTEM / ADMIN
↓
DEFINE SCHEDULE
one-time
delay
interval
calendar recurrence
deadline
↓
STORE:
owner
tenant
target
rule
timezone
misfire policy
version
next_run
↓
TIME ADVANCES
↓
SCHEDULER FINDS DUE RULE
↓
ATOMIC CLAIM
↓
CREATE UNIQUE OCCURRENCE
schedule_id
schedule_version
scheduled_for
idempotency_key
↓
DURABLE HANDOFF
├─ №57 QUEUE / JOB
├─ №42 EVENT / TRIGGER
└─ №68 WORKFLOW SIGNAL
↓
ADVANCE NEXT_RUN
↓
OBSERVE
lag
overdue
misfire
duplicate
handoff failure
AFTER DOWNTIME:
reconcile due schedules
apply misfire policy
emit only intended logical occurrences
CORE PRINCIPLE:
SCHEDULER DOES NOT
"DO THE JOB".
SCHEDULER MAKES TIME
A DURABLE, VERSIONED,
IDEMPOTENT SIGNAL.
QUEUE ANSWERS:
"WHAT IS WAITING?"
WORKER ANSWERS:
"WHO EXECUTES?"
SCHEDULER ANSWERS:
"WHEN SHOULD THE NEXT
LOGICAL OCCURRENCE EXIST?"
A. Related ECC ideas. Context-as-cache, scoped memory, lifecycle hooks, selective capabilities, feature flags, deterministic enforcement, provider-neutral adapters and eval-gated learning are applied only where relevant to №58 Scheduler.
B–E. Existing boundary and placement. The existing conceptual boundary, class PRODUCTION, default CONDITIONAL and owner Production Fabric remain authoritative. Runtime/control/data/offline placement is unchanged; durable state stays outside model context.
F–H. Hooks and contracts. Use bounded PRE_MODEL/POST_MODEL, PRE_TOOL/POST_TOOL, CHECKPOINT and TASK_COMPLETED events as applicable. Illustrative fields and canonical contracts are defined in NEW_CONTRACTS_SPEC.md; no universal schema is implied.
I–J. Security and evaluation. Host-side schema, permission, secret, budget, idempotency and audit checks take precedence over LLM output. Optional mechanisms require a feature flag and WITH/WITHOUT ablation; measure quality, acceptance, correction, latency, cost, escalations and severe errors.
K–L. Task profiles and cross-references. A TaskProfile selects the relevant skill, tool/context slice, memory scope and enforcement profile independently from FAST/STANDARD/DEEP. See cross-reference map, hook spec and ablation plan. Provider adapters remain outside the core.