Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 16 из 25 ~18 мин +20 XP

Wrappers: Recover, DelayIfStillRunning, Skip

Cron wrappers — механизм обёрток в robfig/cron v3, который добавляет поведение всем задачам через одну настройку при создании планировщика: Recover гасит панику вместо падения процесса, DelayIfStillRunning задерживает перекрывшийся тик, а SkipIfStillRunning пропускает его. Обёртка — это функция высшего порядка типа JobWrapper, принимающая задачу и возвращающая обёрнутую задачу, а опция WithChain собирает такие функции в цепочку. Разберём механику цепочки, порядок обёрток, где Recover обязателен, а где выбор между Delay и Skip решает продуктовый вопрос: терять тики или копить очередь.

Содержание урока · 10 разделов

Сценарий, который знают все, кто выкатил планировщик без обёрток: через месяц в проде ночной отчёт падает на новой конфигурации внешнего API, паника вылетает из горутины тика — и процесс умирает целиком вместе с HTTP-сервером и остальными задачами. Или второй вариант: задача синхронизации при деградации внешней системы начинает работать дольше интервала, запускается в двух, трёх, десяти копиях, и каждая пишет в базу. Обе проблемы лечатся одной механикой — обёртками; Recover и guard уже упоминались как спасение в уроках про AddFunc и конкурентность. Здесь разберём механизм целиком: как устроен JobWrapper, в каком порядке применяется цепочка, что делают три обёртки из коробки и как написать собственную.

Cron wrappers: цепочка WithChain и тип JobWrapper#

Обёртка в robfig/cron v3 — это тип type JobWrapper func(job Job) Job: функция, которая принимает задачу и возвращает задачу. Опция cron.WithChain(w1, w2, w3) при создании планировщика объявляет список обёрток, и каждая зарегистрированная задача оборачивается всей цепочкой: AddFunc и AddJob из урока про AddJob принимают обычный Job, а внутрь среза записей уходит уже обёрнутый. Порядок применения обратный: последняя обёртка в списке оказывается ближе всего к вашей задаче, первая — снаружи. Поэтому Recover ставят первой: внешняя обёртка ловит панику, вылетевшую из любой внутренней. Cron wrappers — декоратор поверх интерфейса Job: поведение приписывается задаче снаружи, а сам код задачи остаётся чистым. Граница тоже важна: цепочка фиксируется при создании cron.Cron — поменять её на живом планировщике нельзя, только собрать новый, как в уроке про динамическое расписание. Зато любая задача, добавленная на лету, получает те же обёртки автоматически.

Планировщик с защитой на старте

GO
logger := cron.PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))

c := cron.New(cron.WithChain(
    cron.Recover(logger),
    cron.SkipIfStillRunning(logger),
))

c.AddFunc("*/15 * * * *", pullOrders)
c.AddFunc("0 2 * * *", nightlyDigest)
c.Start()

Две обёртки закрывают два самых дорогих вида инцидентов: панику одной задачи, которая раньше роняла процесс, и перекрытие запусков, которое раньше плодило параллельные копии. Логгер передаётся в обе обёртки: Recover пишет в него панику со стеком, SkipIfStillRunning — событие пропущенного тика. Выбор PrintfLogger здесь условен: подойдёт любой тип с методами Info и Error, а разговор про логгеры — тема следующего урока.

Проверка: порядок обёрток в цепочке

WithChain(cron.Recover(logger), cron.SkipIfStillRunning(logger)): какая обёртка снаружи и что это даёт?

Recover: паника в задаче вместо падения сервиса#

Обёртка cron.Recover(logger) оборачивает каждую задачу отложенным recover и при панике делает две вещи: пишет в логгер сообщение с ошибкой и стеком, после чего возвращает управление — планировщик продолжает тикать, остальные задачи не тронуты. Без неё паника из горутины тика по умолчанию убивает всю программу, и это уже было разжевано в уроке про AddFunc: robfig/cron не ставит защиту сам. Что Recover не делает: он не исправляет баг и не снимает с вас ответственность. Паника — это ошибка программирования: nil-указатель, выход за границы, обращение к закрытому ресурсу. Обёртка меняет лишь масштаб аварии: вместо падения сервиса в три ночи — запись в логе и работа задачи на следующем тике. Поэтому связка такая: Recover на старте как страховка, а по каждой записи паники — разбор и фикс в коде. Механику panic и recover в чистом виде разбирает урок про панику; в cron поверх неё добавляется только вопрос, куда записать событие, — вопрос логгера из следующего урока.

Своя обёртка: замер длительности

GO
func Timed(name string, next cron.Job) cron.Job {
    return cron.FuncJob(func() {
        start := time.Now()
        next.Run()
        d := time.Since(start)
        if d > 30*time.Second {
            log.Printf("%s: %v", name, d)
        }
    })
}

c := cron.New(cron.WithChain(
    cron.Recover(logger),
    Timed("sync", cron.SkipIfStillRunning(logger)),
))

