Skip to content

Un reinicio tras reproducir música agota TimeoutStopSec y dispara una alerta falsa #35

Description

@Isma-L154

Apareció al desplegar #34 el 2026-09-12. El despliegue funcionó, pero el
apagado del proceso anterior agotó TimeoutStopSec=15
, systemd lo mató con
SIGKILL, y eso disparó OnFailure= — una alerta de Discord causada por el
propio despliegue, no por un fallo.

Sep 12 04:08:41  Stopping loopify-bot.service...
                 (ningún "Shutting down (KeyboardInterrupt)" — nunca llegó al handler)
Sep 12 04:08:56  loopify-bot.service: Failed with result 'timeout'.
Sep 12 04:08:56  Main process exited, code=killed, status=9/KILL
Sep 12 04:08:56  Failed to kill control group ...: Invalid argument
Sep 12 04:08:56  Triggering OnFailure= dependencies.
Sep 12 04:08:56  Consumed 7min 24.747s CPU time, 555.3M memory peak

Lo que más dice es lo que no está: no hay línea Shutting down (KeyboardInterrupt). El SIGINT se entregó pero el proceso no salió del
asyncio.run() en 15 segundos.

No es un problema general del apagado

Los 8 apagados anteriores de los últimos 14 días fueron todos limpios, y tras el
despliegue un reinicio de control también lo fue, en el mismo segundo:

Sep 12 04:12:17  Stopping loopify-bot.service...
Sep 12 04:12:17  INFO loopify: Shutting down (KeyboardInterrupt).
Sep 12 04:12:17  loopify-bot.service: Deactivated successfully.

Lo que distingue al que falló: llevaba 20 h en marcha, 7 min de CPU y 555 MB
de pico de memoria
, o sea que había reproducido música de verdad. Todos los
apagados limpios fueron de procesos que estaban idle o llevaban poco tiempo. El
reinicio de control de las 04:12 también fue sobre un proceso idle, así que
no reproduce la condición.

Hipótesis a investigar

La sospecha es algo en la ruta de reproducción que no responde a la cancelación
dentro de la ventana de 15 s, o que la bloquea. Candidatos, por orden:

  1. MusicPlayer.destroy() durante el cierre. Hace
    run_in_executor(None, stream.close), y AudioStream.close() espera al hijo
    hasta _REAP_TIMEOUT = 5.0 s. Con un stream activo más un prefetch, son dos
    cierres de 5 s cada uno, y el executor por defecto se apaga al final de
    asyncio.run() — un run_in_executor lanzado durante el teardown puede
    quedarse sin nadie que lo espere, o retrasarlo.
  2. BufferedAudioSource.cleanup() drena una cola de hasta 250 frames y
    luego llama a cleanup() del FFmpegPCMAudio envuelto, que a su vez espera
    a FFmpeg.
  3. El hilo de read-ahead es daemon=True, así que no debería impedir la
    salida — hay que confirmarlo, no asumirlo.
  4. bot.close() con un VoiceClient conectado: la desconexión de voz hace
    ida y vuelta al gateway, y si el websocket ya no responde puede bloquearse
    hasta su propio timeout.

Por qué importa, aunque el bot se recupere

  • Alertas falsas. Cada despliegue o reinicio de un bot que haya estado
    sonando manda un aviso de fallo a Discord. Eso entrena a ignorar el canal de
    alertas, que es exactamente lo que hizo útil al drop-in.
  • SIGKILL se salta la limpieza. Es el camino que el yt-dlp stream subprocess: unread stderr can stall playback, and processes are not fully reaped #6 y el Fix yt-dlp stream stalls and unreaped processes #15 cerraron: un
    kill -9 no deja correr AudioStream.close(), así que los hijos de yt-dlp y
    FFmpeg pueden quedar sin cosechar. Tras este despliegue concreto no quedaron
    huérfanos (pgrep -af 'yt_dlp|ffmpeg' salió vacío), pero es por suerte del
    cgroup, no por diseño.
  • Failed to kill control group: Invalid argument sugiere que algo del
    cgroup estaba en un estado raro al matarlo; merece una mirada aparte.

Cómo reproducirlo

La condición es haber reproducido, no estar reproduciendo:

# En Discord: !play <algo>, dejarlo sonar un rato, luego !stop
sudo systemctl restart loopify-bot
journalctl -u loopify-bot -n 20 --no-pager
# ¿aparece "Shutting down (KeyboardInterrupt)" y "Deactivated successfully",
# o "result 'timeout'" y status=9/KILL?

Probar también con el bot reproduciendo activamente al reiniciar, y con la cola
llena para que haya un prefetch vivo además del stream actual.

Criterio de aceptación

  • Un reinicio tras haber reproducido apaga limpio, dentro de TimeoutStopSec, y
    registra Shutting down (KeyboardInterrupt).
  • Un test que cierre un MusicPlayer con un stream y un prefetch vivos y afirme
    que el teardown termina en un plazo acotado.
  • Ningún hijo de yt-dlp ni de FFmpeg sobrevive al apagado.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions