Пишем cron-выражения, которые не стреляют сюрпризами
Пять полей и их диапазоны, имена и числа, ловушка ИЛИ между днём месяца и днём недели, воскресенье как 0 или 7, провалы и наложения летнего времени и валидация по предпросмотру ближайших запусков перед деплоем.
Пять полей, читаем слева направо
Классическое cron-выражение — пять полей через пробел: минута (0–59), час (0–23), день месяца (1–31), месяц (1–12) и день недели (0–7). Каждое поле принимает одно значение, список через запятую, диапазон через дефис, шаг после слэша или звёздочку для всех значений.
Большинство cron-багов — не экзотический синтаксис, а обычное поле, прочитанное небрежно: шаг, сбрасывающийся на границе диапазона, или поле часов, написанное в двенадцатичасовом формате для системы, которая считает до двадцати трёх.
- СпискиСписки выбирают дискретные значения: 0,30 в поле минут срабатывает в начале часа и на половине.
- Диапазоны и шагиДиапазоны выбирают интервал: 9-17 в поле часов покрывает рабочий день, а шаг прореживает его — 9-17/2 срабатывает каждый второй час внутри интервала.
- Звёздочка совпадает со всемЗвёздочка — не «игнорировать поле»; она активно совпадает со всеми значениями, что становится важным, как только взаимодействуют два поля дат.
Имена — это числа с документацией
Поля месяца и дня недели принимают трёхбуквенные английские имена — от JAN до DEC и от SUN до SAT — как псевдонимы чисел. 0 9 * JAN MON и 0 9 * 1 1 — одно и то же расписание, но именованная версия объясняет себя следующему читателю и устойчива к правкам «на единицу мимо».
- Без учёта регистраИмена парсятся без учёта регистра — mon, Mon и MON работают одинаково, — но верхний регистр читается как намеренный выбор.
- Имена в диапазонахИмена работают и в диапазонах, и в списках: MON-FRI — стандартный будничный интервал, а JAN,APR,JUL,OCT выбирает квартальные месяцы.
- Читайте объяснениеСоздание и объяснение Cron нормализует выражение и объясняет каждое поле отдельно — «month: every value», «weekday: 1, 2, 3, 4, 5», — так что вы можете убедиться, что парсер прочитал имена так, как вы имели в виду.
Как взаимодействуют два поля дня
Этот инструмент следует правилам дней в стиле Vixie: если ни одно из двух полей дня не начинается с *, достаточно совпадения любого из них. 0 9 13 * FRI означает не «пятницу, 13-е», а каждое 13-е число месяца и каждую пятницу.
Вывод объяснителя говорит об этом прямо: семантика помечена как dom-dow-or рядом с вычисленными запусками, поэтому выражение, которое сработает гораздо чаще задуманного, показывает свою суть до того, как попадёт в crontab.
- Шаги со звёздочкой тоже ограничиваютЕсли поле дня начинается с *, включая шаг вроде */2, инструмент требует совпадения обоих полей дня; ограничение шага при этом действует.
- Без звёздочки в начале действует ИЛИЕсли ни одно поле дня не начинается с *, инструмент применяет ИЛИ: может совпасть число месяца либо день недели.
- И требует обходаЧтобы попасть в настоящую пятницу 13-е, пусть cron выбирает 13-е число, а сама команда проверяет день недели — либо используйте планировщик с семантикой И, которой классический cron не является.
Воскресенье — это 0, и обычно ещё 7
Поле дня недели нумерует воскресенье как 0, понедельник как 1 и так далее до субботы как 6. Большинство реализаций принимает и 7 как второе воскресенье для тех, кто считает с понедельника, — парсер Toolars внутренне нормализует 7 в 0, — но некоторые системы принимают только одно из двух, а где-то существует нумерация в стиле ISO, начинающая понедельник с 1.
- 0 и 70 и 7 — оба воскресенье там, где 7 принимается; писать 0 — переносимый выбор между планировщиками.
- Тихая ошибка на единицуДень недели 7 в системе, которая его отвергает, громко падает при деплое, но день, ошибочно прочитанный как «седьмой, считая с понедельника», тихо падает не в тот день.
- Перепроверяйте платформуКогда выражение переезжает между реализациями — системный crontab, контейнерная платформа, планировщик CI — переподтвердите нумерацию дней недели, а не предполагайте, что она пережила дорогу.
Летнее время гнёт расписание
Переходы на летнее время создают отсутствующие и повторяющиеся местные минуты. В этом превью несуществующее время, например 02:30 при переводе часов вперёд, пропускается, а повторившаяся минута при переводе назад показывается дважды. Реальный планировщик может выполнить пропущенное задание позже или подавить дубль; проверьте его правила.
- Пропавший часВесенний пропуск: превью не находит минуты 02:30; некоторые планировщики выполняют пропущенное задание после перехода.
- Удвоенный часОсеннее наложение: превью может показать оба совпадающих момента, а реальный планировщик — подавить второй запуск.
- В UTC нет DSTЧистый выход — UTC: переходов на летнее время нет, поэтому 30 2 * * * означает один и тот же момент каждый день года. Объяснитель предлагает UTC, локальный пояс устройства и именованные пояса вроде America/New_York и Europe/London.
Валидируйте по ближайшим запускам, а не по надежде
Разница между правильным выражением и правдоподобно выглядящим — список конкретных моментов срабатывания. Создание и объяснение Cron вычисляет десять ближайших запусков от текущего момента в выбранном часовом поясе, просматривая до 366 дней вперёд, и считает в фоновом воркере, так что тяжёлое выражение не подвесит вкладку.
- Читайте запускиСверьте первые запуски с намерением: «будни в 09:00» должны показать понедельник–пятницу в девять, а суббота в списке — это ловушка ИЛИ, объявившая о себе.
- Пересеките переводВыберите именованную временную зону и просмотрите следующие запуски, если ближайший переход на летнее время попадает в 366-дневное превью. Отсчёт начинается сейчас; произвольную начальную дату выбрать нельзя.
- Явные ошибкиНеверные выражения вызывают ошибки. Ноль совпадений за 366 дней даёт ошибку ограничения поиска; от одного до девяти совпадений возвращаются как частичный результат, а не пустой список.
Частые вопросы
Почему cron сработал в дни, которых я не ожидал?
Воскресенье — это 0 или 7 в cron?
Как сделать расписание независимым от летнего времени?
Расписания и паттерны разделяют одну дисциплину.
И то и другое — крошечные строки с огромным радиусом поражения: изучите цикл «написал — проверил — отрефакторил» для регулярных выражений, работающий в локальном воркере с таймаутом.