Transacciones de NDB

Una transacción es una operación o un conjunto de operaciones que aseguran ser atómicas, lo cual significa que las transacciones nunca se aplicarán parcialmente. En la transacción, se aplican todas las operaciones o no se aplica ninguna. Las transacciones tienen una duración máxima de 60 segundos, con un tiempo de vencimiento por inactividad de 10 segundos una vez transcurridos 30 segundos.

Con la API asíncrona de NDB, una aplicación puede administrar varias transacciones a la vez si son independientes. La API síncrona ofrece una API simplificada mediante 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 "choca" con otra, es decir, falla, NDB recupera automáticamente las transacciones que fallaron algunas veces. Es posible que se llame a la función varias veces si la transacción se reintenta. Existe un límite (predeterminado de 3) para la cantidad de intentos de recuperación. Si la transacción aún no se realiza de forma correcta, NDB genera un TransactionFailedError. Puedes cambiar el recuento de reintentos si pasas retries=N al decorador transactional(). Un conteo de intentos de 0 significa que la transacción se intenta una vez, pero no vuelve a intentarse si falla. Un conteo de intentos de N significa que la transacción puede intentarse N+1 veces en total. Ejemplo:

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

En las transacciones, solo se permiten consultas principales. De forma predeterminada, una transacción solo puede funcionar con las entidades del mismo grupo (entidades cuyas claves tienen el mismo “principal”).

Puedes pasar xg=True para especificar las transacciones entre grupos (“XG”) (que permiten hasta veinticinco grupos de entidades):

@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 las entidades de más de un grupo de entidades.

Si la función genera una excepción, la transacción se cancela de inmediato y NDB vuelve a generar la excepción para que el código de llamada la visualice. Puedes forzar que una transacción falle de forma silenciosa si aumentas la excepción ndb.Rollback (en este caso, la llamada a función muestra None). No existe un mecanismo para forzar un reintento.

Es posible que tengas una función que no siempre desees ejecutar en una transacción. En lugar de decorar esa función con @ndb.transactional, pásala como una función de devolución de llamada 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 probar si un código se ejecuta dentro de una transacción, usa la función in_transaction().

Puedes especificar cómo se debe comportar una función "transaccional" si la invoca un código que ya existe en una transacción. El decorador @ndb.non_transactional especifica que una función no se debe ejecutar en una transacción. Si se llama en una transacción, se ejecuta fuera de ella. 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 independiente nueva, 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 detallan con las demás opciones de contexto y opciones de transacción.

El comportamiento de la transacción y el comportamiento del almacenamiento en caché de NDB se pueden combinar y confundirte si no sabes lo que está sucediendo. Si modificas una entidad dentro de una transacción, pero todavía no confirmaste la transacción, la caché del contexto de NDB tiene el valor modificado, pero el almacén de datos subyacente sigue teniendo el valor sin modificar.

Tareas transaccionales en cola

Puedes poner una tarea en cola como parte de una transacción de Datastore para que la tarea solo esté 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 en cola, la tarea no se ejecutará de inmediato; por lo tanto, la tarea no es atómica con la transacción. Sin embargo, una vez en cola, la tarea se volverá a intentar hasta que tenga éxito. Esto se aplica a cualquier tarea en cola durante una función decorada.

Las tareas transaccionales son útiles dado que te permiten combinar acciones que no son de Datastore con una transacción que depende del éxito (por ejemplo, enviar un correo electrónico para confirmar una compra). También puedes vincular las acciones de Datastore con la transacción, por ejemplo, para confirmar los cambios en los grupos de entidades fuera de la transacción, siempre y cuando la transacción tenga éxito.

Una aplicación no puede insertar más de cinco tareas transaccionales en las listas de tareas en cola 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