Informe resultados HA 08 v2 - martin-hendricks/MISO-PROYECTO-FINAL-SOLVENTA GitHub Wiki

Informe de resultados — Experimento HA-08

  • Módulo 4 — EDA
  • Maestría en Ingeniería de Software — MISW4501 Arquitectura de Software
  • Caso de estudio: Solventa — plataforma insurtech
  • Versión: 2.0 — 2026-09-11 (v1: Informe_Resultados_HA-08.md)

1. Resumen

A este volumen (1.000.000 de siniestros) y en una rampa de 10 a 80 req/s (media real ~30.8 req/s — el protocolo formal usa ramping-arrival-rate, que interpola linealmente entre escalones; 80 req/s solo se toca en el último instante, no se sostiene), la lectura directa sobre el modelo transaccional normalizado (brazo A) ya satisface EC-LAT-11 (p95 ≤ 150 ms) con 19-24× de margen. La hipótesis de que la separación de modelos de lectura (CQRS) es necesaria para cumplir este ASR no se confirma en las condiciones ensayadas. Si se implementa de todos modos, la proyección materializada sin caché (brazo B) es preferible al caché en memoria (brazo C): C introduce un costo de escritura neto sin ganancia de latencia bajo el patrón de acceso ensayado. La justificación válida para separar el modelo de lectura no es la latencia — es el aislamiento de carga sobre Postgres, respaldado con evidencia cuantitativa de CPU (§4, Anexo D). Este dictamen se puso a prueba bajo condiciones deliberadamente más duras que las del montaje original (shared_buffers reducido 4× y el hot set dispersado físicamente en vez de contiguo, siguiendo una advertencia explícita de la guía técnica del equipo) y se sostiene: el p95 de A solo sube 4.2%, de 19.1× a 18.3× de margen (§3ter).

Este resultado se obtuvo tras corregir un defecto de orquestación que invalidó una primera ejecución completa de 12 corridas (§2) y tres rondas de revisión externa que identificaron errores de interpretación adicionales, ya corregidos en la versión de este informe (ver bitácora técnica para el detalle completo del proceso).


2. Nota sobre la primera ejecución (invalidada)

Una primera ejecución completa del protocolo formal (2026-09-08, 12 corridas) resultó inválida: scripts/verify_parity.sh recreaba el contenedor de la API con los brazos A, B y C en un bucle para comparar payloads, y lo dejaba arrancado con el último brazo del bucle (C). El script de orquestación invocaba esa verificación dentro de cada corrida, después de levantar el brazo correcto y sin volver a levantarlo antes de medir con k6. Consecuencia: las 12 corridas midieron todas el brazo C, sin importar la etiqueta A/B/C del archivo de resultados.

El defecto se detectó, se reprodujo de forma controlada y se corrigió: la verificación de paridad ahora corre una sola vez antes de toda la serie, y se agregó una aserción dura (GET /health → compara el brazo activo contra el esperado) que corta la ejecución si la API no está sirviendo el brazo correcto. Todas las corridas reportadas en este informe (§3-§6) se ejecutaron con esa corrección activa y ninguna falló la aserción. El detalle completo de la detección, reproducción y corrección está en experiments/ha-08-lectura-siniestro/INFORME.md §0.


3. Resultados por brazo (escalón de operación regular)

Protocolo contrabalanceado (cuadrado latino: R1=A→B→C, R2=B→C→A, R3=C→A→B), rampa de 10→80 req/s en 4 escalones de 3 min (media real ~30.8 req/s — read_estado.js no declara thresholds por sub-métrica, así que el p95 reportado agrega toda la corrida, incluyendo el minuto de warm-up), dataset de 1.000.000 de siniestros.

Brazo Corrida RPS p50 (ms) p95 (ms) p99 (ms) Error (%) Hit-rate (%) Lag p95 (s) ¿Cumple?
A R1 10-80 2.63 8.65 n/d¹ 0 n/a ≤0.05² ✅
A R2 10-80 2.82 7.79 n/d¹ 0 n/a ≤0.05² ✅
A R3 10-80 3.06 7.13 n/d¹ 0 n/a ≤0.05² ✅
B R1 10-80 2.63 6.44 n/d¹ 0 n/a ≤0.05² ✅
B R2 10-80 2.54 6.05 n/d¹ 0 n/a ≤0.05² ✅
B R3 10-80 2.70 6.06 n/d¹ 0 n/a ≤0.05² ✅
C R1 10-80 2.84 6.77 n/d¹ 0 n/d³ ≤0.05² ✅
C R2 10-80 2.86 6.72 n/d¹ 0 n/d³ ≤0.05² ✅
C R3 10-80 2.89 6.67 n/d¹ 0 7.6⁴ ≤0.05² ✅
C' R1 10-80 — — — — — — ⚠️ pendiente⁵

