Traducción. Autoritativo: fs-core-0007-job-infrastructure.md.
Contexto
Toda cola es at-least-once. Un worker va a procesar el mismo job dos veces —tras un timeout, un deploy, un reintento de proveedor— y en un sistema que acredita puntos, eso es dinero. La idempotencia no es un atributo de calidad aquí; es la condición de corrección, y por eso DEC-C6 hizoprocessed_jobs obligatorio para todo consumidor en vez de una elección por worker.
La segunda mitad es la garantía de evidencia de DEC-C5: toda falla y todo agotamiento de reintentos
queda persistido, no solo registrado en logs. Una línea de log rota; una fila de dead_letters
es algo que un operador puede encontrar, entender y reprocesar. Los éxitos a volumen son métricas,
no filas — la asimetría es deliberada.
Alcance (normativo)
core.processed_jobs: la valla de deduplicación, escrita dentro de la transacción de trabajo.core.dead_letters: fallas con payload, intentos, último error y estado de revisión.- El harness de consumidor: valla de idempotencia, reintentos con backoff y jitter, escritor de dead letters.
- Limpieza por TTL de
processed_jobs. - Alertas a BetterStack ante dead letters nuevos.
- Replay desde Softcrum Ops, republicando con el mismo
job_id.
Fuera de alcance (normativo)
QueuePorty sus adaptadores — TS-002.- Los carriles de notificación — son de
messaging, aunque usan este harness. - Una UI de replay —
frontend/ops.
Comportamiento (normativo)
- Un consumidor verifica e inserta en
processed_jobsdentro de la misma transacción que su trabajo. Fuera de ella, una caída entre ambos deja el job marcado como hecho sin nada hecho. - Un
job_idduplicado hace ack y salta. En silencio, con una métrica — un duplicado es operación normal, no un error. - Política de reintentos: 5 intentos, backoff exponencial con jitter, base 30 s, tope 1 h. Sobreescribirla por cola exige justificación en el spec de ese módulo.
- Los reintentos agotados y las fallas duras escriben una fila de
dead_letterscon el payload completo, el número de intentos, el último error y elcorrelation_id, y alertan a BetterStack. - Los mensajes envenenados se saltan los reintentos por completo. Un payload inválido de schema nunca se volverá válido; reintentarlo cinco veces son cinco minutos y cinco alertas idénticas desperdiciados.
- El replay republica con el mismo
job_id, de modo queprocessed_jobsgarantiza que no haya doble efecto. Esa propiedad es lo que hace al replay seguro para usarlo en masa. - Las filas de
processed_jobsexpiran por TTL; el TTL debe exceder con margen la ventana máxima de reintentos, o un reintento tardío tras la expiración volvería a aplicar el trabajo. - Un dead letter pasa
pending_review → replayed | discarded, y descartar exige una razón. - Los éxitos de alto volumen son métricas, no filas. Escribir una fila por job exitoso haría de esta tabla la más grande de la plataforma sin beneficio.
Datos (normativo)
API (normativo)
En la práctica son de Ops: un tenant no administra nuestras colas.
Eventos (normativo)
Ninguno en el outbox. Los dead letters alertan por observabilidad, no por eventos de dominio.Criterios de aceptación (normativo)
- El mismo
job_idprocesado dos veces en paralelo produce exactamente un efecto. - Una caída entre el trabajo y la valla no deja ninguno de los dos.
- Una falla transitoria reintenta 5 veces con intervalos crecientes y luego escribe un dead letter.
- Un payload inválido de schema va directo a dead letters con cero reintentos.
- El replay de un dead letter con el mismo
job_idno produce un segundo efecto. - La limpieza por TTL borra filas expiradas y nunca una dentro de su ventana de reintentos.
- Descartar sin razón se rechaza.
- Negativo: ningún job exitoso escribe una fila más allá de su valla de
processed_jobs.
Ejecución
Un solo slice, comando síncrono. Schema en TS-001; el harness en TS-002 junto aQueuePort.
Runbook: ../../../runbooks/dead-letter-replay.md.