03 / ГИДЫ
Пишем 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», — так что вы можете убедиться, что парсер прочитал имена так, как вы имели в виду.
Два поля дат объединяются по ИЛИ
Это самый дорогой сюрприз cron: когда ограничены и день месяца, и день недели — ни одно поле не звёздочка — расписание срабатывает при совпадении любого из них. 0 9 13 * FRI не значит «пятница, 13-е»; это 13-е число каждого месяца и каждая пятница.
Вывод объяснителя говорит об этом прямо: семантика помечена как dom-dow-or рядом с вычисленными запусками, поэтому выражение, которое сработает гораздо чаще задуманного, показывает свою суть до того, как попадёт в crontab.
- Одна звёздочка — простоЕсли одно из полей дат — звёздочка, второе правит в одиночку: интуитивное поведение, которое большинство ожидает повсюду.
- Оба ограничены — это ИЛИОба ограничены — значит ИЛИ: классическая семантика Vixie cron, которой следует большинство Unix-планировщиков, запускает задачу при совпадении дня месяца или дня недели.
- И требует обходаЧтобы попасть в настоящую пятницу 13-е, пусть cron выбирает 13-е число, а сама команда проверяет день недели — либо используйте планировщик с семантикой И, которой классический cron не является.
Воскресенье — это 0, и обычно ещё 7
Поле дня недели нумерует воскресенье как 0, понедельник как 1 и так далее до субботы как 6. Большинство реализаций принимает и 7 как второе воскресенье для тех, кто считает с понедельника, — парсер Toolars внутренне нормализует 7 в 0, — но некоторые системы принимают только одно из двух, а где-то существует нумерация в стиле ISO, начинающая понедельник с 1.
- 0 и 70 и 7 — оба воскресенье там, где 7 принимается; писать 0 — переносимый выбор между планировщиками.
- Тихая ошибка на единицуДень недели 7 в системе, которая его отвергает, громко падает при деплое, но день, ошибочно прочитанный как «седьмой, считая с понедельника», тихо падает не в тот день.
- Перепроверяйте платформуКогда выражение переезжает между реализациями — системный crontab, контейнерная платформа, планировщик CI — переподтвердите нумерацию дней недели, а не предполагайте, что она пережила дорогу.
Летнее время гнёт расписание
Cron срабатывает по показаниям настенных часов в часовом поясе планировщика, а дважды в год эти часы капризничают. В весеннем провале локальное время вроде 02:30 просто не существует; при осеннем переводе оно существует дважды. Расписание, прибитое к этой минуте, пропустит запуск в марте и может сработать дважды в ноябре.
- Пропавший часВесенний провал: задача на 02:30 в ночь перевода не имеет совпадающего времени, поэтому ничего не срабатывает — ни ошибки, ни повтора, просто пропущенный запуск.
- Удвоенный часОсеннее наложение: одно и то же время наступает дважды, и реализации расходятся в том, запускать задачу один раз или два; считайте, что два, если не проверили обратное.
- В UTC нет DSTЧистый выход — UTC: переходов на летнее время нет, поэтому 30 2 * * * означает один и тот же момент каждый день года. Объяснитель предлагает UTC, локальный пояс устройства и именованные пояса вроде America/New_York и Europe/London.
Валидируйте по ближайшим запускам, а не по надежде
Разница между правильным выражением и правдоподобно выглядящим — список конкретных моментов срабатывания. Создание и объяснение Cron вычисляет десять ближайших запусков от текущего момента в выбранном часовом поясе, просматривая до 366 дней вперёд, и считает в фоновом воркере, так что тяжёлое выражение не подвесит вкладку.
- Читайте запускиСверьте первые запуски с намерением: «будни в 09:00» должны показать понедельник–пятницу в девять, а суббота в списке — это ловушка ИЛИ, объявившая о себе.
- Пересеките переводНамеренно пересеките границу летнего времени: выберите дату в марте или ноябре в именованном поясе и убедитесь, что расписание ведёт себя как ожидается, когда часы переводят.
- Явные ошибкиСбои явные: невалидное поле, неверное число полей или расписание, не способное дать десять запусков в окне поиска, получают именованную ошибку вместо молча пустого списка.
Расписания и паттерны разделяют одну дисциплину.
И то и другое — крошечные строки с огромным радиусом поражения: изучите цикл «написал — проверил — отрефакторил» для регулярных выражений, работающий в локальном воркере с таймаутом.