Почему cron запускается и по дню месяца, и по дню недели
Разбираем ловушку пяти полей cron: почему 30 4 1 * 1 срабатывает каждое первое число и каждый понедельник, а не только в первый понедельник.
Задача 30 4 1 * 1 должна была запускать отчёт в первый понедельник месяца.
Вместо этого отчёт пришёл 1 августа, потом 3 августа, затем снова через неделю.
Cron не опоздал и не задвоил очередь. Расписание изначально означало другое.
В обычном пятичастном crontab поля идут так:
минута час день-месяца месяц день-недели
30 4 1 * 1
Последние два ограничения читаются не как «первое число, которое оказалось понедельником», а как «первое число или понедельник». Именно такое правило описано в crontab(5): когда ограничены оба поля дня, достаточно совпадения одного из них.
Откройте генератор cron, вставьте 30 4 1 * 1 и посмотрите
пять ближайших запусков. Превью здесь полезнее словесной расшифровки. Календарь
сразу выдаёт лишние понедельники, которые легко пропустить при ревью одной
строки.
Звёздочка меняет правило
Нужен запуск только первого числа месяца в 04:30? Выражение будет таким:
30 4 1 * *
Нужен каждый понедельник в 04:30?
30 4 * * 1
В обоих случаях одно поле дня оставлено без ограничения через *, поэтому
расписанием управляет второе. Ловушка появляется, когда конкретное значение
задано сразу и в дне месяца, и в дне недели.
Если первое число само выпало на понедельник, одна строка crontab всё равно даёт один запуск в эту минуту. OR расширяет набор подходящих дат, а не создаёт две независимые задачи.
А как записать первый понедельник месяца
Чистое пятичастное выражение не умеет потребовать одновременное совпадение этих
двух полей. Практичный вариант для Unix cron: запускать задачу каждый
понедельник, а проверку число месяца <= 7 оставить небольшому скрипту-обёртке.
Так условие можно покрыть тестом и не прятать shell-арифметику в одной строке
crontab.
Другой вариант - планировщик с отдельным правилом календаря. Но сначала
проверьте его диалект. Quartz добавляет секунды и ?, GitHub Actions принимает
пять полей, AWS EventBridge использует свой формат. Строка, валидная в одном
месте, не обязана означать то же самое в другом.
Devdeck намеренно разбирает традиционный формат из пяти полей. Поддерживаются
списки, диапазоны, шаги и короткие английские имена месяцев и дней недели.
Спецсимволы Quartz вроде ?, L, W и # сюда не относятся.
Проверка перед деплоем
Для расписания из pull request я сначала смотрю не на перевод «человеческим
языком», а на ближайшие реальные даты. У 30 4 1 * 1 в списке быстро появятся
и понедельники, и первые числа. После исправления на 30 4 1 * * останутся
только первые числа месяца.
Генератор считает превью локально в текущей вкладке и использует часовой пояс
браузера. Production cron может жить в UTC, внутри контейнера или с CRON_TZ,
поэтому финальная проверка всё равно делается рядом с конфигурацией сервиса.
Граница локальных браузерных утилит подробнее разобрана в статье
про инструменты разработчика и Network tab.
Сохраните при ревью не только выражение, но и две-три ожидаемые даты запуска. Для cron это короткий тест, который ловит ошибку лучше любого уверенного комментария рядом со строкой.
Вопросы
Как cron объединяет день месяца и день недели?
В традиционном пятичастном crontab два ограниченных поля соединяются через OR: запуск происходит при совпадении дня месяца или дня недели.
Что означает 30 4 1 * 1?
Запуск в 04:30 каждое первое число месяца, а также каждый понедельник. Это не расписание только для первого понедельника месяца.
Можно ли проверить такое выражение в devdeck?
Да. Генератор cron разбирает пять полей и показывает пять ближайших запусков в часовом поясе браузера. Он не проверяет диалекты Quartz, AWS EventBridge или конкретной CI-системы.