Les tâches différées sont parfois perdues entre le zset du planning Sidekiq et leur exécution (pas d'exécution, pas de nouvelle tentative, aucune trace)

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

Résumé

Depuis la mise à niveau d’un forum auto-hébergé de Discourse 2026.2.0-latest (Ruby 3.3.8) vers 2026.10.0-latest.1 (Ruby 4.0.9, Rails 8.1.4, Sidekiq 7.3.10) le 2026-10-26, les travaux mis en file d’attente via Jobs.enqueue_in sont perdus de manière intermittente et silencieuse : le travail disparaît du jeu trié schedule à son échéance, mais perform ne s’exécute jamais. Il n’y a aucune tentative de nouvelle exécution, aucun travail mort, aucune exception consignée dans les journaux (nous utilisons Logster::RedisStore, donc les exceptions avalées sont visibles — il n’y en a aucune), et stat:failed reste à 0. Taux de perte observé sur 9 occurrences contrôlées/réelles : 4 perdues / 5 exécutées (~40 %).

Environnement

  • Installation officielle via Docker (./launcher rebuild), site unique
  • Discourse 2026.10.0-latest.1, image 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 supervisé par Demon::Sidekiq, lancé par pitchfork)
  • L’état de Sidekiq se trouve dans une base de données Redis dédiée (Discourse.sidekiq_redis_config), séparée de la Redis de Discourse
  • Le processus Sidekiq est sain pendant toutes les observations : 0 redémarrages, [0 of 5 busy], files d’attente vides, retry = 0, dead = 0, stat:failed = 0
  • Aucune erreur OOM, aucun événement dmesg, aucun redémarrage de conteneur

Observations (2026-10-30, cas réel + expériences contrôlées)

Le travail affecté est Jobs::NotifyMailingListSubscribers, mis en file d’attente via Jobs.enqueue_in(SiteSetting.email_time_window_mins.minutes, ...) (600 s) → jeu trié schedule avec score = échéance.

Échéance (UTC) Travail Résultat
15:33:05 réel (post 282) perdu — a disparu de schedule, jamais exécuté
15:50:10 contrôle (post_id 999999 → retour silencieux précoce) exécuté (+1 stat:processed ~8 s après l’échéance)
16:04:18 réel (post 283) exécuté (erreurs SMTP écrites, voir ci-dessous)
16:21:44 réel ×2 + contrôle tous exécutés
17:34:39 réel ×2 perdus ×2
17:42:39 contrôle exécuté (+1, 11 s après l’échéance)
17:55:59 réel perdu

Preuves recueillies pour chaque travail « perdu » :

  • zrange schedule ne contient plus la charge utile (le zrem a eu lieu), et elle n’est présente ni dans retry, ni dans dead, ni dans aucune file d’attente (queue:low, etc. sont toutes à 0), ni dans l’attribution de stat:processed.
  • Un outil de surveillance pg_stat_activity échantillonnant toutes les 300 ms pendant la fenêtre d’échéance n’a enregistré aucune requête SQL provenant de la connexion Sidekiq — pas même le SELECT ... FROM posts initial que execute effectue en premier.
  • Logster::RedisStore (__LOGSTER__MAP) ne contient aucune nouvelle entrée pour ces travaux, tandis que des entrées sont écrites pour les exécutions réussies (échecs SMTP liés à un problème différent et sans rapport, corrigé en parallèle).
  • Le titre du processus Sidekiq reste [0 of 5 busy] ; aucun redémarrage de Sidekiq (même PID depuis le démarrage), aucune erreur OOM.
  • Le nombre d’entrées SkippedEmailLog est inchangé pour les travaux perdus (les exécutions échouant y écrivent bien des lignes).

Les exécutions « réussies » sont confirmées : elles ont généré des entrées d’erreur SMTP par utilisateur (capturées dans SkippedEmailLog.custom_reason), donc la classe de travail et la structure de la charge utile sont identiques entre les occurrences perdues et exécutées (même classe, même file d’attente low, seul le post_id diffère).

L’installation précédente (Discourse 2026.2.0-latest, Ruby 3.3.8) ne présentait pas ce comportement : les mêmes travaux retardés de liste de diffusion ont été livrés de manière fiable le 2026-10-01 et le 2026-10-21 (vérifié via EmailLog + files d’attente MTA en aval).

Mesure d’atténuation appliquée

Nous avons défini email_time_window_mins = 0 pour ce site afin qu’enqueue_in(0) ignore la clé at et pousse le travail directement dans sa file d’attente (client_push sans at), contournant entièrement le poller planifié. Après ce changement, les 6 lignes EmailLog attendues ont été créées immédiatement et les courriels ont été acceptés par le MTA en aval.

Arbitrage accepté : les notifications de liste de diffusion sont désormais envoyées immédiatement au lieu d’être regroupées avec un décalage de +10 min.

Questions

  1. S’agit-il d’une régression connue dans la combinaison 2026.10 / Sidekiq 7.3.10 (ou dans Demon::Sidekiq / le poller planifié sous Ruby 4.0) ?
  2. Quels diagnostics supplémentaires souhaitez-vous que nous recueillions lors de la prochaine occurrence (par exemple, l’activation de l’instrumentation de travail DISCOURSE_LOG_SIDEKIQ=1, les backtraces d’exceptions Sidekiq, les dumps des threads du poller) ?
2 « J'aime »