Los trabajos diferidos se pierden intermitentemente entre el zset de programación de Sidekiq y su ejecución (sin ejecución, sin reintentos, sin rastreo)

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

Resumen

Desde que actualizamos un foro autoalojado de Discourse 2026.2.0-latest (Ruby 3.3.8) a 2026.10.0-latest.1 (Ruby 4.0.9, Rails 8.1.4, Sidekiq 7.3.10) el 2026-10-26, los trabajos encolados con Jobs.enqueue_in se pierden de forma intermitente y silenciosa: el trabajo desaparece del zset schedule en su hora de vencimiento, pero perform nunca se ejecuta. No hay reintentos, no hay trabajos muertos y no hay excepciones registradas (usamos Logster::RedisStore, por lo que las excepciones capturadas serían visibles — no hay ninguna), y stat:failed permanece en 0. Tasa de pérdida observada en 9 ocurrencias controladas/reales: 4 perdidos / 5 ejecutados (~40 %).

Entorno

  • Instalación oficial con Docker (./launcher rebuild), sitio único
  • Discourse 2026.10.0-latest.1, imagen 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 supervisado por Demon::Sidekiq, bifurcado por pitchfork)
  • El estado de Sidekiq está en una base de datos Redis dedicada (Discourse.sidekiq_redis_config), separada del redis de Discourse
  • El proceso de Sidekiq está saludable durante todas las observaciones: 0 reinicios, [0 of 5 busy], colas vacías, retry = 0, dead = 0, stat:failed = 0
  • Sin OOM, sin eventos en dmesg, sin reinicios de contenedores

Observaciones (2026-10-30, caso real + experimentos controlados)

El trabajo afectado es Jobs::NotifyMailingListSubscribers, encolado con Jobs.enqueue_in(SiteSetting.email_time_window_mins.minutes, ...) (600 s) → zset schedule con score = hora de vencimiento.

Hora de vencimiento (UTC) Trabajo Resultado
15:33:05 real (post 282) perdido — desaparece de schedule, nunca ejecutado
15:50:10 control (post_id 999999 → return temprano silencioso) ejecutado (+1 stat:processed ~8 s después del vencimiento)
16:04:18 real (post 283) ejecutado (se escribieron errores SMTP, ver abajo)
16:21:44 real ×2 + control todos ejecutados
17:34:39 real ×2 perdidos ×2
17:42:39 control ejecutado (+1, 11 s después del vencimiento)
17:55:59 real perdido

Evidencia recopilada para cada trabajo “perdido”:

  • zrange schedule ya no contiene el payload (se ha producido el zrem), y no está presente ni en retry, ni en dead, ni en ninguna cola (queue:low, etc., todas en 0), ni en la atribución de stat:processed.
  • Un monitor pg_stat_activity que muestreaba cada 300 ms durante la ventana de vencimiento no registró ninguna consulta SQL desde la conexión de Sidekiq — ni siquiera el SELECT ... FROM posts inicial que execute realiza primero.
  • Logster::RedisStore (__LOGSTER__MAP) no contiene nuevas entradas para estos trabajos, mientras que las entradas SÍ se escriben para las ejecuciones ejecutadas (fallos SMTP de un problema diferente e no relacionado que arreglamos en paralelo).
  • El proctitle de Sidekiq permanece en [0 of 5 busy]; no hay reinicio de sidekiq (mismo PID desde el arranque), no hay OOM.
  • El recuento de SkippedEmailLog no cambia para los trabajos perdidos (las ejecuciones fallidas sí escriben filas allí).

Las ejecuciones “ejecutadas” están positivamente confirmadas: produjeron entradas de error SMTP por usuario (capturadas en SkippedEmailLog.custom_reason), por lo que la clase del trabajo y la forma del payload son idénticas entre las ocurrencias perdidas y las ejecutadas (misma clase, misma cola low, solo difiere el post_id).

La instalación anterior (Discourse 2026.2.0-latest, Ruby 3.3.8) no mostraba este comportamiento: los mismos trabajos diferidos de lista de correo se entregaron de forma fiable el 2026-10-01 y el 2026-10-21 (verificado mediante EmailLog + colas del MTA descendente).

Mitigación aplicada

Establecimos email_time_window_mins = 0 para este sitio, de modo que enqueue_in(0) omite la clave at y envía el trabajo directamente a su cola (client_push sin at), evitando por completo el sondeo programado. Tras este cambio, las 6 filas EmailLog esperadas se crearon inmediatamente y los correos fueron aceptados por el MTA descendente.

Compensación aceptada: las notificaciones de lista de correo ahora se envían inmediatamente en lugar de agruparse +10 min.

Preguntas

  1. ¿Es esta una regresión conocida en la combinación 2026.10 / Sidekiq 7.3.10 (o en Demon::Sidekiq / el sondeo programado bajo Ruby 4.0)?
  2. ¿Qué diagnósticos adicionales quisieran que capturemos en la próxima ocurrencia (por ejemplo, habilitar la instrumentación de trabajos DISCOURSE_LOG_SIDEKIQ=1, backtraces de excepciones de Sidekiq, volcados de hebras del sondeo)?
2 Me gusta