¹ p99 no disponible en el protocolo formal — la configuración de k6 usada solo exportó p(90)/p(95) en el resumen. Sí está disponible para la iteración de alta carga (§6). ² El lag reportado por Prometheus es un histograma con el bucket más fino en 0.05s; el valor real es "≤50ms", no una medición puntual — ≥99.5% de las observaciones caen en ese bucket en las 9 corridas. ³ Hit-rate no capturado para C R1/R2: el fix que captura los contadores de caché se aplicó mientras la serie ya estaba en curso, después de que esas dos corridas ya habían pasado el paso de captura. No es recuperable retroactivamente. ⁴ Estimado sobre un solo worker de uvicorn (la API corre --workers 2 sin prometheus_client en modo multiproceso) — esta corrida capturó el 91% de las 23.999 peticiones reales (21.948), muestra representativa. Ver nota de método completa en la bitácora técnica §2.3. ⁵ La variante C' (coalescencia, bypass de re-serialización) corrió bajo el mismo defecto de la sesión inválida (§2) y no se ha repetido con el script corregido.

Consolidado y variabilidad (n=3 por brazo)

Brazo p95 medio (ms) Desviación estándar (ms) Diferencia vs. B Veredicto
A 7.86 0.76 +27% (peor) Cumple con 19× de margen; es el más lento de los tres, pero la diferencia no compromete el ASR
C 6.72 0.05 +9% (peor) Cumple con 22× de margen; peor que B — el costo de escritura del caché supera el ahorro de los aciertos
B 6.19 0.22 — (referencia) Cumple con 24× de margen; el más rápido de los tres

A > C > B es consistente y reproducible en las 3 repeticiones de cada brazo, sin excepción. La diferencia C−B (0.53ms) es ~4× la desviación estándar combinada (~0.14ms) — es señal, no ruido de medición. Pero esa diferencia representa menos del 1.2% del umbral de 150ms: ningún brazo está remotamente en riesgo de incumplir el ASR, así que la diferencia entre ellos no altera el dictamen de cumplimiento — es información útil para elegir entre B y C si se decide implementar CQRS por otras razones (§4), no evidencia de que A sea insuficiente.


3bis. Latencia interna media

Además del p95 medido por k6 (cliente), la API expone ha08_read_latency_seconds, un histograma que mide el tiempo dentro del bloque with REQUEST_LATENCY.labels(arm=arm).time(): de main.py — desde que empieza a resolver la estrategia de lectura hasta el return. El bucket más fino de ese histograma es 5ms, y las latencias internas reales caen casi todas por debajo, así que un percentil calculado sobre esos buckets no es válido (se intentó en una versión anterior de este informe y se descartó). Los contadores _sum/_count sí son exactos y dan la latencia media (no un percentil) sin depender de buckets:

Brazo Latencia interna media por corrida (ms) Promedio
A 2.50 / 2.45 / 2.08 2.34
B 1.70 / 1.50 / 1.49 1.56
C 2.02 / 2.03 / 1.91 1.99

C−B = 0.42ms, consistente con el costo de escritura de 0.45ms ya establecido en §4 (comparando C-TTL0 vs. C-TTL30) — es una confirmación independiente, con otra métrica, de que el costo del SET+serialización en cada miss de caché es real y de esa magnitud.

Esta métrica no mide "overhead de transporte". El cronómetro termina antes de la validación Pydantic (response_model=EstadoSiniestro) y de ORJSONResponse — la diferencia entre esta latencia interna (1.56-2.34ms) y el p95 de k6 (6.19-7.86ms) incluye esa serialización completa, no solo el transporte HTTP. Aislar ese costo específico requeriría el brazo C' (bypass de serialización), pendiente de correr con el script corregido (§9).

