vegman.dev
Все статьи

Расписания и вотчдоги: почему исправная задача молчит

2026-05-28статья

В шесть утра телефон вибрирует: «Очередь пуста, новых материалов нет». В половине седьмого - то же сообщение. В семь - снова. К концу суток их сорок восемь, и вы делаете единственное разумное: отключаете уведомления от этой задачи. Через неделю очередь встает по-настоящему - и об этом вам сообщает человек, который ждал результат.
Планировщик (сервис, который запускает задачи по расписанию) отработал ровно так, как его попросили. Ошибка была раньше: задачу спроектировали как «раз в полчаса рассказывай, что видишь», а не как «молчи, пока все в порядке».
Дальше - как я развожу регулярные задачи по типам, почему у большинства из них правильный вывод пустой, кому вообще позволено будить человека и в каком порядке задача должна помечать работу сделанной, чтобы данные не терялись.

Три вида регулярных задач

В моих системах регулярные задачи бывают трех видов, и требования у них разные.

  • Сбор данных. Задача ходит наружу - в источник, в почтовый ящик, в чужой API - и забирает новое. Главный риск здесь не шум, а потеря: то, что забрано и не обработано, исчезает бесследно.
  • Проверка состояния. Задача сравнивает то, что есть сейчас, с тем, что было известно в прошлый раз. Главный риск - шум: такая задача запускается часто и в норме не находит ничего.
  • Доставка результата человеку. Единственный вид, которому вообще позволено писать в мессенджер. Главный риск - дубли: один и тот же факт, доставленный дважды, обесценивает канал.

Смешивать их в одной задаче заманчиво и почти всегда плохо. Смешанная задача наследует худшие требования всех трех сразу: она обязана не терять, не шуметь и не дублировать - а при сбое непонятно, какая ее часть успела отработать.

Молчание как нормальное состояние

Главное правило: у задачи, которая обычно ничего не находит, вывод должен быть пустым. Не «пока ничего», не «проверка завершена, 0 записей», а буквально пустой вывод. Пустой вывод - это молчание.
Арифметика тут простая и безжалостная. Задача, проверяющая очередь каждые полчаса и честно сообщающая «пока ничего», разбудит человека 48 раз в сутки. Такую задачу отключают через день - и вместе с шумом отключают те редкие сообщения, ради которых все затевалось.
Второе следствие того же правила: у задачи должно быть состояние, иначе она сообщит об одном и том же событии столько раз, сколько раз запустится. Если объект перешел в проблемное состояние и остался в нем, это одно событие, а не одно событие на каждый прогон. Мне почти всегда хватает простого файла соответствия «объект - последнее известное состояние»: задача сравнивает текущее с записанным, сообщает только при расхождении и обновляет запись. База данных для этого не нужна.

Как отличить молчание от смерти

У молчания есть неприятное свойство: сломанная задача выглядит ровно так же, как работающая. И то и другое - тишина.
Поэтому у каждой регулярной задачи должны быть видны три вещи: время последнего запуска, статус и текст последней ошибки. Без этого вы не отличите «все спокойно» от «задача падает на второй строке уже месяц».
Сюда же - практическая деталь про интервалы. У планировщика обычно есть разница между «через 30 минут один раз» и «каждые 30 минут». Задача, созданная как одноразовая, тихо отработает ровно один раз и больше никогда. Ни ошибки, ни падения - просто вечная тишина, неотличимая от нормы. Я проверяю это не по коду создания, а по времени последнего запуска: если оно не двигается, задача не «спокойна», она мертва.

flowchart LR
  subgraph before["Было: проверка через модель"]
    A[Пустая очередь] --> B[Вызов модели на форматирование]
    B --> C[1007 вызовов]
    C --> D[45 процентов всех расходов на модели]
  end
  subgraph after["Стало: проверка скриптом"]
    E[Пустая очередь] --> F[Скрипт сравнивает условие]
    F --> G[Ноль вызовов модели]
    G --> H[Пустой вывод, то есть молчание]
  end
Один и тот же результат при пустой очереди: проверка через модель и проверка скриптом

Кто имеет право будить человека

