¿Qué hace que un consumidor de mensajes sea idempotente, y por qué la entrega at-least-once obliga a diseñarlo así?
Los brokers reentregan. Las redes reintentan. Si procesar un mensaje dos veces cambia el resultado, tu sistema no falla por casualidad -- falla por diseño.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué hace que un consumidor de mensajes sea idempotente, y por qué la entrega at-least-once obliga a diseñarlo así?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Por qué la entrega 'at-least-once' es la opción por defecto
Todo message broker tiene que elegir un compromiso en torno a las garantías de entrega, y la respuesta honesta es que la 'entrega exactamente una vez' casi no existe en la práctica. Brokers como Kafka, RabbitMQ o SQS funcionan por defecto en modo at-least-once: un mensaje se reentrega hasta que el consumidor lo confirma, porque la alternativa -- confirmar antes de estar seguro de que el trabajo se realizó -- arriesga perder mensajes en silencio. En el momento en que aceptás la entrega at-least-once, el procesamiento duplicado deja de ser un caso extremo y pasa a ser una certeza para la que hay que diseñar.
La reentrega ocurre por razones mundanas e inevitables: un consumidor se cae después de confirmar su escritura en base de datos pero antes de enviar el ack; un problema de red pierde el ack en el camino de vuelta al broker; un consumer group se reequilibra a mitad de un lote y entrega el mismo offset de partición a otra instancia; un visibility timeout expira en un consumidor lento de SQS aunque el trabajo ya haya terminado. Ninguno de estos casos requiere un bug -- son el costo normal de operar sistemas distribuidos sobre una red no confiable.
#Qué significa realmente la idempotencia para un consumidor
Un consumidor idempotente produce el mismo estado final sin importar si un mensaje se procesa una vez o diez. Esa es una propiedad de la operación, no del formato del mensaje -- algunas operaciones son idempotentes de manera gratuita, y otras necesitan ingeniería deliberada para volverse seguras frente a la reentrega.
| Operación | ¿Idempotente por defecto? | Por qué |
|---|---|---|
UPDATE accounts SET status = 'active' WHERE id = ? | Sí | Fijar un campo a un valor constante da el mismo resultado sin importar cuántas veces se ejecute |
UPDATE accounts SET balance = balance + 10 WHERE id = ? | No | Cada ejecución suma 10 de nuevo -- diez entregas suman 100 |
INSERT INTO orders ... sin una clave única | No | Cada ejecución crea una fila nueva |
| Enviar un correo de confirmación | No | Cada ejecución envía otro correo |
DELETE FROM sessions WHERE id = ? | Sí | Borrar una fila ya borrada no hace nada |
#El patrón de tabla de deduplicación
Cuando la operación en sí no es naturalmente idempotente -- el caso más común, ya que la mayoría de la lógica de negocio interesante muta el estado en relación con lo que había antes -- se necesita un límite explícito de deduplicación. La versión más confiable de esto es una tabla que registra qué ids de mensaje ya se procesaron, protegida por una restricción de unicidad, escrita en la misma transacción de base de datos que el efecto de negocio.
import psycopg2from psycopg2 import errorsdef handle_message(conn, message):message_id = message['id']cur = conn.cursor()try:# La restricción de unicidad sobre message_id es todo el mecanismo:# una entrega duplicada falla en este insert en vez de ejecutarse dos veces.cur.execute('INSERT INTO processed_messages (message_id, processed_at) ''VALUES (%s, now())',(message_id,))except errors.UniqueViolation:conn.rollback()return # entrega duplicada: ya se manejó, no-op seguro# El efecto de negocio y la marca de deduplicación confirman como una sola unidad atómica.cur.execute('UPDATE accounts SET balance = balance + %s WHERE id = %s',(message['amount'], message['account_id']))conn.commit()cur.close()
La restricción de unicidad es lo que realmente garantiza esto -- no una verificación a nivel de aplicación tipo '¿ya vi este id?', que compite consigo misma bajo consumidores concurrentes. Dos instancias de este consumidor pueden tomar el mismo mensaje reentregado al mismo tiempo; solo una gana el insert, y la otra hace rollback limpiamente.
#Dónde ubicar el límite de idempotencia
La tabla de deduplicación es la opción más sólida, pero no la única, y elegir el límite correcto depende de qué estás protegiendo.
| Estrategia | Cómo funciona | Solidez |
|---|---|---|
| Tabla de dedup, misma transacción que la escritura | Restricción única sobre el id del mensaje; el insert falla ante un duplicado | Fuerte -- resiste caídas, consumidores concurrentes y reordenamientos |
| Idempotencia natural vía upsert | La escritura misma es un SET/UPSERT con clave en un id estable | Fuerte, y no necesita tabla adicional |
| Clave de idempotencia en la capa de API | Quien llama provee una clave; el servidor cachea la respuesta para esa clave | Buena para APIs de request/response, menos natural para consumidores async |
Caché de dedup con TTL (por ejemplo SETNX de Redis) | Marca un id de mensaje como 'visto' durante una ventana limitada | Más débil -- una reentrega después de que expira el TTL parece nueva |
🧠 Check your understanding
0/3 · 0/3 answered1. Un consumidor procesa un mensaje y actualiza la base de datos, pero se cae antes de confirmar el ack al broker. ¿Qué ocurre bajo entrega at-least-once?
2. ¿Por qué el insert de la tabla de deduplicación y la actualización de negocio deben ocurrir en la misma transacción?
3. ¿Qué operación es naturalmente idempotente sin ningún trabajo extra?