Подтвердить что ты не робот

Длительные задания delayed_job остаются заблокированными после перезагрузки на Heroku

Когда рабочий из Heroku перезапускается (либо по команде, либо в результате развертывания), Heroku отправляет SIGTERM в рабочий процесс. В случае delayed_job сигнал SIGTERM пойман, а затем рабочий прекращает выполнение после того, как текущее задание (если оно есть) остановлено,

Если работник добирается до конца, тогда Героку отправит SIGKILL. В случае delayed_job это оставляет заблокированное задание в базе данных, которое не будет получено другим работником.

Я бы хотел, чтобы задания заканчивались (если не было ошибок). Учитывая, что лучший способ приблизиться к этому?

Я вижу два варианта. Но я хотел бы получить другой ввод:

  • Измените delayed_job, чтобы прекратить работу над текущим заданием (и отпустите блокировку), когда он получит SIGTERM.
  • Выясните (программный) способ обнаружения сиротских заблокированных заданий, а затем разблокируйте их.

Любые мысли?

4b9b3361

Ответ 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.

Когда процесс перезагрузится, он будет продолжен, как предполагалось.