Como ha08_read_latency_seconds no tiene label run_id, la ventana de cada corrida se estimó por timestamp (no por una fuente que confirme el límite exacto) — se verificó que no hay solapamiento entre corridas consecutivas, pero es la misma limitación que afecta al panel "p95 interno por brazo" del dashboard de Grafana (§10). Detalle completo en la bitácora técnica §2.1quater.


3ter. Validación del dictamen bajo condiciones más duras

La auditoría externa señaló un hallazgo crítico: Guia_Tecnica_HA-08.md §14 clasifica explícitamente un "p95 del brazo A sospechosamente bajo (< 20ms)" como síntoma a corregir, con acción prescrita "reducir shared_buffers" — A midió 7.86ms, dentro de esa zona de alarma, y esa acción nunca se había ejecutado. Además, el hot set (80% del tráfico, IDs 1-10.000) corresponde a las primeras filas insertadas en el seed script — físicamente contiguas en el heap, la disposición más favorable posible para los JOIN de A.

Se repitió el brazo A bajo ambas condiciones simultáneamente: shared_buffers reducido 4× (256MB→64MB, recreando el contenedor de Postgres para aplicar el cambio) y el hot set disperso (múltiplos de 100 en el rango 1-1.000.000, en vez de 1-10.000 consecutivos — mismo tamaño, distribuido por todo el heap).

A original A validación (64MB, hot set disperso) Diferencia
p95 7.86ms 8.19ms +0.33ms (+4.2%)
Margen sobre el umbral 19.1× 18.3× —
Error 0% 0% —
CPU Postgres (avg/max) 4.9-5.5% / 9.3-11.1% 8.6% / 30.2% ~3× más I/O real

El dictamen se sostiene. El cambio de configuración sí tuvo efecto real (CPU de Postgres casi se triplicó, confirmando que se forzó I/O real en vez de resolver todo desde caché de página), pero el impacto en el p95 es marginal — A no se acerca al umbral de 150ms ni bajo las condiciones que la propia guía técnica del equipo señalaba como necesarias para descartar que el resultado fuera un artefacto del montaje.


4. Sensibilidad al TTL del caché (punto de sensibilidad 2)

Brazo C, n=3 por valor de TTL, mismo protocolo de carga que el escalón regular.

TTL p95 (ms) Hit-rate (%) Staleness observado p95 (s) ¿Aceptable para HA-08?
0 s (sin caché) 6.27 0.0 n/a ✅ — equivale al brazo B sin el SET
30 s (protocolo formal) 6.72 7.6 ≤0.05⁶ ✅ — cumple, pero es el peor de los tres TTL medidos
300 s 6.50 32-33 hasta 300s⁷ ⚠️ — cumple latencia, pero introduce riesgo de consistencia no cuantificado

Cambio en v2 respecto a v1: los valores de TTL=0 y TTL=300 reflejan las 6 corridas completamente limpias (r1b/r2/r3 de cada valor), no las 6 originales de v1 donde r1 de ambos TTL tenía un http_reqs.rate real de ~50% del diseñado (14.1/16.5 req/s vs. ~30.8 req/s). Se repitieron ttl0_r1 y ttl300_r1 como ttl0_r1b/ttl300_r1b, confirmadas con http_reqs.rate = 30.77 req/s (criterio objetivo de corrida limpia). El patrón no cambia (TTL=300 sigue siendo más lento que TTL=0), pero la desviación estándar de la serie completa bajó de 0.16/0.09ms a 0.06/0.06ms sin las corridas contaminadas. Detalle completo en la bitácora técnica §2.3bis.

⁶ El lag medido es el del proyector (evento → proyección), no el que ve el cliente vía caché — ver riesgo de carrera de invalidación abajo. ⁷ arm_c.py tiene una ventana de carrera con el proyector: en un miss, la secuencia es GET falla → lee proyección → escribe en Redis. Si el proyector invalida la clave entre la lectura y la escritura, el SET posterior sobrescribe esa invalidación con un valor obsoleto, que persiste hasta que expire el TTL completo (hasta 300s). No se pudo medir esta carrera en esta ejecución porque EstadoSiniestro no expone el número de versión del siniestro en la respuesta — se documenta como riesgo abierto, no como hallazgo cuantificado.

