Сигналы и deploy

Предпосылки: Sidekiq: retry и обработка ошибок, сигналы.

Retry и обработка ошибок | Дизайн задач

Retry обрабатывает ошибки: email-сервис вернул 500 → подождать → попробовать снова. Но причина ошибки может быть не во внешнем сервисе, а в самом коде — баг, который нужно исправить и задеплоить. Deploy означает перезапуск Sidekiq-процесса. В этот момент несколько потоков могут выполнять задачи. Как остановить процесс, не потеряв их?

Сигналы Sidekiq

Sidekiq обрабатывает четыре сигнала, каждый с конкретной ролью:

TSTP — «тихий режим» (quiet). Процесс перестаёт забирать новые задачи из Redis, но дорабатывает текущие. Это первый шаг graceful shutdown: прекратить приём, завершить начатое.

TERM и INT — начать shutdown. Sidekiq ждёт завершения текущих задач в течение timeout (по умолчанию 25 секунд). Если задачи не завершились за это время — принудительная остановка потоков и bulk_requeue: незавершённые задачи возвращаются в Redis. Задача, прерванная на середине, выполнится заново — нужна идемпотентность.

TTIN — вывести backtrace всех потоков в лог. Полезно для отладки: какой поток завис, на каком вызове.

В многопоточном процессе обработка сигналов требует осторожности: обработчик сигнала ограничен async-signal-safe операциями — нельзя захватывать мьютексы, выделять память, писать в лог. Sidekiq решает это через self-pipe trick: обработчик записывает байт в pipe (единственная операция — write, которая async-signal-safe), а основной поток читает из pipe в event loop и выполняет всю логику без ограничений.

Последовательность deploy

Правильный deploy — два шага:

1. TSTP → процесс перестаёт забирать задачи
2. Deploy нового кода (занимает время)
3. TERM → старый процесс начинает shutdown
4. Новый процесс запускается с новым кодом

Зачем два шага вместо одного TERM? Между TERM и фактической остановкой есть только timeout (25 секунд). Если deploy занимает 30 секунд, а задача — 20, одного TERM недостаточно: задача не завершится до timeout. TSTP в начале deploy даёт задачам время на завершение, пока код ещё компилируется и копируется.

# Начало deploy
kill -TSTP $(cat tmp/pids/sidekiq.pid)
 
# ... deploy происходит ...
 
# Конец deploy
kill -TERM $(cat tmp/pids/sidekiq.pid)
# Подождать timeout + запас
sleep 30
# Запустить новый процесс
bundle exec sidekiq -d

Настройка timeout

# config/sidekiq.yml
:timeout: 25  # секунд на завершение после TERM

Timeout — компромисс. Слишком короткий — задачи прерываются и возвращаются в очередь (дубликаты). Слишком длинный — deploy затягивается. 25 секунд — разумный default для большинства задач.

Lifecycle events

Sidekiq предоставляет хуки на ключевые моменты жизненного цикла процесса:

Sidekiq.configure_server do |config|
  config.on(:startup)  { puts "Process ready" }
  config.on(:quiet)    { puts "TSTP received, finishing current jobs" }
  config.on(:shutdown) { puts "TERM received, shutting down" }
end

SIGKILL: когда graceful shutdown невозможен

SIGKILL (kill -9) нельзя перехватить — процесс убивается мгновенно. Никакого bulk_requeue, никакого graceful shutdown. В OSS: задачи, забранные через BRPOP, потеряны. В Pro с SuperFetch: задачи остаются в приватных очередях и будут восстановлены другими процессами (orphan recovery).

OOM-killer ядра Linux отправляет SIGKILL — то есть ведёт себя как kill -9. Для Sidekiq это означает: если процесс потребляет слишком много памяти, задачи пропадут без предупреждения.


Один job работает корректно: retry, shutdown, deploy — покрыто. Теперь представим задачу крупнее: обработка заказа включает резервирование товара, списание оплаты, email и аналитику. Четыре действия в одном perform. Аналитика падает — retry перезапускает весь job, включая списание оплаты. Двойное списание. Решение в дизайне задач.


Retry и обработка ошибок | Дизайн задач

Sources