Transacciones de NDB

Una transacción es una operación o un conjunto de operaciones que tienen la garantía de ser atómicas, lo que significa que las transacciones nunca se aplican parcialmente. Se aplican todas las operaciones de la transacción o no se aplica ninguna. Las transacciones tienen una duración máxima de 60 segundos con un tiempo de caducidad por inactividad de 10 segundos después de 30 segundos.

Con la API asíncrona de NDB, una aplicación puede gestionar varias transacciones simultáneamente si son independientes. La API síncrona ofrece una API simplificada que usa el decorador @ndb.transactional(). La función decorada se ejecuta en el contexto de la transacción.

@ndb.transactional
def insert_if_absent(note_key, note):
    fetch = note_key.get()
    if fetch is None:
        note.put()
        return True
    return False
note_key = ndb.Key(Note, note_title, parent=parent)
note = Note(key=note_key, content=note_text)
inserted = insert_if_absent(note_key, note)

Si la transacción "colisiona" con otra, falla. NDB vuelve a intentar automáticamente esas transacciones fallidas varias veces. La función se puede llamar varias veces si se vuelve a intentar la transacción. Hay un límite (3 de forma predeterminada) en el número de reintentos. Si la transacción sigue sin completarse, NDB genera TransactionFailedError. Puedes cambiar el número de reintentos pasando retries=N al decorador transactional(). Si el número de reintentos es 0, significa que la transacción se intenta una vez, pero no se vuelve a intentar si falla. Si el número de reintentos es N, significa que la transacción se puede intentar un total de N+1 veces. Ejemplo:

@ndb.transactional(retries=1)
def insert_if_absent_2_retries(note_key, note):
    # do insert

En las transacciones, solo se permiten las consultas de ancestros. De forma predeterminada, una transacción solo puede funcionar con entidades del mismo grupo de entidades (entidades cuyas claves tienen el mismo "ancestor").

Puede especificar transacciones entre grupos ("XG"), que permiten hasta 25 grupos de entidades, si transfiere xg=True:

@ndb.transactional(xg=True)
def insert_if_absent_xg(note_key, note):
    # do insert

Las transacciones entre grupos operan en varios grupos de entidades y se comportan como transacciones de un solo grupo, pero no fallan si el código intenta actualizar entidades de más de un grupo de entidades.

Si la función genera una excepción, la transacción se aborta inmediatamente y NDB vuelve a generar la excepción para que el código de llamada la vea. Puedes forzar que una transacción falle de forma silenciosa generando la excepción ndb.Rollback (en este caso, la llamada a la función devuelve None). No hay ningún mecanismo para forzar un reintento.

Puede que tengas una función que no quieras ejecutar siempre en una transacción. En lugar de decorar una función de este tipo con @ndb.transactional, pásala como función de retrollamada a ndb.transaction().

def insert_if_absent_sometimes(note_key, note):
    # do insert
inserted = ndb.transaction(lambda:
                           insert_if_absent_sometimes(note_key, note))

Para comprobar si un código se está ejecutando dentro de una transacción, usa la función in_transaction().

Puedes especificar cómo debe comportarse una función "transaccional" si se invoca mediante código que ya está en una transacción. El decorador @ndb.non_transactional especifica que una función no debe ejecutarse en una transacción. Si se llama en una transacción, se ejecuta fuera de la transacción. El decorador @ndb.transactional y la función ndb.transaction toman un argumento de palabra clave propagation. Por ejemplo, si una función debe iniciar una transacción nueva e independiente, decórala de la siguiente manera:

@ndb.transactional(propagation=ndb.TransactionOptions.INDEPENDENT)
def insert_if_absent_indep(note_key, note):
    # do insert

Los tipos de propagación se muestran junto con las demás opciones de contexto y opciones de transacción.

El comportamiento de las transacciones y el comportamiento de la caché de NDB pueden combinarse para confundirte si no sabes qué está pasando. Si modificas una entidad dentro de una transacción, pero aún no has confirmado la transacción, la caché de contexto de NDB tendrá el valor modificado, pero el almacén de datos subyacente seguirá teniendo el valor sin modificar.

Puesta en cola de tareas transaccionales

Puedes poner en cola una tarea como parte de una transacción de Datastore, de modo que la tarea solo se ponga en cola si la transacción se confirma correctamente. Si la transacción no se confirma, la tarea no se pone en cola. Si la transacción se confirma, la tarea se pone en cola. Una vez puesta en cola, la tarea no se ejecutará inmediatamente, por lo que no será atómica con la transacción. Aun así, una vez puesta en cola, la tarea se volverá a intentar hasta que se complete correctamente. Esto se aplica a cualquier tarea puesta en cola durante una función decorada.

Las tareas transaccionales son útiles porque te permiten combinar acciones que no son de Datastore en una transacción que depende de que la transacción se realice correctamente (por ejemplo, enviar un correo para confirmar una compra). También puedes vincular acciones de Datastore a la transacción, como confirmar los cambios en los grupos de entidades fuera de la transacción solo si esta se completa correctamente.

Una aplicación no puede insertar más de cinco tareas transaccionales en colas de tareas durante una sola transacción. Las tareas transaccionales no deben tener nombres especificados por el usuario.

from google.appengine.api import taskqueue
from google.appengine.ext import ndb
@ndb.transactional
def insert_if_absent_taskq(note_key, note):
    taskqueue.add(url=flask.url_for('taskq_worker'), transactional=True)
    # do insert