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

基于Rails+Active Job+Sidekiq+Clockwork+Heroku的任务重复执行排查

定时任务重复执行的可能原因及排查方案

1. 任务幂等性缺失引发竞态重复处理

你的SendInvoiceOverdueRemindersJob逻辑是先查询overdue_reminder_sent: false的发票,再逐个标记为已处理并保存。这种模式存在竞态条件:如果同一任务被多次入队并同时执行,多个进程会同时查询到未标记的发票,进而重复发送邮件。

解决/排查方法:

  • 给查询添加行级锁,确保同一时间只有一个进程能处理某张发票:
    overdue_invoices = Invoice.overdue.where(overdue_reminder_sent: false).lock(true)
    
  • 改用批量原子更新替代逐个保存,避免中间状态的竞态:
    # 先筛选符合条件的发票ID
    eligible_invoice_ids = Invoice.overdue.where(overdue_reminder_sent: false).select { |inv| (Date.today - inv.due_date.to_date).to_i > DAYS_OVERDUE_TRIGGER }.pluck(:id)
    # 批量标记为已处理
    Invoice.where(id: eligible_invoice_ids).update_all(overdue_reminder_sent: true)
    # 再基于已标记的ID处理邮件发送
    

2. Clockwork进程重启触发额外执行

Heroku dyno会定期自动重启(每日至少一次),如果Clockwork进程重启时,当天的定时任务时间已过,部分版本的Clockwork会立即触发一次任务,导致当天执行两次。另外需确认是否真的只有一个Clockwork进程在运行(虽heroku ps显示1个,但可能存在进程异常重启后残留的调度)。

解决/排查方法:

  • 查看Heroku日志中SendInvoiceOverdueRemindersJob.perform_later的触发记录,确认是否有多次调度日志,判断是Clockwork重复入队还是Sidekiq重复执行。
  • 在Clockwork中添加当日任务执行的幂等校验,比如用Redis记录执行状态:
    every(1.day, "SendInvoiceOverdueRemindersJob", at: '08:00') do
      key = "send_invoice_overdue_reminders_#{Date.today}"
      unless Redis.current.get(key)
        SendInvoiceOverdueRemindersJob.perform_later
        Redis.current.setex(key, 86400, "done")
      end
    end
    

3. Sidekiq重试机制导致邮件任务重复发送

从提供的Action Mailer异常日志来看,邮件发送任务曾抛出异常,而Sidekiq默认会自动重试失败任务。如果主任务没有正确标记发票的overdue_reminder_sent状态,重试的邮件任务会再次发送提醒。

解决/排查方法:

  • 查看完整异常日志,定位邮件发送失败的具体原因并修复,避免触发重试。
  • 给邮件任务添加幂等校验,比如发送前再次检查发票的overdue_reminder_sent状态,或使用Sidekiq的唯一任务特性避免重复执行。

4. 旧Heroku Scheduler任务残留

虽已移除Heroku Scheduler插件,但可能之前配置的定时任务未完全删除,导致旧任务仍在触发,与Clockwork的任务重复执行。

解决/排查方法:

  • 登录Heroku Dashboard,进入应用的"Scheduler"页面,确认所有旧任务已删除。
  • 查看Heroku日志中任务触发的来源,区分是来自clockwork进程还是旧Scheduler任务。

5. 时区配置不一致

Clockwork设置了America/Chicago时区,但Heroku系统默认使用UTC时区,需确认Clockwork的时区配置是否生效,是否因时区计算错误导致任务在一天内被多次触发。

解决/排查方法:

  • 在Clockwork中添加日志,输出任务触发的时间及时区,确认是否符合预期:
    every(1.day, "SendInvoiceOverdueRemindersJob", at: '08:00') do
      Rails.logger.info "Triggering SendInvoiceOverdueRemindersJob at #{Time.now} (timezone: #{Time.now.zone})"
      SendInvoiceOverdueRemindersJob.perform_later
    end
    
  • 检查Heroku应用的时区环境变量,确保未覆盖Clockwork的配置。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 22:27:05