ACH Framework · Depuración Paralela

Bug #NF-2847 — Exportación PDF Intermitente

Cliente: NutriFlow Analytics · Node.js 20 + TypeScript + PostgreSQL 15 + Redis 7

35%
tasa de fallo
Desde v2.4.1 (hace 5d)
Agentes: 6 en paralelo
Hipótesis: 6 investigadas
Evidencias: 17 recopiladas
Tiempo análisis: ~4 min
Método: ACH · Analysis of Competing Hypotheses
Progreso del Framework
Generación6 hipótesis
clasificadas
InvestigaciónAgentes en
paralelo
Evidencias17 items
citados
ArbitrajeCausa raíz
determinada
FixPlan de
corrección
ValidaciónChecklist
pendiente
Investigación Paralela — 6 Agentes Independientes
A1
Categoría: Datos
CONFIRMADO
findByDateRange devuelve null en resultados vacíos
Confianza 91%
Directa: src/repositories/macro.repository.ts:156 — query retorna null en lugar de [] cuando no hay registros en el rango
Directa: report.service.ts:216macros.map() llama a null → TypeError
Correlacional: Pacientes sin datos en el rango exacto = 35% del tráfico de clínicas grandes
A2
Categoría: Estado
CONFIRMADO
Cache Redis con TTL bajo causa race condition en escritura/lectura
Confianza 77%
Directa: TTL reducido de 3600s→300s en src/cache/config.ts:34 aumenta expiración durante picos
Correlacional: Redis hit rate bajó de 94%→71%, lag de 200-800ms coincide con re-generación
Ausencia: No hay lock distribuido en report.service.ts:211-225 para evitar stampede
A3
Categoría: Integración
PLAUSIBLE
Puppeteer v22 cambió API de generación de PDF
Confianza 42%
Correlacional: Puppeteer 21→22 actualizado en v2.4.1, changelog menciona breaking changes en page.pdf()
Contradictoria: El stack trace falla en report.service.ts:214 antes de llegar a Puppeteer
A4
Categoría: Lógica
FALSIFICADO
Off-by-one en cálculo de rango de fechas
Confianza 8%
Falsificada: src/utils/date.utils.ts:23-41 — lógica de rango validada con tests; cubre casos límite correctamente
Falsificada: El error no es consistente por rango concreto — es no determinista, descarta lógica determinista
A5
Categoría: Recursos
INCONCLUSO
Pool de conexiones PostgreSQL agotado en horas pico
Confianza 31%
Ausencia: No hay métricas de pool en logs de Railway; imposible confirmar sin añadir instrumentación
Correlacional: Fallo correlaciona con horas pico pero esto también explica el cache stampede
A6
Categoría: Entorno
FALSIFICADO
Diferencia de timezone Railway vs base de datos
Confianza 5%
Falsificada: Todos los timestamps usan UTC explícito en src/config/db.config.ts:12
Falsificada: Clientes en misma zona horaria reportan el bug en el mismo porcentaje
Matriz ACH — Evidencias vs Hipótesis
Evidencia Tipo H1 Datos H2 Estado H3 Integr. H4 Lógica H5 Recurs. H6 Entorno
macro.repository.ts:156 retorna null DIRECTA C ? N N ? N
report.service.ts:216 .map() en null DIRECTA C ? N N N N
Redis hit rate 94%→71% tras v2.4.1 CORREL. P C ? N P N
TTL cambiado 3600→300s en cache/config.ts:34 DIRECTA N C N N ? N
Fallo 35% no determinista en mismo input CORREL. P C P N P N
Stack trace falla antes de llegar a Puppeteer CONTRADICT. C ? N ? ? ?
Clínicas >500 pacientes más afectadas CORREL. C C ? N P N
Score (C=3, P=1, N=0, ?=0.5) 21 18 5 0 7 0
Arbitraje del Árbitro — Causa Raíz Determinada

Causa Compuesta: Null Return + Cache Stampede bajo Alta Concurrencia

Cadena causal

  • v2.4.1 introduce findByDateRange que retorna null (no []) cuando no hay macros en el rango
  • El TTL reducido (300s) agota la cache 12x más rápido → más cache misses
  • En picos, múltiples requests concurrentes para el mismo paciente pasan el cache miss simultáneamente (stampede)
  • macros.map(m => m.macros) en línea 216 lanza TypeError porque macros es null

Por qué es intermitente

El 65% de las llamadas tiene cache hit (los datos ya están) y funciona. El 35% ocurre en el momento exacto de expiración/miss donde findByDateRange es invocado — y si el paciente no tiene datos en el rango solicitado, retorna null.


Contribución de H3 (Puppeteer)

Descartada como causa raíz. El bug ocurre upstream en la capa de servicio, antes de invocar PDF generation. Puppeteer v22 puede tener otros issues pero no causa este bug.

Plan de Corrección — 4 Pasos Priorizados
1

Fix inmediato: null guard en findByDateRange CRÍTICO

El repositorio debe retornar array vacío, no null. Una línea de fix.

// src/repositories/macro.repository.ts:156 async findByDateRange(patientId, range) { const rows = await db.query(SQL, [patientId, range]); return rows ?? []; // garantiza array, nunca null }
2

Defensive null check en service CRÍTICO

Aunque el fix 1 resuelve la causa raíz, añadir defensa en profundidad.

// src/services/report.service.ts:215-217 const macros = await db.macros.findByDateRange( patientId, dateRange ) ?? []; const data = { macros: macros.map(m => m.macros), // seguro summary: calculateSummary(macros) };
3

Restaurar TTL y añadir lock anti-stampede ALTO

Revertir TTL a 3600s o usar mutex distribuido para evitar thundering herd.

// src/cache/config.ts:34 const REPORT_CACHE_TTL = 3600; // revertido // src/services/report.service.ts:211 const lock = await redis.set( `lock:report:${patientId}`, 1, 'NX', 'EX', 10 ); if (!lock) return waitForCache(patientId);
4

Test de regresión + monitoreo MEDIO

Añadir tests y métricas de conexiones de pool para el H5 inconcluso.

// tests/report.service.test.ts it('devuelve data vacía si no hay macros', async () => { mockRepo.findByDateRange.mockResolvedValue(null); const result = await buildReportData('p1', range); expect(result.macros).toEqual([]); expect(result.summary).toBeDefined(); });
Checklist de Validación del Fix
Fix identifica y ataca la causa raíz (null return)
Fix no introduce regresiones en clientes con macros normales
Caso de reproducción original (paciente sin macros en rango) ya no falla
Tests de regresión añadidos y pasando en CI
Redis hit rate recuperado a >90% tras revertir TTL
Monitoreo de pool DB añadido para verificar H5 en producción
Deploy v2.4.2 en producción y tasa de error <1% en 1h
Edge case: paciente con solo macros parciales en el rango cubierto