关于用ActiveJob retry_on重试所有工作的说明

300 阅读8分钟

我想把我所有的ActiveJob配置成在失败时重试,我想通过ActiveJob retry_on方法 来实现

所以我打算在我的ApplicationJob类中配置它,以便在任何错误时重试,也许是这样的:

class ApplicationJob < ActiveJob::Base
  retry_on StandardError # other args to be discussed
end

为什么要用ActiveJob retry_on来实现?为什么是StandardError?

许多人在重试时使用后端特定的逻辑,特别是使用Sidekiq。这很好!

我喜欢使用ActiveJob功能的想法:

  • 我目前使用resque(稍后会有更多关于重试的挑战),但计划在中期的某个时候改用其他东西。也许是sideqkiq,但也许是delayed_jobgood_job。 (仅仅使用DB而不需要redis对我来说是有吸引力的,就像开源一样)。我喜欢这样的想法,即当我切换后端,或尝试不同的后端时,不必重做这个设置。
  • 总的来说,我喜欢ActiveJob作为可交换的商品化后端的承诺。
  • 我喜欢good_job的理念,如果一个功能可以在ActiveJob层面完成,为什么每个后端都要重新发明轮子?这可以帮助保持单个后端更小,维护成本更低。我认为good_job鼓励你使用ActiveJob retries。

注意,dhh从2018年开始就记录在案,他认为为所有StandardError设置retries是一个坏主意。但我真的不明白为什么!他说 "你应该知道为什么要重试,代码应该记录这些知识"--但是这么多ActiveJob后端都提供了 "重试所有作业 "的功能,这在我看来是一个既定的共同需求和最佳实践,为什么你不应该单独用ActiveJob来做呢?

dhh认为ActiveJob重试可能是针对特定的目标重试,而后端重试应该用于通用的普遍重试?老实说,我不认为自己会做很多有针对性的重试,让你所有的工作都是空闲的**(很重要**!ActiveJob的最佳实践!),让它们在任何错误时都重试,在我看来是一种更有效地利用开发者时间的方法,至少对一个相对简单的应用程序来说是足够的。

我有一种情况,重试是至关重要的,那就是当我有一个相当长的运行作业(比如它需要超过60秒的运行时间;我有一些不可避免的!),而运行作业的机器需要重新启动。这可能会打断工作。 如果它只是自动重试--放回队列中,由重启后的或其他作业工作者主机再次运行,那就很方便了 否则,它只是坐在那里失败,永远不会再运行,需要手动操作。自动重试将几乎在无形中处理它。

Resque和Resque Scheduler

Resque默认不支持未来计划的工作。你可以用Resque-scheduler插件添加它们。但是我有一个也许是不理性的愿望来避免这一点--Resque和它的生态系统在不同的时间有不同的维护/放弃,而且我(也许是不理性的)不愿意复杂化我的Resque栈。

而且我需要未来的重试调度吗?对于我最重要的用例,如果我只重试一次,立即重试,wait: 0 ,就完全没有问题。当然,这不会照顾到所有潜在的用例,但这是一个好的开始。

我想即使没有resque支持未来的计划,我也可以摆脱:

retry_on StandardError, wait: 0

唉,这实际上是行不通的,它最终还是会被转换为一个future-schedule的调用,除非你安装了resque-scheduler,否则会被Rails捆绑的resque_adapter拒绝。

当然,Resque可以在语义上处理wait:0,如果代码愿意通过排队一个普通的resque job....。 我不知道这是否是一个好主意但是,对Rails绑定的resque_adapter的这个简单补丁将使它愿意接受 "已安排 "的作业,当需要安排的时间实际上是 "现在 "时,只是正常地安排它们,同时仍然对未来安排的尝试提出。对我来说,它使retry_on.... wait: 0 ,只用普通的resque就可以了。

注意:retry_onattempts 计数包括第一次运行

因此,我想只重试一次,就尝试了这样的方法:

# Will never actually retry
retry_on StandardError, attempts: 1