JobWrapper — обычная функция высшего порядка, поэтому свои обёртки пишутся в три строки: замер до вызова next.Run(), замер после, решение по результату. Здесь задача синхронизации получает Recover снаружи, затем замер длительности, и только потом guard от перекрытий. На замерах такого рода строится наблюдаемость: в продакшене вместо log.Printf ставят метрику-гистограмму, о чём отдельный урок про метрики задач.

DelayIfStillRunning и SkipIfStillRunning: перекрытия под контролем#

Обе обёртки решают вопрос перекрытий из урока про перекрытие запусков, но с противоположной семантикой. DelayIfStillRunning сериализует запуски: если предыдущий запуск ещё жив, новый тик ждёт его завершения и выполняется после — каждый тик гарантированно отработает, но при длительной деградации очередь догоняющих запусков растёт, и после восстановления они посыпятся подряд. SkipIfStillRunning пропускает тик, пока предыдущий запуск не закончился: перекрытий нет, но потерянные тики не восполняются. Если обёртке передан логгер, событие пропуска или задержки попадает в лог, поэтому решение о том, что делать с шумом, остаётся за вами. Правило выбора — продуктовое: для выгрузок и отчётов честнее Skip, потому что догоняющая серия из двенадцати запусков бесполезна — нужен один свежий; для задач, где важен каждый тик, например инкрементальных синхронизаций без чекпоинта, подходит Delay. Обе обёртки работают на уровне одной задачи: защита не распространяется на соседние задачи.

Проверка: выбор обёртки для отчёта

Ночной отчёт раз в сутки иногда не укладывается в интервал: сбор данных затягивается. Какая обёртка уместнее?

Что напечатает цепочка обёрток?

Прочитайте код и выберите, что он выведет:

wrap := func(name string, next func()) func() {
    return func() {
        fmt.Println(name, "вход")
        next()
        fmt.Println(name, "выход")
    }
}
job := func() { fmt.Println("тик") }
job = wrap("первая", job)
job = wrap("вторая", job)
job()

Обёртки из коробки и их семантика#

ОбёрткаЧто делаетКогда уместнаЦена
cron.Recover(logger)гасит панику, пишет ошибку со стекомна старте для всех задач, без вариантовскрывает баг, если нет разбора записей
cron.DelayIfStillRunning(logger)ждёт завершения предыдущего запускакаждый тик важен, деградации короткиеочередь догоняющих запусков растёт
cron.SkipIfStillRunning(logger)пропускает тик, пока задача живаотчёты и выгрузки со свежими даннымипотерянные тики не восполняются
своя JobWrapperлюбое поведение: замер, повтор, лимитне закрыто коробочными обёрткамипишется и тестируется руками

Свои JobWrapper: метрики, ретраи, лимиты#

Коробочных обёрток три, а тип JobWrapper — публичный, поэтому всё, что не покрылось из коробки, пишется как обычная функция высшего порядка. Типичные свои обёртки: замер длительности с отправкой в метрики, повтор запуска при ошибке, лимит параллельных запусков через семафор, аудит-запись о начале и конце. Строить их лучше поверх материала соседних уроков: повтор запусков при ошибке — ретраи из урока про ретраи, замер длительности — материал урока про метрики задач, а лимит параллелизма — очередь воркеров из урока про конкурентность. Два правила к своим обёрткам. Первое: обёртка не должна знать о бизнесе — она получает Job и возвращает Job, всё бизнес-поведение живёт внутри самой задачи, иначе декоратор превращается в нечитаемую свалку. Второе: обёртка обязана быть честной к панике — отложенные действия должны выполняться и при панике ниже по цепочке, поэтому recover-безопасные обёртки тестируют задачей, которая паникует.

Чеклист обёрток планировщика#

  • Recover первым элементом WithChain на каждом планировщике сервиса.
  • Выбор между Delay и Skip по продуктовому вопросу: терять тики или копить очередь.
  • Логгер передан в обе обёртки: события пропусков и паник видны в одном месте.
  • Своя обёртка не знает о бизнесе: получает Job, возвращает Job.
  • Тест на панику в задаче: процесс жив, тик записан в лог, планировщик тикает дальше.
  • Цепочка не меняется на лету: смена обёрток — новый Cron и старт из урока про Start и Stop.

Recover не оправдание: спорное про честный код#

Спорная привычка, которую стоит оспорить: считать Recover готовой архитектурой вместо части работы с ошибками. Обёртка гасит панику, но не возвращает задачу к незавершённому делу: если отчёт упал посреди транзакции, Recover не откатит её, не почистит временные файлы и не перезапустит выгрузку до следующего тика. Зрелая настройка выглядит иначе: Recover как подстраховка плюс честная обработка ошибок внутри задач — ошибки через return и лог, а не через panic; дедлайны через контекст из урока про контекст; идемпотентность, чтобы повторный тик после пропуска ничего не ломал. Отдельный вопрос — мониторинг: если Recover молча глотает паники каждую ночь, обёртка превратилась в маскировку деградации. События паник должны быть видимыми: лог с уровнем ошибки и стека, а лучше — счётчик в метриках, по которому ставится алерт. Тогда Recover выполняет свою роль: авария не распространяется, а её причина не живёт в проде месяцами.

