Вопрос по ruby, ruby-on-rails – Долгосрочные задания delayed_job остаются заблокированными после перезапуска на Heroku

13

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

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

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

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

Modify delayed_job to stop working on the current job (and release the lock) when it receives a SIGTERM. Figure out a (programmatic) way to detect orphaned locked jobs and then unlock them.

Какие-нибудь мысли?

Ваш Ответ

6   ответов
12

TLDR:

Поместите это в верхней части вашего метода работы:

begin
  term_now = false
  old_term_handler = trap 'TERM' do
    term_now = true
    old_term_handler.call
  end

AND

Убедитесь, что это вызывается как минимум раз в десять секунд:

  if term_now
    puts 'told to terminate'
    return true
  end

AND

В конце вашего метода, поместите это:

ensure
  trap 'TERM', old_term_handler
end

Explanation:

У меня была такая же проблема и натолкнулсяэта статья Heroku.

Работа содержала внешний цикл, поэтому я последовал за статьей и добавилtrap('TERM') а такжеexit, тем не мениеdelayed_job  выбирает это какfailed with SystemExit и помечает задачу как невыполненную.

СSIGTERM теперь в ловушке нашихtrap обработчик работника не вызван и вместо этого он немедленно перезапускает работу, а затем получаетSIGKILL несколько секунд спустя. Возвращается на круги своя.

Я попробовал несколько альтернативexit:

A return true marks the job as successful (and removes it from the queue), but suffers from the same problem if there's another job waiting in the queue.

Calling exit! will successfully exit the job and the worker, but it doesn't allow the worker to remove the job from the queue, so you still have the 'orphaned locked jobs' problem.

Мое окончательное решение было приведено в верхней части моего ответа, оно состоит из трех частей:

Before we start the potentially long job we add a new interrupt handler for 'TERM' by doing a trap (as described in the Heroku article), and we use it to set term_now = true.

But we must also grab the old_term_handler which the delayed job worker code set (which is returned by trap) and remember to call it.

We still must ensure that we return control to Delayed:Job:Worker with sufficient time for it to clean up and shutdown, so we should check term_now at least (just under) every ten seconds and return if it is true.

You can either return true or return false depending on whether you want the job to be considered successful or not.

Finally it is vital to remember to remove your handler and install back the Delayed:Job:Worker one when you have finished. If you fail to do this you will keep a dangling reference to the one we added, which can result in a memory leak if you add another one on top of that (for example, when the worker starts this job again).

@ M.ScottFord Я обновил ответ, предупреждаю, что мой предыдущий вызовет утечку памяти.
Я любил TL; DR :)
Спасибо за внимание. M. Scott Ford
Спасибо за решение! M. Scott Ford
2

поэтому я создал модуль, который вставляю в 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
delayed_job 3.0.5 теперь поддерживает опцию для создания исключения для сигнала TERM:github.com/collectiveidea/delayed_job/commit/…
4

max_run_time для: послеmax_run_time с момента блокировки задания другие процессы смогут получить блокировку.

Увидетьэто обсуждение от групп Google

4

поэтому не может комментировать пост Дэйва и должен добавить новый ответ.

Проблема, с которой я столкнулся при подходе Дейва, состоит в том, что мои задачи длинные (от минут до 8 часов) и не повторяются вообще. Я не могу "позвонить" каждые 10 секунд. Кроме того, я попробовал ответ Дейва, и задание всегда удаляется из очереди, независимо от того, что я возвращаю - истина или ложь. Мне неясно, как сохранить работу в очереди.

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

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

Вот мои мысли по поводу этого запроса на извлечение:github.com/collectiveidea/delayed_job/pull/…
Это не похоже на то, что оно отвечает на его вопрос. Это следует либо разместить как комментарий, либо как собственный вопрос.
Я не понимаю, что вы не можете комментировать, если у вас нет определенного количества повторений, что раздражает, поэтому я понимаю, почему вы разместили ответ. Тем не менее, я не знаю, как я не нашел этот запрос. Я предлагаю вам перефразировать ваш ответ так: "посмотреть этот запрос на извлечение" & quot; потому что я считаю, что это представляет собой ответ на вопрос. Я также собираюсь опубликовать эту заявку сейчас.
1

чтобы отслеживать ход выполнения заданий, и делаю процесс идемпотентным, чтобы я мог несколько раз вызвать выполнение заданного задания / объекта и быть уверенным, что он не применяет деструктивное действие повторно. Затем обновите задачу rake / delayed_job, чтобы освободить журнал в TERM.

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

26
Abort Job Cleanly on SIGTERM

параметр, чтобы вызвать исключение для сигналов TERM, добавив его в инициализатор:

Delayed::Worker.raise_signal_exceptions = :term

При такой настройке задание будет корректно очищаться и завершаться до того, как героку выдаст окончательный сигнал KILL, предназначенный для не взаимодействующих процессов:

You may need to raise exceptions on SIGTERM signals, Delayed::Worker.raise_signal_exceptions = :term will cause the worker to raise a SignalException causing the running job to abort and be unlocked, which makes the job available to other workers. The default for this option is false.

Возможные значения дляraise_signal_exceptions являются:

false - No exceptions will be raised (Default) :term - Will only raise an exception on TERM signals but INT will wait for the current job to finish. true - Will raise an exception on TERM and INT

Available since Version 3.0.5.

Увидеть:https://github.com/collectiveidea/delayed_job/commit/90579c3047099b6a58595d4025ab0f4b7f0aa67a

На самом деле не представляется возможным сделать такой тип связи распределенных систем «атомарным». без значительной реинжиниринга обеих систем. Я бы добавил, что в категорию "дерьмо случается". Иногда будет отправлено несколько писем. Это решение, вероятно, лучшее, что может быть сделано.
Спасибо за публикацию. Я должен это проверить. M. Scott Ford
Вопрос: должны ли мы проверять или обрабатывать это исключение в нашей работе, или работа будет "терпеть неудачу"? и быть оставленным разблокированным все из этой единственной строки конфигурации?
где api для отправки почты не обеспечивает идемпотентности (как это делает Stripe при добавлении идентификатора запроса), вы можете подделать его самостоятельно,attaching data to messagesа именно идентификатор вашей работы, которая отправляет почту (и некоторое пространство имен / префикс или uuid, сохраненный в работе), затем используйте API почтовой службы для поиска почты, отправленной с идентификатором с этим идентификатором перед повторной отправкой. Предполагая, что один работник выполняет работу за раз (с блокировкой или арендой), он должен гарантировать отправку ровно один раз, если поиск API не может пропустить недавно отправленные сообщения?
Как это решает проблемы, когда Mailer связывается с SMTP-сервером, полностью отправил запрос и сервер получил его, а затемSignalException перед тем, как Ruby сможет закрыть соединение и завершить ответ? Похоже, что тогда вы снова будете работать. Кажется, что нужно сделать особый акцент на том, чтобы сделать работу на 100% атомарной.

Похожие вопросы