Как понять, что фоновая задача справилась

Фоновый импорт избавляет от ручного обновления данных, если его результату можно доверять без проверки каждого запуска. При этом список публикаций на сайте может оставаться прежним по двум разным причинам: у источника ничего не изменилось или обновление давно не удаётся. Чтобы отличить одно от другого, нужно заранее определить, что задача должна подтвердить перед успешным завершением.

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

В заметке про cron и Capistrano я показывал, как запускать импорт по расписанию и хранить это расписание рядом с проектом. Оно избавляет от ручного запуска, но проверить результат за нас не может.

В таком примере у результата есть несколько проверяемых свойств. Ответ соответствует ожидаемому формату, а его записи относятся к запрошенному источнику. Новая копия записана целиком, и именно её теперь читает приложение. Для большого импорта к этому добавится проверка полноты: получить первую страницу списка ещё не значит получить весь список.

Количество новых записей при этом может быть нулевым. Условие «нашли хоть что-нибудь» здесь не подходит: внешний сервис вполне способен подтвердить, что изменений нет. Это тоже результат успешной проверки.

Следующая попытка

Допустим, данные удалось получить, но соединение оборвалось в момент записи результата. Мы не знаем, успела ли запись сохраниться: мог потеряться только ответ. При следующем запуске задача снова встретит те же публикации. Если каждый раз создавать их заново, повтор после сбоя начнёт размножать записи. Значит, заранее нужен способ узнать уже обработанную публикацию, например устойчивый идентификатор от источника.

Для задачи, которая только заменяет снимок ленты, можно ещё раз подготовить полную копию и заменить предыдущую. Приложение должно переключиться на неё целиком: пока копия готовится, оно читает предыдущую. Но если импорт заодно отправляет уведомления, ситуация меняется. Запись публикации и отправка сообщения становятся двумя отдельными действиями. Сбой между ними способен привести к повторному уведомлению или к тому, что его не будет вовсе. Я бы не прятал это различие за общей функцией с названием «импортировать».

С повторными попытками тоже полезно остановиться вовремя. Если внешний сервис не отвечает, запускать тот же запрос без паузы бессмысленно. Число повторов и время ожидания должны быть ограничены. После исчерпания попыток ошибка остаётся ошибкой; её нельзя превращать в удачное завершение только потому, что продолжать сейчас некуда.

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

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

Что останется после завершения

Ночной импорт обычно работает без присмотра: разработчик не проверяет результат каждого запуска. Поэтому полезно отдельно хранить время последней попытки и время последнего подтверждённого успеха. По первому видно, что задача запускается. По второму видно, как давно мы проверяли данные у источника. Успешная проверка обновляет это время, даже если новых публикаций нет. Дата последней публикации для такого контроля не подходит: автор может просто ничего не писать.

Для повседневной проверки мне понятнее всего допустимое отставание. Если лента должна обновляться хотя бы раз в четыре часа, более старый результат уже требует внимания, независимо от количества записей об ошибках. Четыре часа здесь условны: для подборки фотографий и срочных исправлений в опубликованной статье допустимые сроки будут разными. Частота запуска сама по себе их не определяет.

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

Перед тем как оставить импорт работать самостоятельно, полезно воспроизвести отказ источника и ошибку записи, повторить обработку одних и тех же данных и запустить две попытки одновременно. После каждой проверки должно быть понятно, что сейчас читает сайт и откуда разработчик узнает о неудаче. Мне важно, чтобы без постоянного просмотра журнала можно было отличить «ничего нового» от «давно не удаётся обновиться».