Соберите цепочку обёрток

Впишите опцию цепочки и обе обёртки: защита от паники и пропуск перекрытий.

c := cron.New(cron.( cron.(logger), cron.(logger), ))

Расставьте сборку защищённого планировщика

Расставьте строки в порядке файла: логгер, создание с цепочкой, две обёртки, закрытие опции и регистрация задачи.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: обёртки задач

Термин

JobWrapper

нажмите, чтобы перевернуть
Определение

Тип func(job Job) Job: функция высшего порядка, добавляющая задаче поведение снаружи. Свои обёртки — обычные замыкания вокруг Run.

1 / 6

Обёртки — это декоратор над интерфейсом Job, приписанный всем задачам разом: WithChain собирает JobWrapper в цепочку, первая обёртка в списке снаружи, поэтому Recover ставится первой и ловит паники из всего, что ниже. DelayIfStillRunning и SkipIfStillRunning закрывают перекрытия с противоположными семантиками — копить очередь или терять тики, — и выбор между ними продуктовый. Свои обёртки пишутся как замыкания вокруг next.Run() и наследуют те же правила: никакого бизнеса, честное поведение при панике, тест на живучесть планировщика. Дальше по маршруту: логирование задач — куда попадают события обёрток, метрики задач — как превратить замеры длительности в гистограммы. Если возвращались с пропуска — перекрытие запусков объясняет, откуда берётся проблема, которую закрывают Skip и Delay, а graceful shutdown покажет, как обёртки ведут себя при остановке. Полный маршрут — на странице курса. В домашнем задании — своя обёртка замера и тест на панику.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • JobWrapper — func(job Job) Job: обёртка добавляет задаче поведение снаружи, не трогая её код.
  • WithChain применяет обёртки в обратном порядке: первая в списке — самая внешняя.
  • Recover ставится первым: гасит панику любой задачи, пишет ошибку со стеком и не даёт упасть процессу.
  • DelayIfStillRunning копит очередь догоняющих запусков, SkipIfStillRunning теряет тики: выбор продуктовый.
  • Свои обёртки — замыкания вокруг Run без бизнес-логики: замеры, ретраи, лимиты параллелизма.
  • Цепочка фиксируется при создании Cron: новые задачи получают обёртки автоматически, смена — только через новый планировщик.

Домашнее задание

Соберите планировщик с цепочкой Recover и SkipIfStillRunning и напишите собственную обёртку Timed, которая замеряет длительность задачи и печатает предупреждение при превышении порога. Добавьте тест, в котором задача паникует: процесс жив, планировщик тикает дальше, событие попало в лог. Затем заведите задачу, которая спит дольше своего интервала, и проверьте в логах разницу между Skip и Delay: сколько тиков потеряно, сколько отложено. Финал — измените порядок обёрток и убедитесь тестом, что паника из внутренней обёртки больше не ловится.

Личные заметкиПусто
Хранятся только в вашем браузере (Cron · Wrappers: Recover, DelayIfStillRunning, Skip)

Вопросы и ответы по уроку

Обязательно ли ставить Recover на каждый планировщик?

Без Recover паника одной задачи падает на весь процесс, включая HTTP-сервер и остальные задачи. Обёртка не заменяет разбор ошибок, но меняет масштаб аварии: вместо падения сервиса — запись в логе. Ставить её на старте дешевле, чем после первого ночного инцидента.

Можно ли менять цепочку обёрток на живом планировщике?

Нет: цепочка фиксируется при создании cron.Cron. Чтобы поменять обёртки, собирают новый планировщик, регистрируют задачи и переключаются на него в рамках graceful shutdown. Задачи, добавленные на лету, цепочку текущего планировщика наследуют.

Чем DelayIfStillRunning отличается от SkipIfStillRunning?

Delay ставит перекрывшийся тик в очередь и выполняет после завершения предыдущего запуска — тики не теряются, но накапливаются. Skip пропускает тик, пока задача жива, — потерянные тики восполняются следующим периодом. Для отчётов и выгрузок обычно честнее Skip, для синхронизаций без чекпоинта — Delay.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Slog25 уроков~9 чпрактика25 уроков уже доступны

Логирование в Go: структурные логи на slog

Курс по slog в Go: уровни и атрибуты, JSON-логи, свой Handler, производительность, секреты и прод. 25 уроков с примерами и проектом.

  • Логировать структурированно: уровни, ключи-атрибуты, группы
  • Выбирать формат вывода: TextHandler, JSONHandler, свой Handler
  • Мигрировать со старого пакета log без переписывания всей кодовой базы
  • Держать производительность: Enabled, предвычисление, горячий путь