Saltar a contenido

Calidad y resiliencia

Responde: ¿qué se espera del sistema bajo estrés, qué mecanismo lo sostiene, y qué pasa cuando ese mecanismo no está?

Contenedores C4 ya tiene la tabla de qué deja de funcionar cuando cae cada pieza, y no se repite acá. Esta página va un paso más allá: convierte esas observaciones en escenarios de calidad —estímulo, respuesta esperada, cómo se mide— y separa con claridad lo que el código garantiza de lo que es un objetivo de diseño sin implementación detrás.

La distinción importa porque el sistema tiene las dos cosas.

Escenarios de calidad

# Escenario Respuesta esperada Mecanismo Estado
E1 Se corta el enlace planta ↔ nube durante horas Ningún dato perdido; al volver el enlace se recupera todo Outbox en SQLite, watermark que avanza solo sobre lo aceptado, backoff de 1 s a 60 s implementado, con el límite de E2
E2 El corte dura más que la capacidad del outbox Contrapresión sin pérdida, no descarte El outbox deja de aceptar; el watermark no avanza; los datos siguen en InfluxDB implementado, pero el objetivo de 24 h no se cumple: ver abajo
E3 El Edge reintenta un lote que el Cloud ya había recibido Se deduplica, no se duplica eventId determinista más índice único (tenantId, eventId) en MongoDB implementado
E4 Redis deja de responder La API sigue operando Fail-open: se pierde el rate limiting, nada más implementado
E5 MongoDB deja de responder Login y ABM siguen; solo se degradan los dashboards Aislamiento de dependencias: la ingesta responde 500, el resto no lo nota implementado
E6 Se reinicia el Edge o la Pi Se reanuda donde quedó Outbox y watermark persistidos en disco implementado
E7 InfluxDB no está disponible al arrancar el Edge El servicio arranca de todos modos y reporta su estado Modo scaffold: endpoints de estado activos, reenvío deshabilitado implementado
E8 Un cliente abusa de las consultas del dashboard Se lo limita sin afectar a otros Token bucket en Redis por usuario, 15 por minuto con ráfaga de 5 implementado
E9 Un dispositivo manda lotes desmedidos Se rechaza sin tumbar la ingesta Tope de 4 MiB por cuerpo y 1000 eventos por lote, más 200 req/s con ráfaga de 1000 por clave implementado
E10 Se despliega la API mientras hay un lote de ingesta en vuelo La request en curso se drena antes de cortar Al recibir la señal de terminación la API deja de aceptar conexiones y espera hasta 8 s a las requests en curso; Cloud Run da 10 s implementado, con un límite: ver abajo
E11 Falla la lectura de un registro del PLC Se registra la mala calidad y el ciclo sigue Reintentos, pero bloqueantes; y el indicador de calidad no sale del log parcial: ver abajo
E12 Una variable cruza un umbral peligroso Se genera una alarma y se notifica — no implementado: el motor de reglas de alarma no existe
E13 La Pi se cae y nadie lo nota El sistema lo detecta y lo informa — parcial: solo por chequeos que inicia el Cloud; el heartbeat no está implementado en ninguno de los dos extremos
E14 Se corrompe o se pierde la base de producción Se restaura con pérdida acotada Respaldos diarios del plan gratuito de Supabase, y nada más insuficiente, y no hay procedimiento escrito

El límite del apagado ordenado

La API atiende las señales de terminación, deja de aceptar conexiones y espera hasta 8 s a que terminen las requests en curso, de modo que un lote de ingesta a medio escribir termina en vez de cortarse. El plazo es menor que los 10 s que da Cloud Run entre la señal y el corte, para dejar margen a cerrar Redis y el pool.

Una conexión que nunca queda inactiva —como el flujo en vivo de logs— no libera al apagado y se corta al vencer los 8 s.

Los valores concretos

Lo que el código impone, con sus valores por defecto. Son los números que hay que tocar para mover cualquier escenario de arriba.