El costo real de C no está en el GET que falla — está en la serialización y el SET. Comparando series (nota: B, TTL=0 y TTL=30 corrieron en corridas distintas, comparación indicativa): B promedia 6.19ms, C con TTL=0 (que hace el GET fallido pero nunca ejecuta el SET) promedia 6.27ms — diferencia de 0.08ms, prácticamente cero. C con TTL=30 (que sí serializa y escribe en cada miss) promedia 6.72ms — 0.45ms más. El costo agregado está en orjson.dumps + SET, ejecutado en el ~92% de las peticiones dado el hit-rate real. Esto sugiere una mejora no implementada en este experimento: escribir en caché de forma asíncrona, sin bloquear la respuesta al cliente.

El hit-rate bajo no es falta de calentamiento — es el resultado matemático esperado. Con tráfico uniforme sobre el hot set de 10.000 claves, el hit-rate en estado estacionario para una clave con tasa de llegada λ=0.8·rps/10.000 (el factor 0.8 es la fracción de tráfico dirigida al hot set) se aproxima por λ·TTL / (1 + λ·TTL). Ponderado por solicitudes reales de cada escalón, TTL=30s da ~9.6% en estado estacionario — cercano al 7.6% medido. Para TTL=300s la misma fórmula predice ~44% en estado estacionario, pero se midió 32-33% (en las 3 corridas limpias — r1b/r2/r3): cada escalón dura 180s, menos que el TTL, y arranca con FLUSHALL — el caché nunca alcanza su estado estacionario dentro de la ventana medida. Correr la corrida más tiempo no cambiaría el hit-rate de TTL=30 (ya en estado estacionario), pero sí el de TTL=300. Lo que sí cambiaría ambos es la distribución de acceso: este diseño usa selección uniforme, pero el acceso real a un sistema de siniestros probablemente sigue una distribución sesgada (tipo Zipf) donde el hit-rate real de producción podría ser sustancialmente más alto.

Nota de calidad de dato (resuelta en esta versión): las corridas originales TTL=0 r1 y TTL=300 r1 mostraban una firma de contención del host no detectada en su momento — http_reqs.rate real de 14.1 y 16.5 req/s contra ~30.8 req/s en las demás, es decir, la carga aplicada fue ~50% de la diseñada, no solo un outlier de cola. Se repitieron como ttl0_r1b/ttl300_r1b (confirmadas con http_reqs.rate = 30.77 req/s), y la tabla de §4 ya refleja esos valores. Ver bitácora técnica §2.3bis para el detalle completo del proceso.


5. Sensibilidad a la carga (punto de sensibilidad 3)

RPS p95 brazo A p95 brazo B p95 brazo C
10 no disponible⁸ no disponible⁸ no disponible⁸
25 no disponible⁸ no disponible⁸ no disponible⁸
50 no disponible⁸ no disponible⁸ no disponible⁸
80 no disponible⁸ no disponible⁸ no disponible⁸

(p95 global de la corrida completa 10→80 req/s, como referencia: A=8.65ms, B=6.44ms, C=6.77ms, R1)

⁸ El protocolo formal no registró el p95 por escalón individual — el --summary-export de k6 solo expone un p95 agregado de toda la corrida (10→80 req/s), y la vía prevista para reconstruirlo desde Prometheus (histogram_quantile() sobre k6_ha08_estado_duration_bucket) no es viable: se verificó que esa métrica no existe en Prometheus, porque la configuración usada exporta gauges de percentiles precalculados, no histogramas nativos. Los valores de la tabla son el p95 global de R1 como referencia — no reflejan el escalón real. Esto sí se resolvió para la iteración de alta carga (tabla siguiente), agregando thresholds por sub-métrica en options.thresholds de k6.

No se encontró el punto de quiebre dentro de 10-80 req/s — ningún brazo se acercó al umbral de 150ms. Se extendió la búsqueda con una iteración adicional de alta carga:

Escalón (pico nominal) p95 brazo A p95 brazo B p95 brazo C
r150 (rampa 10→150 rps) 5.13 4.84 5.24
r300 (rampa 150→300 rps) 2.63 2.98 2.76
r600 (rampa 300→600 rps) 2.75 1.50 1.90

