Una transacción entre nodos

Qué pasa por dentro cuando una transacción de TeselaDB toca documentos de rangos que viven en nodos distintos.

5 min de lectura

Una transacción de TeselaDB puede tocar documentos de varios rangos que viven en nodos distintos. Aun así se confirma de forma atómica y con aislamiento por instantánea, igual que una transacción de MongoDB. Para ver cómo, seguimos una compra concreta desde la primera escritura hasta que sus cambios quedan como versiones normales en cada rango, y después miramos qué pasa cuando otra operación se cruza con ella o cuando el nodo que la coordina se cae a mitad.

El ejemplo

rango A (nodo 1): stock      { _id: "silla-roja", disponibles: 3 }
rango B (nodo 2): pedidos    { cliente: "Ana", sku: "silla-roja", unidades: 1 }

La compra descuenta una unidad del stock y crea el pedido. El documento del stock vive en un rango cuyo líder está en el nodo 1. El pedido nuevo cae en otro rango, liderado por el nodo 2. Ninguno de los dos nodos puede confirmar la compra por su cuenta, porque cada uno solo ve la mitad.

Si todo cayera en un rango

Antes de seguir conviene ver el caso fácil. Cuando todas las claves de una transacción caen en el mismo rango, su líder la confirma con una sola escritura replicada por Raft, con las mismas comprobaciones de conflicto que el camino largo. No hay registro aparte ni versiones provisionales. Es el caso más rápido, y la razón por la que una buena clave mantiene juntos los documentos que se escriben a la vez.

El registro de la transacción

En nuestro ejemplo hay dos rangos, así que la transacción necesita un árbitro. Es el registro de transacción, un documento interno que vive en el rango que cubre su clave y que, por tanto, está replicado con Raft como cualquier otro dato. Guarda el estado (Pending, Committed o Aborted), la marca de lectura, la marca mínima de commit, la marca de commit, el último latido del coordinador y los intervalos de claves donde la transacción dejó escrituras. Mientras el registro diga Pending, nadie más ve la compra.

Escribir sin confirmar

Cada escritura de la transacción deja en su rango una versión provisional, una intent. El nodo 1 escribe la intent del stock con disponibles: 2 y el nodo 2 la del pedido. Antes de aceptarla, cada líder comprueba dos cosas. Si hay una versión confirmada más nueva que la marca de lectura de la transacción, o una intent de otra transacción que sigue viva, la escritura falla con WriteConflict y la etiqueta TransientTransactionError. Una escritura suelta, fuera de cualquier transacción, no falla en ese caso. Espera a que la transacción dueña de la intent termine, dentro de su maxTimeMS, como hace MongoDB.

Confirmar

Con las dos intents escritas, el coordinador pide el commit al registro. La marca de commit es la mayor de tres: la que propone el coordinador, la marca mínima de commit que guarda el registro y la marca más alta de sus intents, que un líder puede haber subido. Así ninguna versión queda por debajo de una lectura ya servida. La compra existe desde el momento en que ese cambio de estado se confirma en Raft. Antes de responder, el coordinador espera a que su reloj de pared llegue a la marca de commit, para que cualquier transacción que empiece después de la respuesta vea la compra.

Después de responder

Tu aplicación recibe la confirmación en cuanto el registro cambia de estado. Las intents siguen siendo provisionales en sus rangos durante un momento, porque convertirlas en versiones normales es trabajo de segundo plano. Si un lector llega antes que ese trabajo y encuentra una intent cuyo registro ya ha terminado, la resuelve él mismo en ese momento. En ningún caso la conversión retiene al cliente.

Cuando dos operaciones se cruzan

Supón que otra lectura con una marca mayor encuentra la intent del stock mientras la compra sigue Pending. No espera. Empuja la marca mínima de commit de la compra por encima de la suya. Después mira lo que le devuelve el registro. Si sigue Pending, ya empujada, o está Aborted, lee la versión anterior, disponibles: 3. Si ya está Committed con una marca menor o igual que la suya, lee el valor de la intent. La lectura nunca se queda parada esperando un commit, y la compra, si se confirma, lo hará por encima de esa lectura.

Si el coordinador se cae

El coordinador manda latidos al registro. Una transacción sin latido durante 5 segundos se considera abandonada, y cualquiera que tropiece con una de sus intents la anula con un empujón antes de seguir. Si el coordinador murió antes de crear el registro, ese empujón lo crea ya en Aborted, y cuando el coordinador vuelve e intenta crearlo recibe NoSuchTransaction con TransientTransactionError. Así una transacción nunca acaba anulada para un lector y confirmada para otro. Un recolector en cada núcleo recorre también los registros pendientes de sus rangos y anula los abandonados.

Límites

Una transacción se anula al pasar de 60 segundos por defecto. Si su estado no cabe en la parte de memoria que le toca a su coordinador, falla con TransactionTooLarge (257). Una lectura a una marca más antigua que el historial que conserva el nodo falla con SnapshotTooOld (239). Si el rango del registro se divide durante la transacción, el registro viaja con su clave y la transacción no se entera. Para verlo con código, la guía Inventario sin vender de más descuenta stock y crea un pedido en una sola transacción.

Etiquetas

  • transacciones
  • intents
  • aislamiento
Todos los artículos