Второе правило: детерминированная проверка - это скрипт, а не агент с моделью. Детерминированная - значит, что при одних и тех же входных данных результат всегда один и тот же: сравнить два значения, посчитать записи, проверить дату. Здесь нечего интерпретировать, здесь есть условие.
У меня однажды регулярная задача с моделью, поставленной на форматирование, съела 45 процентов всех расходов на модели. Она сделала 1007 вызовов - при пустой очереди. То есть заплачено было целиком за то, чтобы аккуратно оформить «ничего не найдено».
Отсюда разделение обязанностей, которое я держу жестко:

  • Скрипт решает, есть ли повод. Это условие: дешевое, предсказуемое, проверяемое глазами.
  • Модель вызывается только тогда, когда повод уже есть, и только на ту часть, где нужна интерпретация, - например, свести разрозненные записи в связный текст.
  • Человека будит только доставка, и только с фактом, которого он еще не видел.

Проверять «а не пора ли» с помощью модели - это платить за вопрос, ответ на который вычисляется сравнением.

flowchart TD
  A[Прогон сборщика] --> B[Читаем непомеченные источники]
  B --> C[Обработка и вызов модели]
  C -->|Успех| D[Помечаем просмотренными]
  C -->|Ошибка| E[Состояние не меняем]
  D --> F[Обновляем файл состояния]
  E --> G[Сигналы уедут на следующем прогоне]
Пометка «просмотрено» - последний шаг прогона, а не первый

Порядок операций: когда помечать сделанным

Третье правило вытекает из первого вида задач - сбора. У меня был сборщик сигналов, который сначала помечал источники просмотренными, а потом вызывал модель. Логика понятная: «взял в работу - отметил, чтобы не взять снова». Пока вызов не сбоил, все выглядело правильно.
При сбое вызова сигналы были уже помечены. Повторный прогон их не видел - для него они просмотрены. Данные терялись навсегда и молча: задача завершалась с ошибкой, ошибку чинили, а то, что провалилось в щель между пометкой и вызовом, восстановить было уже неоткуда.
Правильный порядок обратный: помечать только после успешной обработки. Прочитать, обработать, убедиться, что результат получен, и только тогда обновить состояние.
Пятое правило - та же мысль с другой стороны: ошибка задачи не должна менять состояние. Если публикация не удалась, материал остается в очереди и уедет на следующем прогоне. Это скучное, тихое поведение - и именно оно позволяет чинить задачу спокойно, а не в режиме спасения данных.
Побочный эффект приятный: задачу становится безопасно перезапускать. Прогон, который упал, ничего не испортил. Прогон, который прошел, второй раз то же самое не отправит, потому что состояние уже обновлено.

Применить у себя

Что проверить на практике

  • Возьмите каждую свою регулярную задачу и назовите ее вид: сбор, проверка или доставка. Если одним словом не получается - задача делает три дела, ее стоит разделить.
  • Запустите задачу на заведомо пустых данных и посмотрите на вывод. Если там есть хоть один символ - это будущий спам.
  • Найдите все вызовы модели внутри задачи и по каждому спросите: это интерпретация или условие? Условия переносите в скрипт.
  • Посчитайте вызовы модели за сутки и сопоставьте с числом реальных находок. Расхождение в сотни раз - это счет за формулировку «ничего нет».
  • Найдите в коде место, где ставится пометка «обработано», и убедитесь, что оно стоит после успешной обработки, а не до нее.
  • Уроните задачу намеренно - оборвите вызов, подсуньте недоступный источник - и проверьте, что после падения данные остались в очереди.
  • Откройте то, чем создается расписание, и убедитесь, что это «каждые N минут», а не «через N минут один раз».
  • Посмотрите на время последнего запуска у всех задач. Все, что не двигалось дольше своего интервала, считайте сломанным, пока не доказано обратное.

Как понять, что получилось

Критерий один, и он неудобно простой: за сутки нормальной работы система не написала вам ничего - и вы при этом спокойны, потому что можете за минуту открыть список задач и увидеть, что все они запускались вовремя и завершились без ошибок.
Если система молчит, а спокойствия нет - не хватает наблюдаемости. Если система пишет, а вы уже не читаете - не хватает молчания. Получилось тогда, когда есть и то и другое: тишина в норме и сообщение ровно в тот момент, когда изменилось что-то, чего вы еще не видели.