Estrategias de repetición e idempotencia
1) Por qué es necesario
En redes, las fallas son la norma: tiempos de espera, errores de tránsito, flappings de red, sobrecarga. Los retiros solo aumentan la fiabilidad si:1. la repetición es segura (idempotente),
2. se respetan los pasajes entre repeticiones,
3. se respetan los límites/cuotas y la «salud» de las adicciones.
El objetivo es un comportamiento effectively-once a nivel de operaciones empresariales sin tomas y carreras falsas.
2) Taxonomía de la semántica de la entrega
At-most-once: sin repeticiones, riesgo de pérdida (lógica, fuego y forget).
At-least-once: duplicados posibles → se necesita la idempotencia del consumidor (la mayoría de colas, webhooks).
Effectively-once: los duplicados son posibles, pero se deduplican correctamente (claves, transacciones, outbox).
3) Cuándo retraerse y cuándo no
El retiro tiene sentido: '408', '429' (respetando 'Retry-After'), '425' (Too Early), '499' (client closed en el perímetro), '5xx', '504', timeouts/brechas de red, '502' en la pasarela, «connection reset».
No retraemos sin cambiar la consulta: '400/ 401/403/404/422'.
Casos controvertidos: '409 Conflict' (normalmente no retraído; primero leemos el estado de la operación/reconfirmamos la intención).
4) Timeouts, backoff y jitter
4. 1 Reglas
Primero el taimaut, luego los retrayas: cada solicitud debe tener un «deadline».
Backoff exponencial: 'delay _ n = base 2 ^ n', limitamos' max _ delay '.
Jitter es obligatorio: agregue aleatoriedad para el desacoplamiento de «ondas sincrónicas contundentes».
4. 2 Plantillas de jitter
Full jitter: 'sleep = rand (0, base2 ^ n)' es la mejor opción general.
Decorrelated jitter: 'sleep = min (max_delay, rand (base, sleep_prev3))' - para los diálogos largos.
Equal jitter: 'sleep = base2 ^ n/2 + rand (0, base2 ^ n/2)' es una variación suave.
4. 3 Retry-budget
Limite la proporción de retraídos:- `retry_budget_per_min = max(α success_rps, floor β)`; normalmente 'α = 0. 1–0. 2`.
- Cuando se agota el presupuesto - cambiar a fail-fast/circuit breaker «abierto».
5) Interacción con rate limiting y Circuit Breaker
Respeta 'Retry-After', 'RateLimit-Reset' y cuéntalo en el back-off.
Con los altos '5xx '/timeouts - reduzca la frecuencia de retraídas y el paralelismo general.
- Medio-abierto: permite una muestra limitada.
- Abierto: rechaza instantáneamente (ahorra recursos).
- Cerrado: trabajo normal.
- En las operaciones de escritura, es preferible devolver 409/503 con una pista clara que torcer retraídas agresivas.
6) Idempotencia de las operaciones de escritura
6. 1 Idea general
Las mismas intenciones → un solo resultado. La base es la clave de idempotencia y el repositorio de registros de ejecución.
6. 2 contrato HTTP
El cliente envía el encabezado:
Idempotency-Key: 7a6b7f9e-2a46-4d0b-9c3a-2b30e1c3c9e3
Idempotency-Key-Expiry: 24h # optional
Servidor:
- la primera vez que se realiza correctamente, guarda (clave → resultado, estado, hash del cuerpo);
- cuando se repite, devuelve la respuesta anterior y el encabezado 'Idempotency-Replay: true';
- cuando hay un conflicto corporal (la misma clave pero otra payload) es '409 Conflict'.
6. 3 Almacenamiento y TTL
Tabla/clave-valor: 'idempotency _ key', 'request _ hash', 'nat', 'status', 'expiry _ at'.
TTL = ventana de repeticiones posibles y entregas tardías (generalmente 24-72 h para pagos).
Índices por 'idempotency _ key'; para la carga alta - charding hash.
6. 4 Ejemplo de esquema (SQL)
sql
CREATE TABLE idempo_store (
key UUID PRIMARY KEY,
req_hash BYTEA NOT NULL,
status INT NOT NULL,
response JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expiry_at TIMESTAMPTZ NOT NULL
);
6. 5 Pseudocódigo del manejador
pseudo handle_write(req):
k = req. headers["Idempotency-Key"]
h = hash(req. body)
rec = idempo_store. get(k)
if rec and rec. req_hash == h:
return rec. status, rec. response, {"Idempotency-Replay": "true"}
if rec and rec. req_hash!= h:
return 409, problem("IDEMPOTENT_CONFLICT")
begin tx result = apply_business_mutation (req) # change status upsert once (idempo_store, key = k, req_hash=h, status = 201, response = result, expiry = now () + 2d)
commit
return 201, result
7) Patrones «effectively-once»
Transactional Outbox: registrar un evento comercial y enviar un mensaje desde la misma transacción de BD a través de un relayer de fondo; el consumidor es idempotente.
Inbox/Processed-table en el consumidor: guardamos 'event _ id' para ignorar las tomas.
Exactly-once en Kafka ≠ exactly-once en el negocio: incluso con EOS productor/consumer, la lógica aplicada todavía debe ser idempotente.
Transacciones compensatorias (Saga): si los pasos se retrotraen y causan efectos secundarios, devolvemos el sistema al invariante.
8) Casos privados: pagos y transacciones financieras
Strong idempotency: la clave está vinculada a la lógica de la operación (por ejemplo, 'external _ payment _ id').
Deduplicación en PSP: almacene 'merchant _ reference' → cuando se repita, PSP devolverá el resultado anterior.
Retraídas «del cliente»: permitir sólo en 'Idempotency-Key', de lo contrario el riesgo de doble cargo.
Competitividad: bloqueos «en la cuenta/instrumento/contrato» durante el tiempo de ejecución; si se repite, devuelva 409/423.
Observabilidad: métricas 'idempo _ replay _ total', 'idempo _ conflict _ total'.
9) Webhooks y llamadas externas
Firmas HMAC y ventana de tiempo; primero la inspección, luego el procesamiento.
Retratos del remitente: retroceso exponencial + jitter, 'max _ attempts' y DLQ.
Consumidor - idempotente: 'event _ id' → tabla/in-memory cache; el orden «ordenado» no está garantizado.
Códigos: 2xx = éxito, 4xx = no repetir, 5xx/timaut = repetir.
10) Colas y tareas de fondo
At-least-once es el valor predeterminado → los duplicados son inevitables.
Almacene 'task _ id '/' event _ id' y el estado de ejecución; cuando se toma es un camino corto «replay».
DLQ y mensajes de punto: contador de intentos, cuarentena, análisis manual.
Límites competitivos (semáforos) y workers idempotentes.
11) Versificación y claves «naturales»
Las llaves naturales (número de cuenta + fecha + número de documento) aumentan la resistencia a las repeticiones.
Cuando cambie de esquema/versión, incluya la clave de versión en 'Idempotency-Key' o en el hash de solicitud.
12) Encabezados HTTP y sugerencias al cliente
'Idempotency-Key', 'Idempotency-Replay', 'Retry-After', 'Prefer: wait = <sec>' (sobre operaciones largas), 'If-Match '/' ETag' (bloqueos optimistas).
409 en conflicto de clave 425/429/503 con el válido 'Retry-After'.
Para operaciones «largas», recibe el estado asíncrono ('202 Accepted' + 'Location' por recurso de estado).
13) Pruebas y escenarios de caos
Pruebas negativas: doble envío, repetición con otro cuerpo, reloj Rassynchron.
Perturbación del orden: 't2' viene antes 't1'.
Inyección de temporizadores/' RST '/' EOF ', consultas a medias (slow-POST).
El almacenamiento de información caído → el comportamiento de fail-cerrado (mejor fallo que doble cargo).
14) Métricas y alertas
`retries_total{reason}`, `retry_budget_used{route}`, `backoff_seconds_bucket`.
`idempo_replay_total`, `idempo_conflict_total`, `duplicate_detected_total`.
Proporción 409/425/429/5xx por ruta; p95/p99 «tiempo antes del éxito» con retraídas.
Alertas: burn-rate presupuesto retrae, estallido de conflictos de idempotencia, crecimiento de DLQ.
15) Antipattern
Retractar todos los errores seguidos.
La ausencia del jitter → las ondas sincrónicas de los retraídos.
Llaves de larga vida sin TTL y limpieza.
Mantener el resultado después de commitir el efecto secundario (infracción de outbox).
Los registros sin 'trace _ id '/' idempotency _ key' → no son posibles.
Retratos paralelos agresivos en operaciones de escritura.
16) Lista de comprobación prod
- Política única: que retraemos, que no; códigos y consejos al cliente.
- Backoff exponencial + full jitter; establecido 'retry _ budget'.
- Contrato 'Idempotency-Key' + almacenamiento de resultados con TTL.
- Outbox/Inbox para eventos; DLQ; límites de competitividad.
- Integración con Circuit breaker, respect 'Retry-After'.
- Métricas/alertas sobre retratos/duplicados/conflictos.
- Conjunto de pruebas de caos y emulación de fallas de red.
- Documentación para clientes: ejemplos de back-offs y estados.
17) TL; DR
Los retraídos solo son útiles junto con la idempotencia. Introduce 'Idempotency-Key' y el repositorio de resultados, aplica un retroceso exponencial con jitter y retry-budget, respeta 'Retry-After', integra con circuit breaker. Para eventos - outbox/inbox; para pagos - deduplicación y bloqueo rigurosos. Mida retraídas y conflictos, pruebe duplicados y tiempos de espera.