Hub

2026-05-23

sábado · 23 de mayo de 2026

2026-05-23 — Sesión maratónica monitoreo

Sesión de ~9 horas (mediodía → noche). Diagnóstico, fix urgente, recovery y documentación de un incidente catastrófico de ingesta.

Resumen ejecutivo

Sergio reportó dos síntomas: 63 dispositivos sin datos del colector + portal con error 500. Las causas resultaron ser dos bugs separados con efectos amplificadores:

  1. Endpoint /metrics colgaba PHP-FPMDispositivo::with('ultima_lectura_historica.detalles') generaba subquery agregada sobre lecturas_historicas_detalles (49.6M filas). Cada scrape tardaba 30-60s y se apilaba.
  2. Bug de permisos del log volvió (el "fix" del 2026-05-15 no era suficiente) — laravel-2026-05-23.log nació electrosystems:electrosystems 664 porque un cron escribió primero a las 00:00:07; workers de Horizon (www-data) no podían escribir → cada job de ProcesarSyncCollector reventaba → 2,573 failed_jobs + ~5,800 archivos en failed_syncs/ acumulados en 14 horas.

Mi diagnóstico inicial fue ERRÓNEO. Sospeché de "red interna rota en sitios" basándome en los patrones de caídas simultáneas y UISP viendo equipos por cloud. Sergio refutó entrando a los collectors remotos y confirmando que sí veían los dispositivos por LAN. Eso me obligó a buscar la causa real en el server. Lección: cuando hay datos contraintuitivos, validar la hipótesis con el usuario antes de profundizar.

Acciones cerradas hoy

  • /metrics desactivado (commit 2efa022, deploy 98s). Endpoint público que disparaba la query letal cuando un scraper externo lo pegaba. Archivo MetricsController.php quedó intacto como referencia. Restauración futura: ver pendiente #190.
  • Permisos del log fix definitivo (sudo chmod g+s /var/www/es-monitoreo/storage/logs/ + chgrp www-data *.log + chmod 664 *.log). Verificado con touch: archivos nuevos heredan group www-data independiente del user que escriba primero. Memoria reference-laravel-daily-log-perms actualizada con nota para aplicar preventivamente al resto de proyectos Laravel del hub.
  • Recovery completo del backlog — ~5,800 archivos a través de 11 sitios. Estrategia evolucionó: primero script en bloques de 200 con polling (se atoraba porque el polling salía a "pending<10" antes de que el batch terminara realmente); cambié a "mover todo failed → syncs + 1 job por sitio" (el lock per-sitio del job serializa por sí solo, throttling natural del loop interno de 4 min con auto-redispatch). Tepehuanes (sitio 12) requirió ejecución INLINE con (new Job)->handle() porque el worker Horizon no tomaba el dispatched job. queue:flush cerró el ciclo (2,573 → 0).

Estado final

  • 0 archivos en failed_syncs/ de cualquier sitio.
  • 0 jobs nuevos failed desde el fix de permisos (13:44).
  • 11 dispositivos siguen marcados "Sin datos recientes" — pero son problemas REALES post-recovery:
    • Tepehuanes 6 (collector remoto del sitio caído desde antes de la sesión, min_sin_collector=487 cuando empezamos).
    • Torreón 3 + Villa Ahumada 2 (probablemente deadlocks MySQL ocasionales que siguen ocurriendo — 133 contados durante el día).

Pendientes abiertos

  • #193 — Revisión integral del pipeline de ingesta: Sergio pidió expresamente revisar TODO el código de jobs (PHP) + collector Go para identificar mejoras de eficiencia/robustez/throughput. Sin restricción de tecnología — abierto a Go/Rust microservicio dedicado, bulk inserts, particionado, Redis Streams, etc. Salida esperada: documento de hallazgos + propuestas con tradeoffs + recomendación priorizada.
  • #194 — Deadlocks MySQL en lecturas_historicas_detalles: 133 deadlocks contados hoy, cada uno = archivo perdido. Patrón: jobs paralelos chocan en INSERT IGNORE con batches grandes. Mitigaciones por explorar dentro de #193.
  • #195 — Worker Horizon zombie: dispatches sin procesar ni encolar. Backoff/lock/supervisor balance issue. Probablemente conectado a #193.
  • #196 — Tepehuanes collector remoto caído: operación de red en sitio físico, capturar en electrosystems-network-map cuando exista.
  • #190 — Retomar /metrics (cuando se necesite Prometheus) + arreglar la misma query monstruosa en el mailable de alertas (visible en error de log a las 13:08:39 con "Query execution was interrupted").
  • #191 — Bug NotificarLogs.php:686 (Attempt to read property "enlaces" on null): el cron de las 07:00 hoy se abortó por esto y las alertas por correo de las caídas no se enviaron.