我的工作实际上从来没有这样重试过!看起来,attempts 计数包括第一次非错误运行,工作将被运行的总次数,包括在任何 "重试 "之前的第一次!因此,尝试1意味着 "永不重试",而且什么也不做。哎呀。如果你真的想只重试 一次,在我的Rails 6.1应用程序中,这就是我所做的:

# will actually retry once
retry_on StandardError, attempts: 2

(我认为这意味着默认情况下,attempts: 5 ,实际上意味着你的工作总共可以运行5次--一次原始时间和4次重试。我想这就是原意吧?)

注意:job_id在重试过程中保持不变,万幸的是

顺便说一句,我检查了一下,至少在Rails 6.1中,ActiveJob#job_id在重试时保持不变。如果作业运行了一次,又重试了两次,那么每次都会有相同的job_id,你会在日志中看到三行Performing ,有相同的job_id。

吁!我想这是正确的做法,所以我们可以很容易地将这些作为日志中相同作业的重试关联起来。如果我们在某处保留job_id,以便回过头来检查它是否成功或失败或其他什么,那么它在重试时就能保持一致。

很高兴这就是ActiveJob正在做的事情。

记录不是很好,但可以自定义

Rails会自动记录重试的情况,看起来像这样的一行:

Retrying TestFailureJob in 0 seconds, due to a RuntimeError.
# logged at `info` level

最终,当它认为它的attempts 已经用尽时,它将会说一些类似的话:

Stopped retrying TestFailureJob due to a RuntimeError, which reoccurred on 2 attempts.
# logged at `error` level

但这并不包括工作编号,这使得它很难与关于这个工作的其他日志行相关联,也很难通过你的日志文件跟踪这个工作的整个过程。

这也与其他默认的ActiveJob日志行不一致,其中包括:

  • 文本中的作业ID
  • 标签(Rails标签日志系统)中的作业ID和字符串"[ActiveJob]" 。由于Rails代码只在执行/enqueue周围应用这些的方式,重试/丢弃相关的日志行显然最终没有包括在内。
  • 异常信息不只是有类时的类。

你可以在非常紧凑的ActiveJob::LogSubscriber类中看到所有内置的ActiveJob日志记录。而且你可以看到重试的日志行eg perform 有点不一致

也许这种不一致持续了这么久,部分原因是很少有人真正使用ActiveJob重试,他们都还在使用他们的后端特定功能? 我确实尝试了向Rails提交PR,以获得至少一致的格式(我的PR不做标记),不确定它是否会有进展,我认为盲目的PR到Rails通常不会有进展。

同时,在尝试了一堆不同的东西之后,我想我找到了合理的方法,即使用ActiveSupport::Notifications/LogSubscriber API来定制重试相关事件的日志,而对于其他的事件则不从Rails那里进行处理? 请看我的解决方案。

(感谢BigBinary博客在google上的显示,让我在弄清ActiveJob重试日志的工作方式上有了一个头绪。)

(注:还有这个github.com/armandmgt/l… 但我不确定它的工作/维护情况如何。它似乎只定制了Activejob的异常报告,而不是重试和其他事件。如果制作一个最新的activejob-lograge,适用于所有的ActiveJob日志,将每个事件表达为键/值,并使用lograge格式化设置来输出,这将是一个有趣的项目。我想我们已经看到了我们如何做到这一点,就像我们在上面做的那样,用一个自定义的日志订阅者来做!)

警告:ApplicationJob配置不会对电子邮件起作用

你可能会认为,既然我们在ApplicationJob 上配置了retry_on所有我们的bg作业现在已经被设置为重试了。

Oops!不是deliver_later emails。

Good_job README解释说,ActiveJob的邮件不从ApplicationMailer降生。(我很好奇这是否有什么好的理由,如果有的话似乎也不错!)

good_job的README提供了一种方法来配置内置的Rails mailer超类以进行重试。

你也可以尝试在该邮件超级类上设置delivery_job ,以使用一个自定义的投递任务(再次感谢BigBinary的指点)......也许是一个子类的默认类,以正常投递邮件,但让你设置一些自定义的选项,如retry_on? 不知道这是否在任何方面更可取。