Transações do NDB

Transação é uma operação ou um conjunto de operações com garantia de serem atômicas, o que significa que as transações jamais são aplicadas parcialmente. Ou todas as operações na transação são aplicadas, ou nenhuma delas é aplicada. As transações têm uma duração máxima de 60 segundos, com um tempo de expiração por inatividade de dez segundos após 30 segundos.

Usando a API assíncrona NDB, um aplicativo pode gerenciar várias transações simultaneamente caso sejam independentes. A API síncrona oferece uma API simplificada usando o decorador @ndb.transactional(). A função decorada é executada no contexto da transação.

@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)

Se "colidir" com outra, a transação falhará. O NDB repete automaticamente essas transações com falha algumas vezes. A função pode ser chamada várias vezes caso a transação seja repetida. Há um limite para o número de novas tentativas. O padrão é de três. Caso a transação continue a falhar, o NDB gerará TransactionFailedError. Para alterar a contagem de novas tentativas, transmita retries=N para o decorador transactional(). Uma contagem de 0 novas tentativas significa que a transação é tentada uma vez, mas não será repetida se falhar. Uma contagem de repetições N significa que a transação pode ser tentada em um total de N+1 vezes. Exemplo:

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

Em transações, somente consultas de ancestral são permitidas. Por padrão, uma transação só pode funcionar com entidades no mesmo grupo de entidades (entidades com chaves que tenham o mesmo "ancestral").

É possível especificar transações entre grupos (XG, na sigla em inglês), o que permite até 25 grupos de entidades. Para isso, transmita xg=True:

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

As transações entre grupos funcionam em vários grupos de entidades e se comportam como transações de grupo único, mas não falham quando o código tenta atualizar entidades em mais de um grupo de entidades.

Caso a função gere uma exceção, a transação é anulada imediatamente e o NDB regenera a exceção. Dessa maneira, o código de chamada a vê. É possível forçar que uma transação falhe discretamente gerando a exceção ndb.Rollback. Nesse caso, a chamada de função retorna None. Não há mecanismo para forçar uma nova tentativa.

Talvez haja uma função que nem sempre precisa ser executada em uma transação. Em vez de decorar essa função com @ndb.transactional, transmita-a como uma função de callback para ndb.transaction()

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

Para testar se algum código está sendo executado em uma transação, use a função in_transaction().

Especifique como uma função "transacional" precisa se comportar caso invocada pelo código já em uma transação. O decorador @ndb.non_transactional especifica que uma função não deve ser executada em uma transação. Caso chamado em uma transação, ele é executado fora da transação. O decorador @ndb.transactional e a função ndb.transaction utilizam um argumento de palavra-chave propagation. Por exemplo, caso uma função precise iniciar uma transação nova e independente, decore-a da seguinte maneira:

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

Os tipos de propagação estão listados com as outras Opções de contexto e transação

O comportamento da transação e o comportamento do armazenamento em cache do NDB podem se confundir caso você não saiba o que está acontecendo. Se você modificar uma entidade dentro de uma transação, mas ainda não tiver confirmado a transação, o cache de contexto do NDB terá o valor modificado, mas o armazenamento de dados subjacente continuará tendo o valor não modificado.

Enfileiramento de tarefa transacional

É possível enfileirar uma tarefa como parte de uma transação do Datastore, assim ela será enfileirada apenas caso a transação seja confirmada com êxito. Não sendo confirmada, a transação não será enfileirada. Em caso de confirmação, a tarefa será enfileirada. Ela não é executada imediatamente após ser enfileirada, por isso a tarefa não é atômica com a transação. Ainda assim, uma vez enfileirada, a tarefa tentará novamente até conseguir. Isso se aplica a qualquer tarefa enfileirada durante uma função decorada.

As tarefas transacionais são úteis porque permitem combinar ações que não são do Datastore com uma transação que depende do êxito da transação, como o envio de um e-mail para confirmar uma compra. É possível também vincular ações do Datastore à transação, como confirmar alterações em grupos de entidades fora dela, apenas em caso de êxito dessa transação.

Um aplicativo não consegue inserir mais de cinco tarefas transacionais nas filas de tarefas durante uma única transação. Tarefas transacionais não podem ter nomes especificados pelo usuário.

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