Cierres del día

  • #192 — Fix de fondo permisos del log (setgid).

Sesión cerrada (monitoreo)

Sergio cerró pidiendo documentar #193 explícitamente. Sin más acción para esa sesión.


Sesiones tareas-hijo — aprendizaje Phoenix LiveView

Después del cierre de la sesión monitoreo, Sergio abrió dos sesiones del mismo día sobre tareas-hijo (tarde + noche maratón) para completar el mapa de aprendizaje C1-C10 que arrancó el 2026-05-22.

Sesión tarde — C5 Mix tooling

  • Sergio: "Vamos a seguir en lo que nos quedamos de tareas-hijo."
  • AskUserQuestion confirma seguir orden del mapa: C5 — Mix tooling.
  • Sección C5 escrita en LEARNING.md (~190 líneas, 12 subsecciones).
  • Cubre: Mix vs composer/artisan/npm scripts en una sola herramienta; mix.exs como composer.json + service provider; tareas core (deps/compile/test/help); tareas Phoenix/Ecto (generadores como código tuyo, sin registro oculto); iex -S mix phx.server como tinker + REPL dentro del server corriendo; mix release para Fly.io; aliases como scripts.json; mix.lock; config por entorno con la trampa prod.exs vs runtime.exs (vars de entorno DEBEN ir en runtime.exs o se hornean en build vacías); tareas custom propias; cheatsheet diario.
  • 3 puntos no obvios resaltados a Sergio en chat: (1) REPL-en-servidor que Laravel no tiene; (2) runtime.exs para env vars en prod; (3) generadores que NO son magia runtime.

Sesión noche — maratón C6→C10 + cierre del mapa

  • Sergio: "Es sesion nuevo, vamos a seguir con C6, explicame a fondo aqui en el chat, como en la sesion pasada".
  • Después en cadena, sin descansos: pasar a archivo + arrancar C7 → C8 → C9 → C10. Modo estricto "Sergio escribe, Antigravity explica" mantenido todo el camino.
  • 5 capas explicadas a fondo primero en chat, después persistidas en LEARNING.md. Total agregado al archivo durante la noche: ~1,500 líneas. Cierre del mapa con sección "🎉 Mapa del territorio completo" listando las 10 capas con resumen 1-línea.