Nota de método: los "escalones" de k6 (ramping-arrival-rate) son rampas lineales, no mesetas constantes — se confirmó por aritmética (el total de requests observado, 136.499, coincide con la integral de las rampas, 136.500, no con lo que darían mesetas constantes, ≈189.600). "r600" alcanza 600 req/s solo en su último instante, con un promedio real de ~450 req/s en ese tramo. Que r150 tenga el p95 más alto de los tres escalones en los tres brazos no es degradación por carga — es la ventana de arranque en frío tras el docker compose restart postgres del inicio de la corrida (shared_buffers vacío, planes de consulta sin cachear).

Tampoco se encontró el punto de quiebre hasta un pico de 600 req/s (7.5× el máximo del protocolo formal), con 0% de error y 0 iteraciones descartadas en los tres brazos. El p99 de A en el escalón r600 (22.89ms global, 60.16ms en ese tramo) sí muestra degradación de cola que el p95 no captura, señal temprana de que el punto de quiebre está más cerca de lo que sugiere el p95 solo. El punto de quiebre real está por encima de este pico en el montaje actual (1M de filas, índices efectivos, CPU de la API — límite real de 2 cores, no 1 — lejos de saturarse); una vía más económica para localizarlo sería reducir deliberadamente el CPU o el pool de conexiones de la API para forzar contención observable a tasas más bajas — no se ejecutó en esta sesión.


6. Evidencia de aislamiento de carga (hallazgo adicional, no previsto en el diseño original)

Durante la iteración de alta carga se capturó el uso de CPU de Postgres por contenedor, en serie cada 10 segundos durante toda la corrida (no un snapshot puntual). En el tramo final (rampa 300→600 req/s):

Brazo CPU ha08-postgres (promedio / máximo)
A 26.7% / 67.7%
B 10.7% / 13.4%
C 8.1% / 13.6%

A impone ~2.5× la carga de CPU sobre Postgres frente a B/C. El porcentaje de docker stats es relativo a 1 core: el máximo de A (67.7%) equivale a ~0.68 cores, ~34% del límite de 2 cores del contenedor — lejos de saturarlo. Lo que se sostiene es la magnitud relativa (~2.5-3× más CPU que B/C en el mismo tramo), no la cercanía a un límite. Esta es evidencia cuantitativa directa de que B sí se justifica frente a A — no por latencia, sino por aislamiento de carga sobre el almacén transaccional: a mayor escala o con más tráfico de escritura concurrente, esa proporción competiría directamente con las transacciones de negocio (creación de siniestros, actualización de hitos) por los mismos recursos de Postgres, mientras que B (y C) no.


7. Dictamen sobre la hipótesis HD-08

Aplicando el criterio del Anexo B del diseño (se acepta HD-08 si el brazo C cumple p95≤150ms/error<1%/lag≤2s en las 3 corridas, y no hay staleness excesivo):

  • El brazo C sí cumple los tres criterios formales (6.72ms, 0% error, lag ≤50ms) — HD-08 se acepta en su enunciado literal.
  • Pero el propio Anexo B anticipa el "resultado intermedio relevante": "si el brazo B ya cumple el umbral, el caché constituye complejidad no justificada y la recomendación debe ser la alternativa más simple que satisface el ASR". Ese resultado intermedio se materializó, y de forma más extrema de lo previsto: no solo B cumple sin necesidad de C — A, la línea base sin ningún tipo de separación de modelos, ya cumple el ASR con 19× de margen.

El dictamen honesto, siguiendo el propósito declarado del experimento ("determinar el nivel mínimo de complejidad que satisface el ASR"), no es "aceptar HD-08 porque C cumple los tres criterios" — es reconocer que la complejidad mínima suficiente es A, y que recomendar B o C por ser más rápidos que A no se sostiene como argumento de necesidad contra un umbral que A ya cumple holgadamente.

Sobre las sub-hipótesis:

ID Enunciado Veredicto
HD-08.1 A no alcanza el p95 objetivo Refutada. A cumple con 19× de margen — es el más lento de los tres, pero no compromete el umbral.
HD-08.2 B reduce el p95 ≥60% vs. línea base Refutada en magnitud. B es el más rápido (−21% vs. A), en la dirección esperada, pero muy por debajo del 60% planteado.
HD-08.3 C mejora sobre B Refutada, en la dirección contraria a la esperada. C es peor que B (+9%) — el costo de escritura del caché supera el ahorro de los aciertos con el hit-rate real observado.
HD-08.4 Lag de proyección ≤2s Aceptada con margen amplio (≤50ms medido), aunque solo cubre el lag del proyector, no el staleness que ve el cliente vía caché (§4).