Parámetro Valor por defecto Dónde
Intervalo de lectura del PLC 1 s historian
Intervalo de reenvío al Cloud 5 s Edge
Margen de seguridad sobre el watermark 2 s Edge
Tamaño máximo de lote 1000 eventos o 2 MiB Edge
Tope que acepta el Cloud 1000 eventos o 4 MiB Cloud
Capacidad del outbox 100 000 filas Edge
Retención de eventos muertos 30 días Edge
Backoff de reintento de 1 s a 60 s, respetando Retry-After Edge
Timeout hacia el Cloud 15 s Edge
Timeouts del servidor HTTP del Edge 10 s de lectura, 10 s de escritura Edge
Timeout hacia MongoDB 10 s Cloud
Límite de ingesta 200 req/s, ráfaga 1000, por clave de API Cloud
Límite de consultas de dashboard 15 por minuto, ráfaga 5, por usuario Cloud
Límite de invitaciones 20 por hora Cloud
Caché de claves de API resueltas 60 s Cloud
Retención de InfluxDB en la Pi sin configurar: los datos no expiran bundle

El Edge corta sus lotes en 2 MiB y el Cloud acepta hasta 4 MiB. La asimetría es deliberada: rechazar a partir de 2 MiB exactos mandaría un lote entero de hasta 1000 eventos a la cola de muertos por un byte de desajuste.

Los tres puntos más frágiles

El objetivo de 24 h offline no se cumple

Una decisión de arquitectura del Edge plantea como contexto un buffer offline de 24 horas o más. La implementación no lo garantiza: lo que impone es un límite de 100 000 filas, que la guía de despliegue en la Pi estima en unas 1,5 h de acumulación al ritmo esperado.

No se pierden datos al llenarse —el mecanismo es contrapresión— pero el objetivo declarado y el comportamiento real difieren en un orden de magnitud. Alcanzar 24 h reales requiere subir el límite y validar el espacio en disco de la Pi. El detalle está en contenedores C4.

El ciclo de lectura del historian se bloquea ante un PLC que no responde

Éste es el hallazgo menos visible y el de peor comportamiento bajo fallo.

El historian recorre sus 18 tags en serie. Ante un error de lectura reintenta hasta 3 veces, y espera 5 segundos entre intentos con una espera bloqueante. Un solo tag que falle agrega unos 15 s a un ciclo que nominalmente dura 1 s. Si el PLC deja de responder por completo, el ciclo pasa de ~1 s a unos 4,5 minutos, y las 17 variables que sí se podían leer quedan rehenes del tag que falla.

Agrava el cuadro que, si la conexión inicial al PLC falla, el historian solo registra una advertencia y sigue: queda girando indefinidamente sobre un PLC inalcanzable, sin que nada escale el problema.

No hay circuit breaker, ni lectura concurrente, ni tiempo máximo de ciclo. Es el escenario E11, y es la brecha de resiliencia más grande del sistema que no está registrada en ningún ADR.

Se pierde el indicador de calidad del dato

El historian calcula un indicador GOOD / BAD por lectura y lo escribe solo en su log estructurado: no es un tag ni un field de InfluxDB. Ni el Edge ni el Cloud ni el dashboard lo ven nunca.

La consecuencia práctica: el dashboard no puede distinguir un valor medido de un valor ausente. Cuando una lectura falla, el punto simplemente no se escribe, y la serie tiene un hueco indistinguible de una máquina detenida. Ver flujo de datos.

Qué verifica el CI, y qué no

La resiliencia que no se prueba es una hipótesis. Lo que hay hoy, por repositorio:

Repositorio Lo que corre el CI Lo que no cubre
Cloud build, vet, linter, go test ./... Los tests de integración se saltean: el CI no provee Postgres, MongoDB ni Redis. Nada verifica los índices de deduplicación, las migraciones ni las cadenas de middleware contra una base real
Frontend verificación del proyecto, y E2E con Playwright Los E2E corren en continue-on-error: informan, no bloquean el deploy. Los contratos con el Cloud no se verifican en ningún CI
Edge build para amd64 y arm64, vet, linter, imagen multi-arch, control de deriva del OpenAPI contra el código y go test (job test, vía make test) No hay integración contra un InfluxDB ni contra un Cloud real: los tests que corren son unitarios
historian tests unitarios, build de imagen arm64, y una integración real: levanta InfluxDB 2.6 y el historian en contenedores, lo deja correr y consulta por Flux que los puntos llegaron Que lleguen las 18 variables —la aserción es «al menos 2»— y los tags, que la consulta no pide. Un cambio que los renombrara pasaría en verde y rompería el sistema aguas abajo. Ningún escenario de fallo del PLC: la integración corre con el simulador
bundle — No tiene CI. El stack nunca corrió en arm64, ni en una Raspberry Pi, ni contra un Cloud real