Lo cubierto por capa

  • C6 — Estructura Phoenix: árbol que mix phx.new deja, split lib/app/ (negocio puro, sin conocer HTTP) vs lib/app_web/ (HTTP/LiveView/render) con regla "app jamás llama a app_web", convención naming módulo↔archivo, contexts como API entre _web y BD, diagrama de flujo de request, archivos top-level app.ex + app_web.ex con __using__ macros, dentro de _web/, priv/ (Erlang-style no-Elixir folder), test/ espeja lib/, application.ex preview, propuesta concreta de 3 contexts para tareas-hijo (Cuentas/Tareas/Recompensas), cheatsheet "dónde va qué" Laravel→Phoenix.
  • C7 — Ecto: las 4 piezas (migration/schema/changeset/repo) y por qué separadas; Data Mapper vs Active Record como clave mental ($user->save() NO existe, siempre Repo.update(changeset)); migrations con change/0 reversible y priv/repo/migrations/ con inserted_at no created_at; schemas con field :foo, virtual: true y asociaciones que NO cargan auto (devuelven %NotLoaded{}); changesets como pipeline (cast whitelist obligatorio + validate_* + unique_constraint etiqueta error de BD no valida); Repo como única puerta; Query DSL con macros + pinning ^var; preload explícito como feature; transacciones con Repo.transaction o Ecto.Multi; constraints vs validations (las dos siempre); embedded schemas para JSONB sin tabla; ejemplo realista completo de Tareas.Instancia con migration+schema+context+LV; cheatsheet Eloquent→Ecto.
  • C8 — LiveView lifecycle: flujo completo (HTTP initial + WS connected); mount/3 corre 2x con connected?(socket) guard para suscripciones/timers; handle_params/3 para cambios de URL sin recarga vs mount/3 para inicialización; render/1 + HEEx con {expr} moderno, :for/:if directivas y XSS-escape default; handle_event/3 del cliente (phx-click, phx-submit, phx-change con auto-debounce); handle_info/2 de procesos (conecta C1+C10 directo); assigns y diffing (LiveView no re-envía partes que no cambiaron); forms con to_form + phx-change="validate" + Map.put(:action, :validate); streams para listas grandes (cliente guarda, server no); LiveComponent vs function component vs LiveView aparte; navegación push_navigate vs push_patch vs redirect + sigil ~p"/..." validado en compile-time.
  • C9 — Supervisores + "let it crash": filosofía contra-PHP/JS (procesos baratos+aislados+reiniciables = reiniciar > recuperarse); procesos como unidad de aislamiento; supervisor tree; Application.start/2 como raíz; child specs (atom/tupla/mapa) con restart :permanent/:transient/:temporary; 3 estrategias (:one_for_one default, :one_for_all, :rest_for_one); max_restarts/max_seconds como red de seguridad; GenServer básico con call/cast y init/1/handle_*; DynamicSupervisor para procesos que aparecen/mueren; cómo Phoenix encaja (Endpoint es supervisor que contiene LV dynamic supervisor); panorama completo del árbol de tareas-hijo (TareasHijo.Supervisor → Repo + PubSub + Finch + Oban + Endpoint); aplicación práctica + cuándo try/rescue SÍ tiene sentido (límite con código externo, LiveView para no matar sesión del user, legacy Erlang).
  • C10 — PubSub: qué es (broker pub/sub intra-cluster BEAM) y qué NO es (no Redis, no MQ, no WS, no service bus); arquitectura supervisor + process group; los 3 verbos (subscribe/broadcast/unsubscribe); handle_info/2 como integración con C1+C8; estrategia de tópicos (granularidad: por_tarea = ruidoso, "todo" = no aislado, sweet spot familia:N + user:N + validacion:token); variantes broadcast / broadcast_from / local_broadcast; distribuido entre nodos BEAM (DNSCluster de Fly.io auto); patrón "context emite, LiveView escucha" (broadcast desde el context con función helper privada, NO desde LV — si emites desde LV otros callers no broadcastean); caso real completo de tareas-hijo (hijo marca → context broadcasts → todos los LVs reciben → DOM update via diff de LV); uso con uploads (foto va en el struct broadcastado) y push_event/3 al cliente para confetti/sonido; comparación con stack Laravel (Redis + Pusher/Reverb + Echo + auth + handlers = 3 líneas Elixir).

Estado del proyecto tareas-hijo

  • ✅ Arquitectura completa cerrada (#182, 2026-05-22 madrugada). Doc en ARCHITECTURE.md.
  • ✅ Cambio Meta Cloud API → deep link wa.me/ (2026-05-23 mañana).
  • ✅ Mapa de aprendizaje C1-C10 completo en LEARNING.md (2026-05-22 → 2026-05-23).
  • ⏳ Próxima sesión: Fase 0 (#183)mix phx.new tareas_hijo --database postgres --no-mailer, Fly.io app + Postgres provisionados, "hola Leonardo" deployado en subdominio (tentativo tareas.sevaor.dev), Tailwind funcionando, repo GitHub privado. Estimación 0.5 sem.
  • Modo cambia: deja de ser "Sergio escribe, Antigravity explica" y pasa a co-construcción con el modelo mental en su lugar.

Sesión cerrada (final del día)

Sesión cerrada por Sergio tras cerrar el mapa C1-C10. Día con dos cierres significativos: incidente monitoreo (recovery + #192 setgid permanente + #193-#196 abiertos) y mapa de aprendizaje Phoenix completo.