Procesamiento “exactamente una vez” y confirmación “a lo sumo una vez”

La creación de colas de mensajes y microservicios confiables en bases de datos distribuidas en la nube, como Spanner, implica desafíos únicos. En este documento, se describen los conceptos básicos y los patrones de diseño que se usan para ejecutar el procesamiento exactamente una vez y el reconocimiento como máximo una vez en las colas de Spanner.

Conceptos básicos y el dilema de la idempotencia

"Idempotencia" significa que, sin importar cuántas veces se ejecute una operación, el resultado siempre será el mismo. Cuando se crean sistemas de colas de mensajes o de entrega de trabajo, hay tres métodos de entrega principales que abordan la idempotencia:

  • At-least-once: Se garantiza que los mensajes se entregarán y procesarán. Si se producen fallas en la red, es posible que un mensaje se entregue y procese varias veces.
  • At-most-once: Los mensajes se entregan como máximo una vez. Se evita el procesamiento duplicado, pero, si se produce una falla, es posible que se pierda o descarte el mensaje.
  • Procesamiento de tipo “exactamente una vez”: Cada mensaje se procesa exactamente una vez, sin perderse ni duplicarse.

Las colas de Spanner proporcionan automáticamente la entrega al menos una vez y el acuse de recibo a lo sumo una vez. Sin embargo, puedes usar los patrones de diseño y los ejemplos que se describen en las siguientes secciones para reducir de forma programática el procesamiento y la reentrega duplicados.

El problema del estado de confirmación desconocido

En una base de datos de una sola máquina, las transacciones se completan o fallan. En una base de datos en la nube distribuida, las transacciones se pueden perder a medida que los datos se replican en varios centros de datos físicos.

Cuando tu aplicación procesa un mensaje y confirma una recepción, como DELETE FROM MessageQueue WHERE message_id = '123' ASSERT_ROWS_MODIFIED 1, la solicitud viaja a través de varios saltos de red:

[Your App] <--(gRPC)--> [Spanner API] <--(Google network)--> [Spanner Storage]

Cuando el líder de almacenamiento de Spanner recibe la confirmación, se sincroniza en todas las réplicas de almacenamiento. Después de que se alcanza el consenso y los datos se escriben en el disco, la transacción se confirma correctamente en la base de datos.

Sin embargo, es posible que se produzca una falla transitoria en la red (como un reinicio del proxy o una desconexión de la red) después de que los datos se confirmen en el disco, pero antes de que la confirmación de éxito llegue a tu aplicación. Si esto sucede, tu aplicación recibe un error de tiempo de espera de red (DEADLINE_EXCEEDED o UNAVAILABLE).

Debido a que la conexión se interrumpió fuera de banda, tu aplicación no tiene forma de saber si la conexión se interrumpió antes o después de que se completara la confirmación. Esto se conoce como el problema del "estado de confirmación desconocido".

Por qué los reintentos automáticos pueden ser peligrosos

Si tu aplicación intercepta un error de tiempo de espera de red y vuelve a intentar la misma transacción sin deduplicación, corre el riesgo de ejecutar la lógica empresarial dos veces. Por ejemplo, si la transacción implicó cargar la tarjeta de crédito de un cliente o enviar un artículo, volver a intentar una transacción exitosa (pero no confirmada) genera un cargo o envío duplicado.

Para proteger a los usuarios, las bibliotecas cliente oficiales de Google Cloud nunca reintentan automáticamente las transacciones que fallan con errores de confirmación desconocidos. Generan un error explícito que te alerta que se desconoce el resultado de la transacción, por lo que el código de la aplicación debe controlar el reintento de forma segura con patrones de idempotencia.

Estrategias prácticas para el procesamiento “exactamente una vez”

Para volver a intentar las transacciones de forma segura y reducir el procesamiento duplicado y el reenvío de mensajes, implementa uno de los siguientes patrones de diseño principales.

Assert rows modified

Si toda tu lógica empresarial (como la actualización de otras tablas) y la confirmación de recepción del mensaje de la cola se producen en una sola transacción de Spanner, puedes usar ASSERT_ROWS_MODIFIED en tu instrucción DELETE. Esta es la estrategia más simple para el procesamiento de exactamente una vez, ya que no requiere una tabla de anulación de duplicación independiente.

