Дизайн cron jobs: scheduling, overlap, observability
Cron vs scheduler service, что делать с overlap'ами, как избежать missed runs, observability и monitoring, failure handling.
Действуй как senior infra инженер. Спроектируй систему scheduled jobs: {{jobs}}. Инфраструктура: {{stack}}.
1. Выбор механизма
| Опция | Когда брать | Подвох |
|---|---|---|
| crontab на VM | Один сервер, < 5 jobs, без HA | Сервер упал → missed runs, не знаешь |
| k8s CronJob | k8s уже есть, нужно HA | Сложно с long-running, missed-run policy надо настраивать |
| Cloud scheduler (EventBridge / Cloud Scheduler) | Serverless / managed | Vendor lock-in, debug сложнее |
| Sidekiq-cron / Celery beat / app-level scheduler | Уже есть job queue | Только один scheduler instance (single point of failure если без leader election) |
| Temporal / Airflow | DAG, retry chain, > 20 jobs с зависимостями | Operational overhead, не для одиночных tasks |
Дефолт для production: managed cloud scheduler или k8s CronJob + leader election (если бизнес-логика в коде).
2. Overlap protection
Что делать если предыдущий run ещё идёт, а пришло время следующего?
- forbid — пропустить новый запуск. Дефолт для idempotent jobs где главное не перегрузить.
- replace — убить старый, запустить новый. Только если новый запуск делает то же, что старый, но "actual".
- allow — параллельно. Только если jobs действительно независимы (по data partition).
Реализация:
- k8s:
concurrencyPolicy: Forbid. - Application-level: distributed lock (Redis SETNX с TTL = expected duration × 2). Lock per job name.
- TTL обязателен — иначе зависший job блокирует все будущие runs forever.
3. Missed runs
Что если scheduler был down 2 часа и пропустил 4 запуска "каждые 30 минут"?
- Catch-up policy явно прописана. Запускать всё пропущенное сразу — может перегрузить систему. Запускать только последний — потерять данные. По умолчанию — запустить один catch-up + alert.
- startingDeadlineSeconds в k8s CronJob: если scheduler опоздал > N секунд, missed run считается потерянным, не запускается.
- Для critical jobs — отдельный monitor (см. п. 5) который алертит если job не запускался > expected interval × 1.5.
4. Failure handling
- Идемпотентность. Каждый job спроектирован так, что повторный запуск с теми же входами безопасен. Без этого retry — рулетка.
- Retry policy: k8s CronJob
backoffLimit: 3+ exponential backoff в коде job'а на внешние вызовы (network/DB). - Permanent failure: после exhausted retries — alert + state
failed_atв БД. Не молчаливо. - Partial failure для batch job: обрабатываемые записи помечать индивидуально как processed/failed, чтобы следующий run забрал только failed без повтора processed.
5. Observability
- Heartbeat / dead man's switch. Сервис (Healthchecks.io, Cronitor, или свой): job в начале делает
POST /start, в конце —POST /finish. Если/finishне пришёл за expected window — алерт. Если/startне пришёл к ожидаемому моменту — алерт (missed run). - Metrics per job:
last_started_at,last_finished_at,last_duration_seconds,last_status,consecutive_failures. - Structured logs: job_name, run_id (UUID), started_at, status в каждой строке. Чтобы grep'ом построить timeline одного run'а.
- Dashboard: грид всех jobs со статусом последнего run + время с последнего успеха. На красный — Pager.
6. Resource limits
- CPU/memory limits в k8s/ECS. Чтобы один сошедший с ума job не уронил соседей.
- Timeout на сам job:
activeDeadlineSecondsв k8s. Лучше упасть на 30-й минуте с ошибкой, чем висеть 6 часов. - Stagger time для тяжёлых jobs: не запускать 10 jobs ровно в 00:00 UTC — DB / external API не справится. Размазать на 0/5/10/15/...
Формат вывода
## Recommended mechanism
<выбор + почему>
## Per-job spec
| Name | Schedule | Concurrency | Timeout | Idempotent? | Monitor |
| daily-cleanup | 0 3 * * * | Forbid | 30m | yes | hc-cleanup |
...
## Distributed lock pseudocode (если app-level)
Anti-patterns
- ❌ Cron на одной VM для бизнес-критичных jobs → сервер упал → ничего не запустилось → молча.
- ❌ Без overlap protection → два инстанса делают одно и то же → дублирование данных / коррупция.
- ❌ Lock без TTL → один зависший job → все future runs заблокированы навсегда.
- ❌ Без heartbeat monitor → "почему отчёты не приходят неделю?" обнаруживается через клиента.
- ❌ Не идемпотентный job + retry → каждый retry удваивает damage.
- ❌ 10 jobs в 00:00 UTC → thundering herd → DB колом.
- ❌ activeDeadline > expected duration × 10 → зависший job висит часами, никто не знает.
- ❌ Catch-up "выполнить всё пропущенное сразу" без лимита → если scheduler стоял сутки → 48 запусков параллельно → overload.
- ❌ Cron-выражение без комментария что оно значит →
*/13 * * * *через год никто не вспомнит зачем.
Мониторинг и алёрты
Что мерить, какие алёрты ставить, как не превратить on-call в ад.
Multi-agent: координатор и специалисты
Архитектура из координатора и специализированных агентов: передача контекста, дедупликация, race conditions.
Новый subagent или новый skill: что выбрать
Decision tree: создавать ли отдельного агента или достаточно skill. Критерии — контекст, переиспользование, frequency, complexity.