16

当 Heroku 工作程序重新启动时(根据命令或部署的结果),Heroku 将发送SIGTERM到工作程序进程。在 的情况下delayed_jobSIGTERM 信号被捕获,然后工作人员在当前作业(如果有)停止后停止执行。

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

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

我看到两个选项。但我想得到其他输入:

  1. 修改delayed_job以在收到SIGTERM.
  2. 找出一种(程序化的)方法来检测孤立的锁定作业,然后解锁它们。

有什么想法吗?

4

6 回答 6

32

在 SIGTERM 上彻底中止作业

delay_job 现在内置了一个更好的解决方案。通过在初始化程序中添加此设置,使用此设置在 TERM 信号上引发异常:

Delayed::Worker.raise_signal_exceptions = :term

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

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

的可能值为raise_signal_exceptions

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

从版本 3.0.5 开始可用。

在引入它的地方查看这个提交

于 2013-05-29T10:47:16.253 回答
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

解释:

我遇到了同样的问题,并遇到了这篇 Heroku 文章

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

现在SIGTERM被我们trap 的工人处理程序困住了,它不会被调用,而是立即重新启动作业,然后在SIGKILL几秒钟后得到。回到原点。

我尝试了一些替代方案exit

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

  • 调用exit!将成功退出作业和工作人员,它不允许工作人员从队列中删除作业,因此您仍然存在“孤立锁定作业”问题。

我的最终解决方案是我的答案顶部给出的解决方案,它包括三个部分:

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

    我们还必须抓住old_term_handler哪个延迟作业工人代码集(由 返回trap记住call它。

  2. 我们仍然必须确保我们Delayed:Job:Worker有足够的时间将控制权返回给它,以便它清理和关闭,所以我们应该term_now至少(略低于)每十秒检查一次,return如果它是true

    您可以return truereturn false取决于您是否希望该工作被认为是成功的。

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

于 2012-09-26T17:00:37.767 回答
5

新网站,因此无法评论 Dave 的帖子,需要添加新答案。

我对 Dave 方法的问题是我的任务很长(几分钟到 8 小时),而且根本没有重复性。我不能“确保每 10 秒打一次电话”。此外,我已经尝试了 Dave 的回答,并且无论我返回什么,无论是真还是假,该作业总是从队列中删除。我不清楚如何让工作保持在队列中。

看到这个这个拉请求。我认为这可能对我有用。请随时对其发表评论并支持拉取请求。

我目前正在试验一个陷阱然后拯救出口信号......到目前为止还没有运气。

于 2012-10-04T04:49:28.480 回答
4

这就是为什么max_run_timemax_run_time从作业被锁定的时间过去后,其他进程将能够获得锁。

从谷歌组看到这个讨论

于 2012-09-26T01:17:38.810 回答
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
于 2012-12-12T23:48:43.260 回答
1

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

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

于 2012-05-04T08:35:03.557 回答