Vale registrar algo contraintuitivo: el único CI del sistema que levanta una dependencia real y comprueba que los datos llegaron es el de proyecto-embolsadora, la tesis de maestría de un autor externo al equipo. Los cuatro repos del equipo corren tests que no tocan ninguna dependencia.

El mecanismo crítico del sistema no se prueba de punta a punta

El índice único (tenantId, eventId) de MongoDB es toda la idempotencia del sistema: sin él, cada reintento del Edge duplicaría mediciones en silencio. Ese índice se crea al arrancar la aplicación y se ejercita con tests de integración que el CI saltea por no tener una base disponible, así que un cambio que rompa su creación pasa el CI en verde. Es el hueco de mayor consecuencia del sistema.

Observabilidad

El Cloud y el Edge exponen métricas en formato Prometheus que nadie recolecta.

Sí existe un recolector en el sistema: el frontend tiene un stack de observabilidad completo, con depuración de datos personales y cinco tableros. Pero su configuración solo declara un receptor OTLP, sin receptor de Prometheus, así que no consulta a los otros dos componentes. Los dos que llevan los datos de la planta a la nube quedan fuera del único sistema de observabilidad que hay.

Los tres componentes emiten logs estructurados en JSON, y el Edge y el Cloud propagan un identificador de request. Los logs de la API quedan en Cloud Run; los de la Pi, en el driver de logging de Docker que configura el compose.

Hay un detalle de exposición: el endpoint de métricas del Edge queda fuera del middleware del secreto compartido, a propósito, para que lo pueda raspar un recolector. Con la configuración por defecto del bundle, el servicio se publica en todas las interfaces de red de la Pi.

El inventario completo de lo que se mide, lo que no, y qué construir primero está en monitoreo.

Riesgos aceptados y brechas

Separados a propósito: un riesgo aceptado es una decisión; una brecha es algo que nadie decidió.

Riesgos aceptados:

  • El sistema es de monitoreo, no de control: nunca escribe en el PLC, así que ningún fallo del software puede mover la máquina — SYS-ADR-007.
  • La lectura del PLC es sin autenticación, porque Modbus TCP no la tiene — mismo ADR.
  • El fail-open de Redis: se prefiere disponibilidad sobre el rate limiting. Es el único de los tres sin decisión escrita que lo respalde, y queda anotado en el índice de decisiones.

Brechas —sin decisión registrada—, en orden de consecuencia:

  1. El ciclo bloqueante del historian ante un PLC caído (E11).
  2. Los tests de integración que el CI saltea, incluido el del índice de idempotencia.
  3. La ausencia de respaldos propios y de procedimiento de recuperación (E14).
  4. La detección de una Pi caída, que depende de chequeos iniciados por el Cloud porque el heartbeat no está implementado (E13).
  5. InfluxDB en la Pi sin retención: crece sin límite hasta llenar el disco.

El motor de alarmas

Es el escenario E12 y la brecha más grande del sistema. Hoy no existe: hay tablas de reglas y de notificaciones en Postgres y un índice en MongoDB preparado para que un futuro motor lea lo reciente, pero ninguna lógica que las evalúe. Una regla guardada no dispara nunca.

Qué hacer con eso —implementarlo o redefinir el objetivo— se registra en visión y alcance y en la bitácora de alcance.

Tres cosas más de esta página se responden mirando el despliegue y no el código: si alguien recolecta las métricas Prometheus, si existe algún respaldo más allá del automático de Supabase, y cómo se comporta el stack sobre el hardware de planta.

Siguiente nivel

Los riesgos de seguridad y su modelo están en seguridad. Las decisiones que respaldan los riesgos aceptados de esta página están en decisiones de arquitectura.