在Heroku上重新启动后,长时间运行的delayed_job作业仍然被锁定

18
当Heroku的worker被重新启动(无论是命令还是部署操作),Heroku会向worker进程发送SIGTERM。在delayed_job中,SIGTERM信号被捕获,然后worker在当前工作完成后停止执行。
如果worker需要执行的任务时间太长,那么Heroku会发送SIGKILL信号。在delayed_job中,这将导致一个被锁定的任务留在数据库中,不会被其他worker选择。
我想确保任务最终能够完成(除非出现错误)。在这种情况下,应该如何处理?
我有两个选项,请给予意见:
1.修改delayed_job,使其在收到SIGTERM时停止当前任务(并释放锁)。
2.找出一种(程序化)检测孤立锁定任务并解锁它们的方法。
您有什么想法吗?
6个回答

33

在 SIGTERM 时干净地终止作业

现在,delayed_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以来可用。

请参见此提交介绍了这个功能。


感谢您的发布。我得去看看。 - M. Scott Ford
4
如何处理当一个发件人与SMTP服务器通信时,已经完全发送了请求并且服务器已经接收到它,但在Ruby能够关闭连接并完成响应之前出现SignalException的问题?看起来你将不得不重新运行该作业。似乎需要更加强调使作业100%原子化。 - maletor
6
如果不对两个系统进行重大的重新设计,就不可能使这种分布式系统通信变得“原子化”。我将其归类为“有时会出现错误”。有时可能会发送多封邮件。这种解决方案可能是可以做到的最好方法。 - Alex Neth
问题:我们是否需要在作业中检查或处理此异常,还是该作业将“失败”并且从这一行配置中解锁所有内容? - Joshua Pinter
当邮件发送API不提供幂等性时(例如Stripe在添加请求ID时),您可以通过附加数据到消息来模拟它,即附加发送邮件的作业ID(和一些命名空间/前缀或保存在作业中的UUID),然后使用邮件服务的API搜索以前发送的带有该ID的邮件,再次发送。假设每次只有1个工作程序执行该作业(带有锁定或租约),这应该确保仅发送一次,除非API搜索可能会错过最近发送的消息? - nruth
显示剩余2条评论

12

简而言之:

将以下代码放在您的作业方法顶部:

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

说明:

我遇到了同样的问题,然后在Heroku文章中找到了解决方法。

这个作业包含一个外循环,所以我按照文章中的步骤添加了一个trap('TERM')exit。但是,delayed_job将其视为SystemExit失败并将任务标记为失败。

现在,由于我们trap捕获了SIGTERM,所以worker的处理程序未被调用,而是立即重新启动作业,然后几秒钟后得到SIGKILL。回到起点。

我尝试了一些替代方法来使用exit

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

  • 调用exit!将成功退出作业和worker,但是它不允许worker从队列中删除作业,因此您仍然有“孤立的锁定作业”问题。

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

  1. 在我们开始潜在的长时间作业之前,我们通过trap(如Heroku文章中所述)为'TERM'添加了一个新的中断处理程序,并使用它来设置term_now = true

    但是我们还必须抓住delayed job worker代码设置的old_term_handler(由trap返回),并记得调用它。

  2. 我们仍然必须确保在足够的时间内将控制权返回给Delayed:Job:Worker以进行清理和关闭,因此我们应该至少每十秒检查一次term_now,如果truereturn

    您可以根据是否要将作业视为成功,选择return truereturn false

  3. 最后,非常重要的是在完成后记得删除您的处理程序并重新安装Delayed:Job:Worker。如果未能执行此操作,则将保留对我们添加的处理程序的悬空引用,如果在其上添加另一个引用(例如,当worker再次启动此作业时),则可能导致内存泄漏。


@M.ScottFord 我已更新答案,但需要注意的是,之前的答案会导致内存泄漏。 - davetapley

5

我是新来的,无法评论Dave的帖子,并需要添加一个新答案。

我对Dave的方法有问题,因为我的任务很长(几分钟到8个小时),并且根本不重复。我不能每10秒“确保调用”一次。 此外,我尝试了Dave的答案,作业总是从队列中删除,无论我返回什么- true或false。我不清楚如何将作业保留在队列中。

请参见此pull请求。我认为这可能适合我。请随意发表评论并支持拉取请求。

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


这似乎并没有回答他的问题。应该将其重新发布为评论或单独的问题。 - Austin Henley
我没意识到你必须拥有一定的声望才能发表评论,这很烦人,所以我理解你为什么要发布一个回答。话虽如此,我不知道为什么我没有找到那个拉取请求。我建议你重新措辞你的回答,改成“看看这个拉取请求”,因为我认为它确实是对问题的回答。我现在也会在那个拉取请求上发布。 - davetapley
这是我对该拉取请求的想法:https://github.com/collectiveidea/delayed_job/pull/285#issuecomment-9352052 - davetapley

4
那就是 max_run_time 的作用:当从作业被锁定的那一刻起,已经过去了 max_run_time 所规定的时间后,其他进程就可以获取该锁。详见 谷歌群组中的讨论

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

1
delayed_job 3.0.5现在支持在TERM信号上引发异常的选项:https://github.com/collectiveidea/delayed_job/commit/90579c3047099b6a58595d4025ab0f4b7f0aa67a - Ari

1
我使用状态机来跟踪作业的进度,并使过程幂等,以便我可以多次调用给定作业/对象上的执行操作,并确信它不会重新应用破坏性操作。然后更新rake任务/delayed_job以在TERM上释放日志。
当进程重新启动时,它将按预期继续进行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接