Transações NDB

Uma transação é uma operação ou um conjunto de operações que têm a garantia de serem atómicas, o que significa que as transações nunca são aplicadas parcialmente. 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 de inatividade de 10 segundos após 30 segundos.

Usando a API assíncrona NDB, uma aplicação pode gerir várias transações em simultâneo se forem independentes. A API síncrona oferece uma API simplificada através do 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 a transação "colidir" com outra, falha. O NDB volta a tentar automaticamente essas transações falhadas algumas vezes. A função pode ser chamada várias vezes se a transação for repetida. Existe um limite (predefinição: 3) para o número de novas tentativas; se a transação continuar a não ser bem-sucedida, o NDB gera TransactionFailedError. Pode alterar a quantidade de tentativas passando retries=N para o decorador transactional(). Uma contagem de novas tentativas de 0 significa que a transação é tentada uma vez, mas não são feitas novas tentativas se falhar. Uma contagem de novas tentativas de N significa que a transação pode ser tentada um total de N+1 vezes. Exemplo:

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

Nas transações, só são permitidas consultas de antecessores. Por predefinição, uma transação só pode funcionar com entidades no mesmo grupo de entidades (entidades cujas chaves têm o mesmo "ancestral").

Pode especificar transações entre grupos ("XG") (que permitem até 25 grupos de entidades) transmitindo xg=True:

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

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

Se a função gerar uma exceção, a transação é imediatamente anulada e o NDB volta a gerar a exceção para que o código de chamada a veja. Pode forçar a falha silenciosa de uma transação gerando a exceção ndb.Rollback (a chamada de função devolve None neste caso). Não existe um mecanismo para forçar uma nova tentativa.

Pode ter uma função que nem sempre quer executar numa transação. Em vez de decorar essa função com @ndb.transactional, transmita-a como uma função de chamada de retorno 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á a ser executado numa transação, use a função in_transaction().

Pode especificar como uma função "transacional" deve comportar-se se for invocada por código que já esteja numa transação. O decorador @ndb.non_transactional especifica que uma função não deve ser executada numa transação. Se for chamada numa transação, é executada fora da transação. O decorador @ndb.transactional e a função ndb.transaction recebem um argumento de palavra-chave propagation. Por exemplo, se uma função deve iniciar uma nova transação independente, decore-a da seguinte forma:

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

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

O comportamento das transações e o comportamento de colocação em cache da NDB podem confundir o utilizador se não souber o que está a acontecer. Se modificar uma entidade numa transação, mas ainda não tiver confirmado a transação, a cache de contexto do NDB tem o valor modificado, mas o arquivo de dados subjacente continua a ter o valor não modificado.

Colocação em fila de tarefas transacionais

Pode colocar uma tarefa na fila como parte de uma transação do Datastore, para que a tarefa só seja colocada na fila se a transação for confirmada com êxito. Se a transação não for confirmada, a tarefa não é colocada na fila. Se a transação for confirmada, a tarefa é colocada em fila. Depois de adicionada à fila, a tarefa não é executada imediatamente, pelo que não é atómica com a transação. Ainda assim, depois de adicionada à fila, a tarefa vai ser repetida até ser bem-sucedida. Isto aplica-se a qualquer tarefa colocada em fila durante uma função decorada.

As tarefas transacionais são úteis porque permitem combinar ações que não sejam do Datastore numa transação que depende do sucesso da transação (como enviar um email para confirmar uma compra). Também pode associar ações do Datastore à transação, como confirmar alterações a grupos de entidades fora da transação, se e apenas se a transação for bem-sucedida.

Uma aplicação não pode inserir mais de cinco tarefas transacionais em filas de tarefas durante uma única transação. As tarefas transacionais não podem ter nomes especificados pelo utilizador.

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