Dos caminos honestos para la decisión arquitectónica de Solventa (ninguno es "implementar B porque es más rápido que A"):

  1. Aceptar que, a esta escala y con este patrón de acceso, la hipótesis de que CQRS es necesario para EC-LAT-11 no se confirma — usar A como línea base y reconsiderar CQRS si el punto de quiebre real (§5) se acerca al rango de operación esperado, o si el dataset/patrón de acceso de producción difiere sustancialmente del ensayado.
  2. Justificar la separación de modelos (B) con otro ASR distinto de latencia — aislamiento de carga de lectura sobre el modelo transaccional (§6, con evidencia cuantitativa de CPU), escalabilidad horizontal independiente del lado de lectura, o disponibilidad. Este experimento no midió esos atributos directamente salvo el de CPU (§6), así que la justificación completa requeriría un experimento adicional centrado en esos ASR.

En ambos caminos, el caché en memoria (C) no aporta valor con el patrón de acceso ensayado y agrega complejidad operativa (Redis, invalidación, TTL, el riesgo de carrera de §4) sin contrapartida de latencia.


8. Amenazas a la validez (Anexo D)

Derivadas del montaje local

  • El umbral absoluto de 150ms no es transferible. El hardware de desarrollo no es producción. Se reporta la mejora relativa entre brazos como resultado principal; el valor absoluto es indicativo.
  • El montaje favorece estructuralmente al brazo A. effective_cache_size no reserva memoria — es solo una pista para el planificador de Postgres, no un límite real de qué cabe en caché. La VM de Docker tiene 8.2GB disponibles y el contenedor de Postgres un límite de 2GB; el dataset completo pesa 3GB, pero el 80% del tráfico se concentra en el hot set de 10.000 siniestros (~100.000 filas relevantes, con 5 hitos y 3 documentos exactos por siniestro, verificado por query directa) que cabe cómodamente en caché de página. No hay latencia de red entre contenedores. Un siniestro real con decenas de hitos haría más pesado el JOIN de A. El dictamen "A cumple" debe leerse como "A cumple en estas condiciones", no como garantía a cualquier escala.
  • Contención de recursos del host. Se detectaron dos episodios de contención por procesos ajenos al experimento durante la sesión: un clúster de Kubernetes de Docker Desktop (resuelto deshabilitándolo) y un contenedor de otro proyecto corriendo en paralelo (resuelto deteniéndolo para las corridas afectadas). El p95 nunca se vio comprometido por esta contención — solo la cola extrema (max) y, en dos corridas de TTL no repetidas, el throughput real. Documentado en detalle en la bitácora técnica.
  • Caché de páginas del sistema operativo no controlable. Mitigado mediante el contrabalanceo del orden de brazos (cuadrado latino).

Derivadas del runtime

  • Los contadores de hit-rate son una estimación sobre un solo worker de uvicorn, no el total de la corrida. La API corre con --workers 2 sin prometheus_client en modo multiproceso — cada worker mantiene sus propios contadores, y el curl /metrics que los captura cae en un worker elegido al azar por el balanceo del kernel. La muestra real capturada varió entre 10% y 91% de las peticiones según la corrida. Como ambos workers ejecutan el mismo código, las estimaciones siguen siendo razonables, pero no deben presentarse como el total exacto de consultas.
  • Variabilidad en la cola de la distribución. Mitigado con tres corridas independientes por brazo, descarte del período de calentamiento y reporte de la desviación entre corridas (§3).
  • Número de procesos de servidor como variable confusora. Fijado en 2, idéntico entre brazos.

Derivadas del diseño del experimento

  • Representatividad del perfil de carga. El modelo de conjunto caliente/frío (80%/20%, selección uniforme dentro de cada grupo) es una aproximación razonada, no un dato medido en producción. El hit-rate del brazo C es sensible a este supuesto — una distribución de acceso sesgada (tipo Zipf) probablemente daría un hit-rate real más alto (§4).
  • Riesgo de consistencia eventual no cuantificado. La carrera entre la invalidación del proyector y la escritura de arm_c.py (§4) puede dejar valores obsoletos en caché por hasta el TTL completo; no se pudo medir en esta ejecución por falta de instrumentación (número de versión no expuesto en la respuesta de la API).
  • Un solo tipo de consulta. Se mide únicamente la consulta por identificador de siniestro. El escenario EC-LAT-11 menciona también la consulta de póliza, no cubierta por este experimento.
  • Punto de quiebre no localizado. Ninguno de los tres brazos se degradó dentro del rango de carga ensayado (hasta un pico de 600 req/s) — el experimento no puede pronunciarse sobre el comportamiento de ninguna alternativa por encima de ese rango en este montaje.