Si agregas ASSERT_ROWS_MODIFIED 1 a tu declaración, el reconocimiento solo se realizará correctamente si se borró exactamente un mensaje. Si se produce una falla en la red y tu aplicación vuelve a intentar la transacción, el mensaje ya no existirá en la cola. El reintento borra 0 filas y la instrucción falla con OUT_OF_RANGE.

El error OUT_OF_RANGE es permanente: la fila ya no existe, por lo que volver a ejecutar la instrucción siempre modificará 0 filas y volverá a fallar. También es a nivel de la instrucción: solo falla DELETE. La transacción permanece abierta, y las escrituras que ya realizaste en ella siguen en búfer y se confirmarán si continúas. Spanner no anula ni revierte la transacción por ti. Para obtener la protección exactamente una vez prevista, debes asegurarte de que se revierta la transacción:

  • Permite que el error se propague: Si usas una API basada en un ejecutor (como ReadWriteTransaction en Go), permite que el error OUT_OF_RANGE se propague fuera de tu función de transacción. La biblioteca cliente abandonará la transacción y la revertirá, lo que garantizará que no se confirmen escrituras de lógica empresarial.
  • No detectes y continúes: Nunca detectes el error de aserción dentro de la función de transacción y continúes. Si lo haces, Spanner confirmará tus otras instrucciones, lo que provocará la actualización duplicada exacta que este patrón pretende evitar.
  • Reversiones manuales: Si administras las transacciones de forma manual (por ejemplo, con ReadWriteStmtBasedTransaction en Go o las APIs de REST/gRPC), debes llamar de forma explícita al método de reversión correspondiente cuando falla la aserción.

Bandeja de salida transaccional

Si tu lógica empresarial no puede ocurrir dentro de una sola transacción de Spanner (como flujos de trabajo de varios pasos o actualizaciones que abarcan varios sistemas), o si el procesamiento de mensajes requiere llamar a servicios externos (como enviar un SMS o procesar un pago), no puedes vincular de forma atómica la lógica empresarial con la confirmación de la cola.

En estos casos, nunca llames a APIs externas ni ejecutes operaciones no transaccionales directamente dentro de un bloque de transacciones de Spanner. Si Spanner vuelve a intentar la transacción o la anula, es posible que tu aplicación ejecute esa lógica empresarial varias veces.

Tu aplicación debe implementar su propia estrategia de idempotencia, pero puedes usar los siguientes patrones para reducir el trabajo duplicado:

  • Operaciones rápidas (se completan dentro del plazo predeterminado de 10 segundos): Ejecuta la lógica empresarial o la llamada a la API externa cuando se recibe el mensaje. Confirma la recepción del mensaje solo después de que la operación se realice correctamente. Si la operación falla o si el trabajador falla antes de confirmar la recepción, no confirme la recepción del mensaje. El arrendamiento vence y Spanner entrega automáticamente el mensaje para que se vuelva a intentar.

  • Operaciones de larga duración (tardan más de 10 segundos): Confirma la recepción del mensaje y, en la misma transacción, vuelve a enviar un mensaje nuevo programado para su entrega en el futuro (con una demora en la entrega mayor que el tiempo de ejecución estimado). Como alternativa, extiende periódicamente el arrendamiento del mensaje con la TVF RENEWLEASE_QUEUE_NAME(). Cuando la lógica empresarial o la llamada externa se ejecutan correctamente, confirma la recepción del mensaje recién puesto en cola o extendido.

Idempotencia de sesión multiplexada

Las sesiones de Spanner se pueden multiplexar. Las sesiones multiplexadas hacen un seguimiento de los estados de las transacciones en la memoria a través de sesiones compartidas. Esto protege contra las fallas de red del cliente, ya que permite que las bibliotecas cliente se vuelvan a conectar y resuelvan los estados de confirmación desconocidos de manera más confiable.

Sin embargo, las sesiones multiplexadas no pueden eliminar los resultados de confirmación desconocidos si falla un servidor de frontend. Si el servidor específico que aloja la tabla de transacciones de tu sesión se reinicia inmediatamente después de una confirmación, se pierde el estado en la memoria y se muestra un error de resultado de transacción desconocido al restablecer la conexión. Para abordar este riesgo, implementa los patrones de idempotencia en esta página.

¿Qué sigue?