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:
- El ciclo bloqueante del historian ante un PLC caído (E11).
- Los tests de integración que el CI saltea, incluido el del índice de idempotencia.
- La ausencia de respaldos propios y de procedimiento de recuperación (E14).
- La detección de una Pi caída, que depende de chequeos iniciados por el Cloud porque el heartbeat no está implementado (E13).
- 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.