从Heroku上的Resque :: TermException或SIGTERM中彻底恢复

Bri*_*ong 11 heroku resque resque-retry

当我们重新启动或部署我们得到的失败队列数Resque工作与任一Resque::TermException (SIGTERM)Resque::DirtyExit.

我们TERM_CHILD=1 RESQUE_TERM_TIMEOUT=10在Procfile中使用new ,所以我们的工作线看起来像:

worker:  TERM_CHILD=1 RESQUE_TERM_TIMEOUT=10 bundle exec rake environment resque:work QUEUE=critical,high,low
Run Code Online (Sandbox Code Playgroud)

我们也在使用resque-retry我认为可能会自动重试这两个例外情况?但它似乎不是.

所以我猜两个问题:

  1. 我们可以Resque::TermException在每个工作中手动救援,并使用它来重新安排工作.但对于所有工作,是否有一种干净的方法可以做到这一点?即使是猴子补丁.
  2. 不应该resque-retry自动重试这些?你能想出为什么不会这样的原因吗?

谢谢!

编辑:在不到10秒的时间内完成所有工作,这似乎是不合理的.在运行Resque :: DirtyExit异常时,似乎需要有一种方法来自动重新排队这些作业.

ilo*_*aly 6

我也遇到了这个问题。事实证明,HerokuSIGTERM不仅向父进程发送信号,还向所有分叉进程发送信号。这不是 Resque 期望的导致RESQUE_PRE_SHUTDOWN_TIMEOUT跳过的逻辑,迫使作业在没有任何时间尝试完成作业的情况下执行。

Heroku 在SIGTERM发出a 后给工人 30 秒的时间来正常关闭。在大多数情况下,如果作业无法完成,这是足够的时间来完成作业,并留出一些缓冲时间以将作业重新排队到 Resque。但是,要使用所有这些时间,您需要设置RESQUE_PRE_SHUTDOWN_TIMEOUTRESQUE_TERM_TIMEOUTenv vars 以及补丁 Resque 以正确响应SIGTERM发送到分叉进程。

这是一个 gem,它修补了 resque 并更详细地解释了这个问题:

https://github.com/iloveitaly/resque-heroku-signals


Dan*_*der 5

这将是一个由两部分组成的答案,首先是寻址Resque::TermException,然后是Resque::DirtyExit

TermException

值得注意的是,如果您使用ActiveJobRails 7 或更高版本,则可以使用retry_on和方法来处理. 您可以在您的工作类别中编写以下内容:discard_onResque::TermException

retry_on(::Resque::TermException, wait: 2.minutes, attempts: 4)
Run Code Online (Sandbox Code Playgroud)

或者

discard_on(::Resque::TermException)
Run Code Online (Sandbox Code Playgroud)

这里需要注意的是,如果您使用的是 Rails 7 之前的版本,则需要添加一些自定义代码才能使其正常工作。

原因是它Resque::TermException不继承自StandardError(它继承自SignalException,来源: https: //github.com/resque/resque/blob/master/lib/resque/errors.rb#L26)和Rails 7之前的版本retry_on并且discard_on仅处理异常继承自StandardError.

这是 Rails 7 提交,它更改了此设置以适用于所有异常子类:https://github.com/rails/rails/commit/142ae54e54ac81a0f62eaa43c3c280307cf2127a

因此,如果您想使用retry_on处理Resque::TermException早于 7 的 Rails 版本,您有以下几种选择:

  1. 猴子补丁TermException,使其继承自StandardError.
  2. rescue向您的方法添加一条perform显式查找Resque::TermException或其祖先之一的语句(例如SignalExceptionException)。
  3. perform_now使用 Rails 7 版本修补 的实现(这是我在代码库中所做的)。

TermException下面介绍了如何通过将 a 添加rescue到作业的perform方法来重试 a :

class MyJob < ActiveJob::Base
  prepend RetryOnTermination

  # ActiveJob's `retry_on` and `discard_on` methods don't handle 
  `TermException`
  # because it inherits from `SignalException` rather than `StandardError`.
  module RetryOnTermination
    def perform(*args, **kwargs)
     super
    rescue Resque::TermException
      Rails.logger.info("Retrying #{self.class.name} due to Resque::TermException")
      self.class.set(wait: 2.minutes).perform_later(*args, **kwargs)
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

或者,您可以使用 Rails 7 的定义,perform_now将其添加到您的作业类中:

  # FIXME: Here we override the Rails 6 implementation of this method with the
  # Rails 7 implementation in order to be able to retry/discard exceptions that
  # don't inherit from StandardError, such as `Resque::TermException`.
  #
  # When we upgrade to Rails 7 we should remove this.
  # Latest stable Rails (7 as of this writing) source: https://github.com/rails/rails/blob/main/activejob/lib/active_job/execution.rb
  # Rails 6.1 source: https://github.com/rails/rails/blob/6-1-stable/activejob/lib/active_job/execution.rb
  # Rails 6.0 source (same code as 6.1): https://github.com/rails/rails/blob/6-0-stable/activejob/lib/active_job/execution.rb
  #
  # NOTE: I've made a minor change to the Rails 7 implementation, I've removed
  # the line `ActiveSupport::ExecutionContext[:job] = self`, because `ExecutionContext`
  # isn't defined prior to Rails 7.
  def perform_now
    # Guard against jobs that were persisted before we started counting executions by zeroing out nil counters
    self.executions = (executions || 0) + 1

    deserialize_arguments_if_needed

    run_callbacks :perform do
      perform(*arguments)
    end
  rescue Exception => exception
    rescue_with_handler(exception) || raise
  end
Run Code Online (Sandbox Code Playgroud)

DirtyExit

Resque::DirtyExit在父进程中引发,而不是在实际执行作业代码的分叉子进程中引发。这意味着您工作中用于挽救或重试这些异常的任何代码都将不起作用。请参阅发生这种情况的这些代码行:

  1. https://github.com/resque/resque/blob/master/lib/resque/worker.rb#L940
  2. https://github.com/resque/resque/blob/master/lib/resque/job.rb#L234
  3. https://github.com/resque/resque/blob/master/lib/resque/job.rb#L285

但幸运的是,Resque 提供了一种处理这个问题的机制,即作业钩子,特别是钩子on_failure: https: //github.com/resque/resque/blob/master/docs/HOOKS.md#job-hooks

这些文档的引用:

on_failure:如果执行作业(或挂钩)时发生任何异常,则使用异常和作业参数进行调用,这包括 Resque::DirtyExit。

这些文档中的一个示例介绍了如何使用钩子重试异常:

module RetriedJob
  def on_failure_retry(e, *args)
    Logger.info "Performing #{self} caused an exception (#{e}). Retrying..."
    Resque.enqueue self, *args
  end
end

class MyJob
  extend RetriedJob
end
Run Code Online (Sandbox Code Playgroud)


jfe*_*ust 1

您的重新请求作业是否需要超过 10 秒才能完成?如果作业在发送初始 SIGTERM 后 10 秒内完成,那么应该没问题。尝试将工作分解成更小的部分,以便更快地完成。

另外,您可以让您的工作人员重新排队作业,执行以下操作: https: //gist.github.com/mrrooijen/3719427