Длительные задания delayed_job остаются заблокированными после перезагрузки на Heroku
Когда рабочий из Heroku перезапускается (либо по команде, либо в результате развертывания), Heroku отправляет SIGTERM
в рабочий процесс. В случае delayed_job
сигнал SIGTERM
пойман, а затем рабочий прекращает выполнение после того, как текущее задание (если оно есть) остановлено,
Если работник добирается до конца, тогда Героку отправит SIGKILL
. В случае delayed_job
это оставляет заблокированное задание в базе данных, которое не будет получено другим работником.
Я бы хотел, чтобы задания заканчивались (если не было ошибок). Учитывая, что лучший способ приблизиться к этому?
Я вижу два варианта. Но я хотел бы получить другой ввод:
- Измените
delayed_job
, чтобы прекратить работу над текущим заданием (и отпустите блокировку), когда он получит SIGTERM
.
- Выясните (программный) способ обнаружения сиротских заблокированных заданий, а затем разблокируйте их.
Любые мысли?
Ответы
Ответ 1
TL;DR:
Поместите это вверху вашего метода работы:
begin
term_now = false
old_term_handler = trap 'TERM' do
term_now = true
old_term_handler.call
end
И
Убедитесь, что это вызывается не реже одного раза в десять секунд:
if term_now
puts 'told to terminate'
return true
end
И
В конце вашего метода поставьте это:
ensure
trap 'TERM', old_term_handler
end
Объяснение:
У меня была такая же проблема, и я пришел к этой статье Heroku.
Задание содержало внешний цикл, поэтому я следил за статьей и добавил trap('TERM')
и exit
. Однако delayed_job
выбирает значение failed with SystemExit
и помещает задачу как неудачную.
С SIGTERM
, теперь захваченным нашим trap
обработчик рабочего не вызывается, и вместо этого он немедленно перезапускает задание, а затем получает SIGKILL
a несколько секунд спустя. Вернуться к квадрату.
Я попробовал несколько альтернатив exit
:
-
A return true
отмечает успешное выполнение задания (и удаляет его из очереди), но испытывает такую же проблему, если в очереди ожидает очередное задание.
-
Вызов exit!
успешно завершит работу и рабочий, но это не позволит работнику удалить задание из очереди, так что у вас все еще есть проблема с "потерянными задачами".
Моим окончательным решением было то, что было дано в верхней части моего ответа, оно состоит из трех частей:
-
Прежде чем начать потенциально длинную работу, мы добавим новый обработчик прерываний для 'TERM'
, выполнив trap
(как описано в статье Heroku), и мы используем его для установки term_now = true
.
Но мы также должны захватить old_term_handler
, который установлен задерживаемый рабочий рабочий код (который возвращается trap
) и запомните call
it.
-
Мы по-прежнему должны гарантировать, что мы вернем управление в Delayed:Job:Worker
с достаточным временем для его очистки и выключения, поэтому мы должны проверять term_now
не менее (чуть ниже) каждые десять секунд и return
if это true
.
Вы можете либо return true
, либо return false
в зависимости от того, хотите ли вы, чтобы задание считалось успешным или нет.
-
Наконец, важно запомнить, чтобы удалить обработчик и установить обратно Delayed:Job:Worker
, когда вы закончите. Если вы этого не сделаете, вы держите ссылку на ту, которую мы добавили, что может привести к утечке памяти, если вы добавите ее поверх этого (например, когда рабочий снова запустит это задание).
Ответ 2
Прервать работу чисто на SIGTERM
Гораздо лучшее решение теперь встроено в delayed_job. Используйте этот параметр, чтобы вызвать исключение для сигналов TERM, добавив его в инициализатор:
Delayed::Worker.raise_signal_exceptions = :term
При такой настройке задание будет корректно очищаться и завершаться до того, как героку выдаст окончательный сигнал KILL, предназначенный для не взаимодействующих процессов:
Вам может потребоваться вызвать исключения для сигналов SIGTERM, Delayed :: Worker.raise_signal_exceptions =: term приведет к тому, что работник вызовет исключение SignalException, в результате чего выполняемое задание будет прервано и разблокировано, что сделает задание доступным для других работников. По умолчанию для этой опции установлено значение false.
Возможные значения для raise_signal_exceptions
:
-
false
- исключений не будет (по умолчанию) -
:term
- будет вызывать исключение только для сигналов TERM, но INT будет ожидать завершения текущей работы. -
true
- возбудит исключение по TERM и INT
Доступно с версии 3.0.5.
Смотрите этот коммит, где он был представлен.
Ответ 3
Новое на сайте, поэтому не могу комментировать сообщение Dave, и вам нужно добавить новый ответ.
Проблема с Dave заключается в том, что мои задачи длинные (минуты до 8 часов) и не повторяются вообще. Я не могу "обеспечить вызов" каждые 10 секунд.
Кроме того, я попробовал ответ Дейва, и задание всегда удаляется из очереди, независимо от того, что я возвращаю - true или false. Я не понимаю, как сохранить задание в очереди.
Смотрите этот запрос на растяжение. Я думаю, это может сработать для меня. Пожалуйста, не стесняйтесь комментировать его и поддерживать запрос на растяжение.
В настоящее время я экспериментирую с ловушкой, а затем спасаю сигнал выхода... Пока не повезло.
Ответ 4
Вот что означает max_run_time
: после истечения max_run_time
с момента блокировки задания другие процессы смогут получить блокировку.
См. это обсуждение из групп google
Ответ 5
Мне пришлось сделать это в нескольких местах, поэтому я создал модуль, который я вставляю в lib/, а затем запустите ExitOnTermSignal.execute {long_running_task} из моего блока выполнения замедленного задания.
# Exits whatever is currently running when a SIGTERM is received. Needed since
# Delayed::Job traps TERM, so it does not clean up a job properly if the
# process receives a SIGTERM then SIGKILL, as happens on Heroku.
module ExitOnTermSignal
def self.execute(&block)
original_term_handler = Signal.trap 'TERM' do
original_term_handler.call
# Easiest way to kill job immediately and having DJ mark it as failed:
exit
end
begin
yield
ensure
Signal.trap 'TERM', original_term_handler
end
end
end
Ответ 6
Я использую машину состояний для отслеживания выполнения заданий и делаю процесс idempotent, поэтому я могу многократно вызывать выполнение заданного задания/объекта и быть уверенным, что он не будет повторно применять деструктивное действие. Затем обновите задачу rake/delayed_job, чтобы освободить журнал в TERM.
Когда процесс перезагрузится, он будет продолжен, как предполагалось.