You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免Sidekiq的MailingWorker因PDF生成失败陷入无限循环?

解决Sidekiq Worker依赖任务的无限循环问题

嘿,这个问题我之前在项目里也碰到过,给你几个实用的解决方案,既能搞定无限循环的坑,又能兼顾代码的整洁性:

方案一:给MailingWorker加重试上限+超时判断(最小改动现有流程)

如果不想大改现有控制器的调用逻辑,那可以直接优化MailingWorker的重试规则,给它设置最大重试次数,同时在任务里加入超时判断,避免无限等待PDF生成。

首先,Sidekiq的retry参数可以设置最大重试次数,默认的重试间隔是指数退避(第一次10秒,第二次20秒,第三次40秒,以此类推),比你之前固定1分钟的重试更合理,能减少无效请求。

具体代码示例:

class MailingWorker
  include Sidekiq::Worker
  # 设置最多重试5次,同时关闭死信队列(如果需要保留失败任务可开启)
  sidekiq_options retry: 5, dead: false

  def perform(d_id, d_class)
    d = d_class.constantize.find(d_id)
    pdf_path = d.pdf_file_path # 假设你的模型有获取PDF路径的方法

    # 先检查PDF是否存在
    if File.exist?(pdf_path)
      # 执行发送两封邮件的逻辑
      FirstRecipientMailer.with(document: d).pdf_notification.deliver_later
      SecondRecipientMailer.with(document: d).pdf_notification.deliver_later
    else
      # 检查当前重试次数是否已达上限
      if current_retry_count >= 5
        # 记录日志或者触发告警,方便排查问题
        Rails.logger.error "⚠️ PDF for #{d_class} #{d_id} failed to generate after 5 retries. Aborting mailing."
        return
      end
      # 抛出异常让Sidekiq自动重试
      raise "PDF file not ready yet for #{d_class} #{d_id}"
    end
  end

  private

  # 获取当前任务的重试次数
  def current_retry_count
    Sidekiq::Worker::RetryCount.new(@job).count
  end
end

这样调整后,MailingWorker最多重试5次,总等待时间大概是10+20+40+80+160=310秒(约5分钟),如果PDF还是没生成,就会停止重试,不会无限循环。

方案二:让PdfWorker成功后触发MailingWorker(更可靠的依赖流程)

如果想彻底避免MailingWorker空等的情况,最好的方式是只有当PDF生成成功后,才调用MailingWorker,这样从根源上消除无效重试。这种方式不算过度嵌套,因为只是在PdfWorker的成功分支里触发下一个任务,逻辑非常清晰。

具体代码示例:

首先修改PdfWorker,给它设置自己的重试次数(比如3次),生成成功后再调用MailingWorker:

class PdfWorker
  include Sidekiq::Worker
  # 设置PDF生成任务最多重试3次
  sidekiq_options retry: 3

  def perform(d_id)
    d = D.find(d_id)
    # 执行Wicked PDF生成逻辑,失败会自动抛出异常
    pdf_content = WickedPdf.new.pdf_from_string(
      ApplicationController.render(template: "documents/pdf", locals: { document: d })
    )
    # 保存PDF到指定路径
    File.write(d.pdf_file_path, pdf_content)

    # 只有生成成功才触发邮件任务
    MailingWorker.perform_async(d.id, d.class.name)
  rescue StandardError => e
    Rails.logger.error "❌ Failed to generate PDF for document #{d_id}: #{e.message}"
    # 抛出异常让Sidekiq自动重试PdfWorker
    raise
  end
end

然后控制器里只需要调用PdfWorker即可,不用再延迟调用MailingWorker:

# 控制器代码
def some_action
  @d = D.find(params[:id])
  PdfWorker.perform_async(@d.id)
  redirect_to @d, notice: "PDF生成和邮件发送任务已启动"
end

这种方案的好处是:

  • 流程依赖关系明确,只有PDF成功生成才会发邮件
  • 避免了MailingWorker的无效重试,资源利用率更高
  • 每个Worker的职责更单一,符合单一职责原则

方案对比和推荐

如果你不想大改现有代码,方案一可以快速解决问题;但从长期维护和可靠性来看,方案二更值得推荐,因为它从根源上避免了无效任务的产生,代码逻辑也更清晰。

另外,如果你担心两个Worker的耦合问题,可以考虑用Sidekiq Pro的Batch功能(批量任务管理),或者在模型里加异步回调,但对于简单的依赖任务,直接在PdfWorker里触发MailingWorker已经足够简洁了。

内容的提问来源于stack exchange,提问作者Maxence

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:01:52