Prueba tus trabajos en segundo plano: idempotencia y reintentos
Tu trabajo en segundo plano se va a ejecutar dos veces. Si no lo probaste para ese caso, a alguien le cobran doble. No es pesimismo — es el contrato de entrega. La documentación de Sidekiq lo dice sin rodeos: "Sidekiq will execute your job at least once, not exactly once." Las colas estándar de Amazon SQS prometen lo mismo: entrega al-menos-una-vez, y "more than one copy of a message might be delivered". Los duplicados no son un bug de tu cola. Son la especificación.
Por qué toda cola re-ejecuta tu trabajo
Un sistema de colas solo puede garantizar que un trabajo no se perdió reenviándolo cada vez que no está seguro de que terminó. Y los casos de incertidumbre están en todas partes:
- Un worker falla después de hacer el trabajo pero antes de confirmar el mensaje — la cola lo vuelve a entregar.
- Un trabajo excede su visibility timeout o su plazo de confirmación — la cola asume que murió y se lo pasa a otro worker.
- Un fallo transitorio (corte de red, reinicio por deploy, límite de tasa) dispara el reintento automático del framework.
Ejecutar dos veces significa cobrar dos veces, enviar dos correos, despachar dos veces — salvo que la segunda ejecución sea inofensiva. Esa propiedad tiene nombre: idempotencia. La guía de tareas de Celery define la meta con exactitud: "the function won't cause unintended effects even if called multiple times with the same arguments".
Las dos pruebas que todo trabajo necesita
| Prueba | Qué verificas | Qué detecta |
|---|---|---|
| Idempotencia | Ejecutas el trabajo dos veces con los mismos argumentos; el estado final es idéntico a ejecutarlo una vez — un cobro, un correo, una fila | Entregas duplicadas, doble encolado por doble clic, fallos entre el trabajo y la confirmación |
| Recuperación tras reintento | Haces fallar el trabajo a mitad de camino (lanzas una excepción después del primer efecto secundario) y lo re-ejecutas; termina sin corromper el estado | Trabajo a medias, tormentas de reintentos, trabajos "veneno" |
En código, la prueba de idempotencia es casi aburrida — y ese es justamente el punto:
def test_charge_job_is_idempotent(db, payment_gateway):
charge_order(order_id=42)
charge_order(order_id=42) # duplicate delivery
assert payment_gateway.charge_count(order_id=42) == 1
assert db.order(42).status == "paid"
La prueba de reintento inyecta el fallo que la cola tarde o temprano va a inyectar por ti:
def test_charge_job_recovers_after_crash(db, payment_gateway):
with payment_gateway.fail_after_charge(): # crash between work and ack
with pytest.raises(GatewayTimeout):
charge_order(order_id=42)
charge_order(order_id=42) # the retry
assert payment_gateway.charge_count(order_id=42) == 1
Si alguna de las dos pruebas no puede pasar, el arreglo suele ser uno de tres patrones: una clave única en el efecto secundario (por ejemplo, una idempotency key enviada al proveedor de pagos), una verificación de estado antes de actuar ("omitir si ya está pagado"), o hacer el trabajo completo transaccional para que un fallo revierta a un estado limpio y re-ejecutable — la página de buenas prácticas de Sidekiq menciona ese par directamente: haz los trabajos idempotentes y transaccionales.
Trucos avanzados
- Prueba la política de reintentos, no solo el reintento. Los frameworks exponen perillas reales —
retry_onde Active Job tiene por defectowait: 3.seconds, attempts: 5con jitter, ydiscard_ondescarta un trabajo en lugar de reintentarlo. Verifica que las excepciones que esperas mapeen a la política que elegiste, para que un refactor no convierta en silencio "reintentar 5 veces" en "reintentar por siempre". - En Celery, combina idempotencia con
acks_late. Por defecto el mensaje se confirma antes de que corra la tarea; conacks_late=Truese confirma después de que retorna, así el trabajo de un worker caído se vuelve a entregar — la documentación lo recomienda específicamente para tareas idempotentes. Usaautoretry_forconretry_backoffpara errores transitorios. - No confíes a ciegas en la casilla "exactly-once". Google Cloud Pub/Sub ofrece entrega exactly-once solo como una función opcional de la suscripción, y la re-entrega igual ocurre con nacks y plazos de confirmación vencidos. Los handlers idempotentes son la capa que funciona en todas partes.
- Haz observables las ejecuciones duplicadas. Registra "already processed, skipping" como evento informativo con la clave de deduplicación. Cuando la cola re-entregue en producción — y lo hará — tendrás una métrica, no un misterio.
Ahora una entrega duplicada es comportamiento seguro, no un ticket de soporte.
Recursos
- Sidekiq Best Practices — trabajos idempotentes y transaccionales
- Guía de tareas de Celery — idempotencia,
acks_late, reintentos - Colas estándar de Amazon SQS — entrega al-menos-una-vez
- API de Active Job:
retry_on/discard_on - Google Cloud Pub/Sub — entrega exactly-once es opcional
¿Construyendo una función con IA? Yeda AI diseña, audita y entrega sistemas LLM de producción.