Verspätete Jobs gehen intermittierend zwischen dem Sidekiq-Schedule-ZSet und der Ausführung verloren (keine Ausführung, kein Retry, kein Trace)

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

Zusammenfassung

Seit dem Upgrade eines selbst gehosteten Forums von Discourse 2026.2.0-latest (Ruby 3.3.8) auf 2026.10.0-latest.1 (Ruby 4.0.9, Rails 8.1.4, Sidekiq 7.3.10) am 26.10.2026 werden Jobs, die mit Jobs.enqueue_in in die Warteschlange gestellt wurden, intermittierend und stillschweigend verworfen: Der Job verschwindet zur Fälligkeit aus dem schedule-zset, aber perform wird nie ausgeführt. Es gibt keinen Retry, keinen Dead Job und keine protokollierte Ausnahme (wir verwenden Logster::RedisStore, sodass verschluckte Ausnahmen sichtbar wären – es gibt jedoch keine), und stat:failed bleibt bei 0. Die beobachtete Verlustrate über 9 kontrollierte/reale Vorfälle: 4 verloren / 5 ausgeführt (~40 %).

Umgebung

  • Offizielle Docker-Installation (./launcher rebuild), einzelne Site
  • Discourse 2026.10.0-latest.1, Image discourse/base:2.0.20261026-0138
  • Ruby 4.0.9 (23.10.2026), Rails 8.1.4, Sidekiq 7.3.10
  • UNICORN_SIDEKIQS=1 (Sidekiq wird von Demon::Sidekiq überwacht, forked durch pitchfork)
  • Sidekiq-Zustand in einer dedizierten Redis-Datenbank (Discourse.sidekiq_redis_config), getrennt vom Discourse-Redis
  • Sidekiq-Prozess während aller Beobachtungen gesund: 0 Neustarts, [0 of 5 busy], Warteschlangen leer, retry = 0, dead = 0, stat:failed = 0
  • Kein OOM, keine dmesg-Ereignisse, keine Container-Neustarts

Beobachtungen (30.10.2026, realer Fall + kontrollierte Experimente)

Der betroffene Job ist Jobs::NotifyMailingListSubscribers, eingeplant mit Jobs.enqueue_in(SiteSetting.email_time_window_mins.minutes, ...) (600 s) → schedule-zset mit Score = Fälligkeitszeit.

Fälligkeitszeit (UTC) Job Ergebnis
15:33:05 real (Post 282) verloren – aus schedule verschwunden, nie ausgeführt
15:50:10 Kontrolle (post_id 999999 → stiller Early-Return) ausgeführt (+1 stat:processed ~8 s nach Fälligkeit)
16:04:18 real (Post 283) ausgeführt (SMTP-Fehler geschrieben, siehe unten)
16:21:44 real ×2 + Kontrolle alle ausgeführt
17:34:39 real ×2 verloren ×2
17:42:39 Kontrolle ausgeführt (+1, 11 s nach Fälligkeit)
17:55:59 real verloren

Zusammengetragene Hinweise für jeden “verlorenen” Job:

  • zrange schedule enthält die Payload nicht mehr (der zrem hat stattgefunden), und sie ist weder in retry, noch in dead, noch in einer Warteschlange (queue:low usw. alle 0), noch in der stat:processed-Attribution vorhanden.
  • Ein pg_stat_activity-Watcher, der alle 300 ms im Fälligkeitsfenster abfragte, zeichnete null SQL von der Sidekiq-Verbindung auf – nicht einmal die anfängliche SELECT ... FROM posts-Abfrage, die execute zuerst durchführt.
  • Logster::RedisStore (__LOGSTER__MAP) enthält keinen neuen Eintrag für diese Jobs, während Einträge für die ausgeführten Läufe geschrieben werden (SMTP-Fehler eines anderen, unrelateden Problems, das wir parallel behoben haben).
  • Sidekiq-Proctitle bleibt bei [0 of 5 busy]; kein Sidekiq-Neustart (gleiche PID seit Boot), kein OOM.
  • Die Anzahl der SkippedEmailLog-Einträge hat sich für verlorene Jobs nicht geändert (die ausgeführten und fehlschlagenden Läufe schreiben dort Zeilen).

Die “ausgeführten” Läufe wurden positiv bestätigt: Sie erzeugten pro Nutzer SMTP-Fehlereinträge (erfasst in SkippedEmailLog.custom_reason), sodass die Job-Klasse und die Payload-Struktur zwischen verlorenen und ausgeführten Vorkommen identisch sind (gleiche Klasse, gleiche Warteschlange low, nur post_id unterscheidet sich).

Die vorherige Installation (Discourse 2026.2.0-latest, Ruby 3.3.8) wies dieses Verhalten nicht auf: dieselben verzögerten Mailing-List-Jobs wurden am 01.10.2026 und 21.10.2026 zuverlässig zugestellt (verifiziert über EmailLog + Downstream-MTA-Warteschlangen).

Angewandte Abschwächung

Wir haben email_time_window_mins = 0 für diese Site gesetzt, sodass enqueue_in(0) den at-Key überspringt und den Job direkt in die Warteschlange pusht (client_push ohne at), wodurch der geplante Poller komplett umgangen wird. Nach dieser Änderung wurden alle 6 erwarteten EmailLog-Zeilen sofort erstellt und die E-Mails vom Downstream-MTA akzeptiert.

Akzeptierter Trade-off: Mailing-List-Benachrichtigungen werden nun sofort gesendet, anstatt gebündelt mit +10 Minuten Verzögerung.

Fragen

  1. Ist dies eine bekannte Regression in der Kombination 2026.10 / Sidekiq 7.3.10 (oder in Demon::Sidekiq / dem geplanten Poller unter Ruby 4.0)?
  2. Welche zusätzlichen Diagnosen sollten wir beim nächsten Auftreten erfassen (z. B. Aktivierung der DISCOURSE_LOG_SIDEKIQ=1 Job-Instrumentierung, Sidekiq-Ausnahme-Backtraces, Poller-Thread-Dumps)?
2 „Gefällt mir“