在Heroku上重启后,长时间运行的delayed_job作业保持锁定状态

时间:2012-05-03 19:30:06

标签: ruby-on-rails ruby heroku delayed-job

当重新启动Heroku工作程序时(无论是在命令上还是作为部署的结果),Heroku都会向工作进程发送SIGTERM。在delayed_job的情况下,SIGTERM signal is caught然后工作程序在当前作业(如果有)停止后停止执行。

如果工人需要很长时间才能完成,那么Heroku将发送SIGKILL。在delayed_job的情况下,这会在数据库中留下一个锁定的作业,不会被另一个工作人员接收。

我想确保工作最终完成(除非出现错误)。鉴于此,最好的方法是什么?

我看到两个选项。但我想得到其他意见:

  1. 修改delayed_job以在收到SIGTERM时停止处理当前作业(并解除锁定)。
  2. 找出(程序化的)方法来检测孤立的锁定作业,然后解锁它们。
  3. 有什么想法吗?

6 个答案:

答案 0 :(得分:27)

在SIGTERM上彻底中止工作

现在更好的解决方案内置于delayed_job中。使用此设置通过在初始化程序中添加此项来在TERM信号上抛出异常:

Delayed::Worker.raise_signal_exceptions = :term

使用该设置,作业将在heroku发出针对非合作进程的最终KILL信号之前正确清理并退出:

  

您可能需要在SIGTERM信号上引发异常,Delayed :: Worker.raise_signal_exceptions =:term将导致worker引发SignalException,导致正在运行的作业中止并被解锁,这使得该作业可供其他工作人员使用。此选项的默认值为false。

raise_signal_exceptions的可能值为:

  • false - 不会引发任何例外(默认)
  • :term - 仅在TERM信号上引发异常,但INT将等待当前作业完成。
  • true - 将在TERM和INT上引发异常

自版本3.0.5起可用。

请参阅this commit介绍它的位置。

答案 1 :(得分:12)

<强> TLDR:

将此放在工作方法的顶部:

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

<强>解释

我遇到了同样的问题并且遇到了this Heroku article

作业包含外部循环,因此我按照文章添加了trap('TERM')exit。但是delayed_job选择failed with SystemExit并将任务标记为失败。

SIGTERM现在被我们的trap the worker's handler isn't called所困,而是立即重新启动作业,然后在几秒钟后获得SIGKILL。回到原点。

我尝试了exit的一些替代方案:

  • return true将作业标记为成功(并将其从队列中删除),但如果队列中有另一个作业正在等待则会遇到同样的问题。

  • 调用exit!将成功退出作业和它不允许工作人员从队列中删除作业,因此您仍然拥有'孤儿锁定工作的问题。

我的最终解决方案是在我的答案顶部给出的解决方案,它由三部分组成:

  1. 在我们开始这个可能很长的工作之前,我们通过执行'TERM'(如Heroku文章中所述)为trap添加一个新的中断处理程序,我们用它来设置{{ 1}}。

    但是我们还必须抓住term_now = true设置old_term_handler设置(trapcall Delayed:Job:Worker它。

  2. 我们仍然必须确保我们将控制权返回term_now并有足够的时间进行清理和关闭,因此我们应该每隔十秒检查return至少(刚刚下) true如果是return true

    您可以return falseDelayed:Job:Worker,具体取决于您是否希望将该作业视为成功。

  3. 最后,记住删除处理程序并在完成后重新安装{{1}},这是至关重要。如果你没有这样做,你将保留对我们添加的那个的悬空引用,如果你在其上添加另一个(例如,当工人再次启动这个工作时),可能会导致内存泄漏。

答案 2 :(得分:5)

网站新手,所以无法对Dave的帖子发表评论,需要添加新的答案。

我与戴夫的方法有关的问题是我的任务很长(分钟长达8小时),根本不重复。我不能每隔10秒“确保打电话”。 此外,我尝试过戴夫的答案,无论我返回什么,无论是真还是假,工作总是从队列中删除。我不清楚如何将工作留在队列中。

this pull request。我认为这对我有用。请随时评论并支持拉取请求。

我正在尝试陷阱,然后拯救退出信号......到目前为止没有运气。

答案 3 :(得分:4)

这就是max_run_time的用途:从作业被锁定之后经过max_run_time之后,其他进程将能够获得锁定。

请参阅this discussion from google groups

答案 4 :(得分: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

答案 5 :(得分:1)

我使用状态机来跟踪作业的进度,并使进程具有幂等性,以便我可以多次调用给定作业/对象上的执行,并确信它不会重新应用破坏性操作。然后更新rake task / delayed_job以释放TERM上的日志。

当流程重新启动时,它将按预期继续。