CreaRack-SL

CCIB 07-09-2026 — un deep discovery contra un equipo mudo bloqueó el websocket y tiró al Agente 3 minutos

Cuándo

07-09-2026, ~11:09 hora local. Primer PR de la sesión 313, con Edu in situ en el CCIB. El Agente local del portátil YogaEdu se desconecta del SaaS (close_code=1006) y no vuelve a estar operativo hasta 3 minutos después (11:12:25).

Síntomas visibles

  • El Agente YogaEdu cae del websocket sin motivo aparente: los POST de métricas del mismo minuto respondían HTTP 200 (la red iba bien) y el otro Agente conectado al mismo servidor no se cayó (el servidor iba bien).
  • El log del Agente muestra, cada 30 segundos entre las 11:09:10 y las 11:12:25, Send error: Cannot write to closing transport — seguía “trabajando” sobre una conexión que el servidor ya daba por muerta.
  • La pestaña de detalle de un equipo Wireless (SpinetiX del CCIB) relanzaba un deep discovery cada 5 minutos contra 172.25.10.201, un equipo aislado por una ACL del switch — sin éxito nunca, sin dejar de insistir.

Causa raíz

Tres defectos encadenados:

  1. Los comandos del SaaS se ejecutaban en línea dentro del bucle de lectura del websocket (terminal/agent/core/connector.py::_receive_loop). Un deep discovery contra un equipo mudo tardaba ~4 minutos en agotar los timeouts SNMP; mientras tanto, el bucle que debía seguir leyendo el socket (y respondiendo al latido) estaba bloqueado ejecutando ese comando.
  2. El deep discovery no tenía sondeo previo de alcance (terminal/agent/network/deep_discovery.py): recorría 8 categorías × ~20 OIDs con 5-11s de timeout cada uno aunque el equipo no respondiera a nada — de ahí los ~4 minutos por intento contra 172.25.10.201.
  3. La pestaña de detalle relanzaba el deep discovery cada 5 minutos sin importar si el intento anterior había fallado (static/js/pages/monitoring/MonitoringDetailPlumbing.js), así que el ciclo de 4 minutos de bloqueo se repetía indefinidamente mientras la pestaña estuviera abierta.

Con los tres defectos juntos: aiohttp llenaba su buffer de entrada (128 KB) con los comandos que se iban acumulando mientras el bucle de lectura estaba ocupado, pausaba la lectura TCP y con ella el PONG del latido (heartbeat=30, pong esperado en 15s) → el servidor declaraba la conexión muerta a las 11:09:10, mientras el Agente seguía procesando a ciegas lo que ya tenía en buffer hasta las 11:12:25.

Fix aplicado (commit 22604b895aa1e8deebfe3886a69f48d71b7990db, Agente 2.27.1, PR #522)

  • terminal/agent/core/command_queue.py (nuevo): clase CommandQueue, un único trabajador que ejecuta los comandos de uno en uno en una tarea propia — el bucle de lectura del websocket ya nunca se bloquea ejecutando un comando. Detalle completo del mecanismo en [[entity—terminal—service—connector-ws-diagnostics]].
  • terminal/agent/network/deep_discovery.py::_probe_alive: sondeo de dos GET del grupo system (sysUpTime, sysDescr) antes de recorrer las categorías; si ninguno responde, falla en ~10s en vez de ~4 min. Un equipo vivo paga solo un GET extra.
  • static/js/pages/monitoring/MonitoringDetailPlumbing.js: el timer de 5 minutos que relanza el deep discovery aplica backoff (5·2^n minutos, tope 30) cuando el intento anterior falló; un éxito o el botón de refresco manual lo reinician. Comparten el mixin las pestañas de detalle de UPS, Wireless y Cartelería.

Lecciones

  • El mismo portátil (YogaEdu) ya había protagonizado un flapping distinto el 21-07-2026 (saturación de uplink, resuelto con el throttle del sync v2.16.0 — ver [[entity—terminal—service—connector-ws-diagnostics]]). Dos incidentes con el mismo síntoma (close_code=1006) y causas raíz completamente distintas: el close_code por sí solo no basta para diagnosticar, hace falta mirar qué estaba haciendo el Agente en ese instante.
  • Un bucle de lectura de red nunca debe compartir tarea con el trabajo que dispara: cualquier comando de duración no acotada (aquí, un timeout SNMP contra un equipo apagado) puede bloquearlo indefinidamente si comparten la misma corrutina.
  • Un timer que reintenta a ciegas sin mirar si el intento anterior tuvo éxito convierte un fallo puntual en una insistencia perpetua — el backoff no es solo cortesía con el servidor, es lo que evita que un único equipo mudo mantenga ocupado al Agente entero mientras la pestaña esté abierta.

Preventivos futuros

  • Verificación en pantalla pendiente (declarada así en el propio CHANGELOG del fix): tras publicar y que la flota se actualice sola a 2.27.1, confirmar que YogaEdu aguanta conectado con la pestaña del SpinetiX abierta y el equipo aún aislado.
  • Hallazgo colateral sin arreglar: en el log de PROD aparece metrics [vm/all] target=1199 ping EXCEPTION: You cannot call this from an async context (09:11 y 09:12 UTC) — una llamada ORM síncrona dentro de código async en el lector de métricas. No intervino en esta caída; queda anotado para el siguiente ciclo.

Véase también

  • [[entity—terminal—service—connector-ws-diagnostics]]
  • [[feature—agent—resync-throttle-v2160]]
  • [[entity—terminal—service—sync-throttle]]
  • [[crearack—monitoring—deep-discovery]]
  • [[crearack—wireless—deep-discovery-troubleshooting]]
  • [[feature—monitoring—deuda-274-tanda-b]]