Volver a la wiki

Incidente: bib_compute_communities genera runs zombie por subrequest budget (sesión 25)

Resumen

El handler bib_compute_communities del Bibliotecario excedía el límite de subrequests de Cloudflare Workers paid plan (1 000/req), muriendo antes de ejecutar completeRun. El cliente recibía HTTP 200 — sin cuerpo de error visible — pero el run D1 quedaba en status='running' para siempre. Se acumularon 160 rows zombie en la tabla bib_index_runs durante varias sesiones, y las estadísticas de comunidades que mostraba el sensor (bib_stats: 292 communities, cohesión 0.83) provenían de un run antiguo que sí había cerrado correctamente.

Regla 15 al 100 %: HTTP 200 ≠ éxito semántico. El cliente debe parsear el body y verificar la presencia de "run_id" antes de loguear OK.


Cronología

MomentoEvento
Sesiones 20–24Cada cron de 10 min lanzaba bib_compute_communities; cada run moría silenciosamente ~52s después (CF Worker timeout/budget).
2026-04-25 08:00 UTCEdu pide revisar sensor Bibliotecario → descubiertos 160 runs en status='running'.
2026-04-25 08:16 UTCCommit 1af002b con fix definitivo en 5 piezas.
2026-04-25 ~08:30 UTCCleanup D1 manual: wrangler d1 execute → 160 rows marcados status='failed'.

Causa raíz

El handler iteraba secuencialmente sobre cada comunidad detectada (292 con el grafo real). Por cada comunidad realizaba:

Subrequests estimados con 292 comunidades: ~1 500–2 000 vs límite 1 000/req del plan paid. El Worker mataba el evento tras superar el budget, pero la response HTTP 200 ya había viajado al cliente (streaming). completeRun nunca se ejecutaba → row queda en running indefinidamente.

Agravante: no había try/catch en el case, así que tampoco se ejecutaba ningún failRun. El reaper automático tampoco existía hasta este fix.


Impacto


Solución definitiva (commit 1af002b)

1. Refactor handler — de ~1 500 a ~10 round trips

functions/api/mcp/handlers/biblioteca.ts:

Presupuesto total resultante:

1 SELECT nodes + 1 SELECT edges
+ ceil(N_unique_ids / 90) SELECTs nombres
+ 1 batch [DELETE + UPDATE NULL]
+ 1 batch INSERT communities
+ 1 batch UPDATE community_id
≈ 6–12 round trips independientemente del número de comunidades

2. try/catch/finally con failRun

Nueva función failRun(db, runId, error, startTime) que escribe status='failed', errors, duration_ms y completed_at. El case bib_compute_communities queda envuelto en try { ... } catch (err) { await failRun(...); throw err; }, garantizando que cualquier excepción (incluido budget excedido si CF lo propaga) cierra el run.

3. Auto-reaper de zombies en createRun

Nueva función reapZombieRuns(db) llamada al inicio de cada createRun. Marca como failed todos los runs en status='running' con started_at < datetime('now', '-5 minutes'):

UPDATE bib_index_runs
  SET status = 'failed',
      errors = '["zombie reaper: status=running > 5min, asumido muerto"]',
      completed_at = datetime('now')
WHERE status = 'running'
  AND started_at < datetime('now', '-5 minutes')

Previene acumulación silenciosa en futuras regresiones.

4. Sensor de zombie_runs

functions/_lib/hot-cache.ts: nueva query en fetchGraphHealth que cuenta runs en status='running' con started_at < datetime('now', '-30 minutes'). El campo zombie_runs: number se añade a la interfaz GraphHealth.

src/components/biblioteca/PulsePanel.tsx: alerta visual roja (#ef4444, fontWeight 600) en la sección “Salud del grafo” si zombie_runs > 0:

⚠ N run(s) zombie (status=running >30 min) — handler murió sin completar

5. Cron separado en CreaRack-Pro

scripts/cron-bib-reindex.sh recibe flag --communities-only. El cron en Hetzner Staging (/etc/cron.d/bib-reindex) pasa de 1 entrada a 2:

*/10 * * * *  root  /opt/crearack-pro/scripts/cron-bib-reindex.sh        # AST — barato, <12s
0    4  * * *  root  /opt/crearack-pro/scripts/cron-bib-reindex.sh --communities-only  # 1×/día 04:00 UTC

AST (indexación incremental) sigue cada 10 min porque completa en <12s y <50 subrequests. bib_compute_communities pasa a 1×/día.


Cleanup D1

wrangler d1 execute crearack-workspace-db --remote \
  --command "UPDATE bib_index_runs SET status='failed', errors='[\"zombie reaper: manual cleanup 2026-04-25\"]', completed_at=datetime('now') WHERE status='running'"
# → 160 rows afectados

Lecciones aprendidas

  1. HTTP 200 ≠ éxito (Regla 15): el cron script debe parsear el body y verificar "run_id" antes de loguear OK.
  2. db.batch() es obligatorio en handlers que tocan >100 filas en un loop. La regla mental: si hay más de ~100 statements en un loop secuencial, es un bug en gestación.
  3. try/catch/finally siempre en handlers MCP que abren un runId. Si el Worker muere, el finally no se ejecuta en CF (a diferencia de Node.js) — por eso se necesita el reaper externo.
  4. Sensor primero: sin zombie_runs en el Pulse, este fallo habría seguido meses sin detectarse porque las stats parecían correctas.
  5. Crons separados por coste: mezclar operaciones de coste O(1) con O(N·comunidades) en el mismo cron interval es una trampa. Separar por frecuencia adecuada al coste real.

Estado final


Véase también

Subir