如何避免Sidekiq的MailingWorker因PDF生成失败陷入无限循环?
嘿,这个问题我之前在项目里也碰到过,给你几个实用的解决方案,既能搞定无限循环的坑,又能兼顾代码的整洁性:
方案一:给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

