CreaRack-SL

Auditoría Suprema 2 · Cola monitoring D: rendimiento de deep-discovery y de Wireless/UPS

Cuándo

04-09-2026 · commit 7b1025166 (PR #499, v1.107.0). Task #286 — cola MEDIA de la Auditoría Suprema 2 en monitoring, bloque D: cambia de foco de sondas/validación (ver [[incident—20260904—auditoria-suprema-2-cola-monitoring-a-sondas-y-targets]]) a rendimiento del deep-discovery y de las páginas Wireless/UPS. Cuatro hallazgos MEDIA, 15 tests nuevos.

Síntomas visibles

Cuatro hallazgos MEDIA en el mismo commit:

  1. D-#10 — El progreso del job de deep-discovery (done/failed/status en Valkey) se actualizaba con un cache.get + cache.set sin bloqueo. El Agente Local manda hasta 5 resultados en paralelo (max_concurrent); dos escrituras concurrentes se pisaban y el job se quedaba clavado sin llegar nunca a completed.
  2. D-#11 — El enlace automático de un perfil descubierto a su Device (Rack Editor) recorría TODOS los devices de la organización, deserializando management_config uno a uno en Python — sin límite y sin excluir los racks en papelera.
  3. D-#40 — Un SAI con el ping filtrado, o con ping_enabled=False y solo SNMP, salía unknown/offline y sin métricas de batería/carga aunque estuviera publicando datos SNMP frescos ahora mismo: UPS solo miraba mt.last_status, sin el mismo salvavidas de “evidencia viva” que Wireless ya tenía desde la auditoría #261.
  4. D-#41 — get_ups_summary/get_ups_status_list y sus gemelos de Wireless cargaban deep_snmp_data (un blob JSON de 20-60 KB por equipo real) de TODA la flota en cada refresco, aunque el dato vivo de VictoriaMetrics ya bastara para responder casi siempre — refrescos cada 2 minutos por pestaña, la mayoría del blob descartado sin usar.

Causa raíz

  • Contador sin exclusión mutua: un read-modify-write sobre un valor compartido en Valkey sin SETNX/lock, bajo escritura concurrente real (el Agente en paralelo).
  • Enlace por recorrido completo en vez de prefiltro: sin filtro SQL sobre el campo que de verdad se compara, cualquier alta de dispositivos escala linealmente con el inventario de la organización.
  • Una sola fuente de “está vivo” (ping) para dos protocolos con comportamiento distinto: Wireless ya había resuelto esto para APs (auditoría #261); UPS heredó el mismo hueco por no compartir el motor.
  • Carga siempre-completa donde el caso común es parcial: pedir el blob entero cuando la mayoría de peticiones solo necesita el resumen agregado.

Fix aplicado

Commit 7b1025166 (PR #499):

  • D-#10 — monitoring/services/deep_discover_service.py: nueva advance_deep_discover_job(job_key, success) toma un lock corto vía cache.add (SET NX atómico, mismo patrón que core/ratelimit.py) antes de leer/incrementar/escribir el job; sin lock en ~1s actualiza igual, degradado, para no perder el resultado del Agente. terminal/api/sentinel_ingest.py::receive_deep_discovery_result pasa a llamarla en vez de tocar cache directamente.
  • D-#11 — terminal/api/sentinel_ingest.py: nueva _reverse_link_device(profile, tenant_id) prefiltra con Device.objects.filter(rack__organization_id=..., rack__deleted_at__isnull=True, management_config__contains=ip) (LIKE, por eso la comparación exacta contra Device.management_ip sigue en Python) antes de iterar.
  • D-#40 — nuevo módulo monitoring/services/live_evidence.py: LiveSnmpEvidence extrae el motor genérico que antes vivía solo dentro de WirelessLive (una consulta a VictoriaMetrics, N métricas, por target); WirelessLive pasa a heredar de él y UpsLive (nuevo, en monitoring/services/ups_metrics.py) lo reutiliza fijando METRICS = "snmp_extras_battery_charge|snmp_extras_output_load".
  • D-#41 — _get_ap_profiles y su equivalente en UPS aceptan with_deep=False (.defer("deep_snmp_data")); hydrate_deep_snmp_data(profiles, predicate) hidrata en una sola consulta id IN (...) solo los perfiles que de verdad lo necesitan (target sin estado o sin dato vivo), evitando el lazy-load fila a fila de Django.
  • Refactor de acompañamiento: monitoring/api/ups.py baja de 621 a 401 LOC extrayendo monitoring/services/ups_metrics.py (lógica de UpsLive/métricas) y monitoring/api/ups_reports.py (reportes), manteniendo la Regla 5 (máx. 500 LOC/módulo).
  • 15 tests nuevos en tests/monitoring/test_monitoring_bloque_d.py.

Lecciones

  • Un contador compartido bajo escritura concurrente necesita exclusión mutua explícita desde el primer día del endpoint que lo escribe más de una vez por request — el patrón de lock corto de core/ratelimit.py es reutilizable, no hace falta reinventarlo cada vez.
  • Cuando dos features resuelven el mismo problema (“¿este equipo está realmente vivo?”) por caminos separados, la segunda implementación hereda el hueco que la primera ya cerró — merece la pena extraer el motor compartido en cuanto aparece el segundo caso, no esperar al tercero.
  • Diferir un campo pesado (.defer(...)) sin un hidratador explícito no ahorra nada: en cuanto algo lee el atributo, Django dispara un lazy-load por fila — el ahorro real exige decidir explícitamente quién lo necesita y traerlo en una sola consulta.

Preventivos futuros

  • Sigue abierta la cola de bloques anunciada en la cola A (CNS e insights; ITSM y notificaciones) — este bloque D nace de una re-priorización hacia rendimiento antes de cerrarlos.
  • Ayuda de usuario sin cambios: el comportamiento visible (SAI vivo por SNMP, refrescos de summary) coincide con lo que la documentación de usuario ya describía.

Véase también

  • [[incident—20260904—auditoria-suprema-2-cola-monitoring-a-sondas-y-targets]]
  • [[entity—monitoring—service—target-lifecycle]]
  • [[entity—monitoring—service—group-service]]
  • [[entity—monitoring—service—snmp-service]]
  • [[feature—wireless—deep-discovery-timestamp]]
  • [[entity—monitoring—model—monitoringtarget]]
  • [[entity—monitoring—service—metric-names]]
  • [[concept—saas—multi-tenancy]]