9. Evidencia visual (dashboard de Grafana)

Cada captura corresponde a los 6 paneles del dashboard (p95 interno por brazo, hit-rate de caché, lag de proyección, p95 de k6, CPU y memoria del proceso), acotados a la ventana de tiempo exacta de la serie indicada. JSON del dashboard, reproducible: experiments/ha-08-lectura-siniestro/observability/grafana/provisioning/dashboards/ha08-dashboard.json.

Protocolo formal (9 corridas, §3)

[Dashboard de Grafana — protocolo formal]

Sensibilidad al TTL (§4)

[Dashboard de Grafana — TTL=0, incluye ttl0_r1 con contención (ver nota en §4)]

[Dashboard de Grafana — TTL=300 r1, con contención (ver nota en §4)]

[Dashboard de Grafana — TTL r2/r3, repetición limpia de ambos valores]

[Dashboard de Grafana — TTL=0 r1b, repetición limpia]

[Dashboard de Grafana — TTL=300 r1b, repetición limpia]

Alta carga (§5, §6)

[Dashboard de Grafana — alta carga A/B/C]

Las capturas grafana_ttl0.png y grafana_ttl300_r1.png (arriba) muestran las corridas originales con contención (r1, http_reqs.rate ~50% del diseñado); grafana_ttl0_r1b.png y grafana_ttl300_r1b.png muestran su repetición limpia (http_reqs.rate = 30.77 req/s), la que respalda los valores de la tabla de §4. Las 9 capturas de la sesión inválida del 2026-09-08 se conservan como evidencia del proceso de detección del bug, no como validación de resultados — ver experiments/ha-08-lectura-siniestro/results/evidencia/r{1,2,3}-{a,b,c}.png y la bitácora técnica §4.2.


10. Carpeta completa del experimento en el repositorio de código

Todo lo ejecutado —implementación, scripts de orquestación, evidencia cruda y bitácora técnica— vive en el repositorio de código, no en esta wiki:

experiments/ha-08-lectura-siniestro/
├── api/                    # Implementación de los 3 brazos (A/B/C), FastAPI
├── projector/              # Proyector de eventos → proyección materializada
├── db/                     # Esquemas del modelo transaccional y de la proyección
├── load/k6/                # Scripts de carga (protocolo formal, TTL, alta carga)
├── observability/          # Provisioning de Prometheus y Grafana (dashboard JSON)
├── scripts/                # Orquestación: run_experiment.sh, run_serie_*.sh, verify_parity.sh, collect_results.sh
├── results/
│   ├── raw/                # summary_*.json, cache_*.txt, lag_*.txt, stats_*.csv de cada corrida — versionado, auditable
│   ├── evidencia/          # Capturas de Grafana, logs de verify_parity, tamaño del dataset
│   └── consolidado.csv     # Tabla consolidada de las 15+ corridas válidas
├── docker-compose.yml
└── INFORME.md              # Bitácora técnica completa: detección del bug, cada corrección, cada corrida

results/raw/ está versionado en el repositorio (no ignorado por git) específicamente para que las cifras de este informe sean auditables sin necesidad de reproducir las corridas.


Anexo — Trazabilidad

Artefacto Identificador / ubicación
Historia de arquitectura HA-08
Escenario de calidad EC-LAT-11
Diseño del experimento Diseno_Experimento_HA-08.md
Implementación experiments/ha-08-lectura-siniestro/
Bitácora técnica completa experiments/ha-08-lectura-siniestro/INFORME.md
Evidencia cruda (auditable) experiments/ha-08-lectura-siniestro/results/raw/ (versionado en el repositorio)
Capturas de dashboard experiments/ha-08-lectura-siniestro/results/evidencia/grafana_*.png
Fecha de ejecución válida 2026-09-09 / 2026-09-11