Sidekiq 调度 zset 与执行之间间歇性丢失延迟任务(无执行、无重试、无记录)

Discourse 2026.10.0-latest.1 / Sidekiq 7.3.10 / Ruby 4.0.9

概述

自2026年10月26日将自建论坛从 Discourse 2026.2.0-latest(Ruby 3.3.8)升级至 2026.10.0-latest.1(Ruby 4.0.9、Rails 8.1.4、Sidekiq 7.3.10)以来,通过 Jobs.enqueue_in 入队的任务偶尔会静默丢失:任务在到期时从 schedule zset 中消失,但 perform 从未执行。没有重试,没有死信任务,没有记录的异常(我们使用 Logster::RedisStore,因此被吞没的异常是可见的——但确实没有),并且 stat:failed 保持为 0。在9次受控/真实案例中,观察到的丢失率为:4次丢失 / 5次执行(~40%)。

环境

  • 官方 docker 安装(./launcher rebuild),单站点
  • Discourse 2026.10.0-latest.1,镜像 discourse/base:2.0.20261026-0138
  • Ruby 4.0.9(2026-10-23)、Rails 8.1.4、Sidekiq 7.3.10
  • UNICORN_SIDEKIQS=1(sidekiq 由 Demon::Sidekiq 监督,由 pitchfork fork)
  • Sidekiq 状态保存在专用的 Redis 数据库中(Discourse.sidekiq_redis_config),与 Discourse redis 分离
  • 在所有观察期间 Sidekiq 进程健康:0 次重启,[0 of 5 busy],队列空,retry = 0,dead = 0,stat:failed = 0
  • 无 OOM,无 dmesg 事件,无容器重启

观察(2026年10月30日,真实案例 + 受控实验)

受影响的任务是 Jobs::NotifyMailingListSubscribers,通过 Jobs.enqueue_in(SiteSetting.email_time_window_mins.minutes, ...) 入队(600秒)→ schedule zset,score = 到期时间。

到期时间 (UTC) 任务 结果
15:33:05 真实 (帖子 282) 丢失 — 从 schedule 消失,从未执行
15:50:10 控制 (post_id 999999 → 静默提前返回) 执行(在到期后 ~8 秒增加 1 个 stat:processed)
16:04:18 真实 (帖子 283) 执行(SMTP 错误已写入,见下文)
16:21:44 真实 ×2 + 控制 全部执行
17:34:39 真实 ×2 丢失 ×2
17:42:39 控制 执行(+1,在到期后 11 秒)
17:55:59 真实 丢失

对于每个“丢失”的任务收集的证据:

  • zrange schedule 不再包含该载荷(zrem 已发生),且它不在 retry、dead 或任何队列中(queue:low 等均为 0),也不在 stat:processed 归属中。
  • pg_stat_activity 监视器在到期窗口期间每 300 ms 采样一次,记录到 Sidekiq 连接的零条 SQL——甚至没有 execute 首先执行的初始 SELECT ... FROM posts。
  • Logster::RedisStore(__LOGSTER__MAP)中没有这些任务的新条目,但已执行的运行确实写入了条目(SMTP 失败来自我们并行修复的另一个无关问题)。
  • Sidekiq proctitle 保持 [0 of 5 busy];没有 sidekiq 重启(自启动以来 PID 相同),无 OOM。
  • 对于丢失的任务,SkippedEmailLog 计数未变(已执行且失败的运行确实会在其中写入行)。

“已执行”的运行得到正面确认:它们生成了每用户 SMTP 错误条目(捕获在 SkippedEmailLog.custom_reason 中),因此任务类和载荷形状在丢失和执行的实例之间是相同的(类相同,队列 low 相同,仅 post_id 不同)。

先前的安装(Discourse 2026.2.0-latest,Ruby 3.3.8)未表现出此行为:相同的延迟邮件列表任务在 2026年10月1日和 2026年10月21日可靠投递(通过 EmailLog + 下游 MTA 队列验证)。

已应用的缓解措施

我们将该站点的 email_time_window_mins = 0,以便 enqueue_in(0) 跳过 at 键并直接将任务推送到其队列(不带 at 的 client_push),从而完全绕过调度轮询器。在此更改后,所有 6 个预期的 EmailLog 行均立即创建,并且邮件被下游 MTA 接受。

接受的权衡:邮件列表通知现在立即发送,而不是批处理 +10 分钟。

问题

  1. 这是 2026.10 / Sidekiq 7.3.10 组合(或 Ruby 4.0 下的 Demon::Sidekiq / 调度轮询器)中已知的回归吗?
  2. 您希望我们在下一次发生时捕获哪些额外的诊断信息(例如启用 DISCOURSE_LOG_SIDEKIQ=1 任务插桩、Sidekiq 异常回溯、轮询器线程转储)?
2 个赞