English

Google App Engine

NDB Transactions

Experimental!

NDB is an experimental, innovative, and rapidly changing new feature for App Engine. Unfortunately, being on the bleeding edge means that we may make backwards-incompatible changes to NDB. We will inform the community when this feature is no longer experimental.

A transaction is an operation or set of operations that either succeeds completely or fails completely. An application can perform multiple operations and calculations in a single transaction. Using the NDB asynchronous API, an application can manage multiple transactions simultaneously if they are independent. The synchronous API offers a simplified API using the ndb.transaction() function.

The transaction() function takes a callback function that is executed in the context of the transaction.

This simple example acts as a transactional get-or-insert operation:

key = ndb.Key(Greeting, 'joe')

def callback():
  # 'key' here uses the key variable in the outer scope;
  # the callback function is a closure.
  ent = key.get()
  if ent is None:
    ent = Greeting(key=key, message='Hey Joe')
    ent.put()
  return ent

ent = ndb.transaction(callback)

If the transaction "collides" with another, it fails; NDB automatically retries such failed transactions a few times. Thus, the callback function may be called multiple times if the transaction is retried. There is a limit (default 3) to the number of retries attempted; if the transaction still does not succeed, the transaction() call raises TransactionFailedError. You can change the retry count by passing retries=N to the transaction() call. A retry count of 0 means the transaction is attempted once but not retried if it fails; a retry count of N means that the transaction may be attempted a total of N+1 times. Example:

ent = ndb.transaction(callback, retries=1) # Total of 2 tries

By default, a transaction can only work with entities in the same entity group (entities whose keys have the same "ancestor").

In transactions, only ancestor queries are allowed.

You can specify cross-group ("XG") transactions (which allow up to five entity groups), by passing xg=True:

ent = ndb.transaction(callback, xg=True)

If the transaction's callback function raises an exception, the transaction is immediately aborted and the transaction() call re-raises the exception. You can force a transaction to fail silently by raising the ndb.Rollback exception (the transaction() call returns None in this case). There is no mechanism to force a retry.

Often you have a function that must be run inside a transaction. The @transactional decorator automatically runs the decorated function inside a transaction, with the added twist that if a transaction is already in effect, it doesn't start a new transaction but just calls the function. For example:

@ndb.transactional
def get_or_insert(keyname):
  key = ndb.Key(Greeting, keyname)
  ent = key.get()
  if ent is None:
    ent = Greeting(key=key, message='Hey Rodrigo')
    ent.put()
  return ent

moraes = get_or_insert('rodrigo')

The @transactional decorator does not support passing a retry count or other options.

To test whether some code is running inside a transaction, use the in_transaction() function:

def maybe_run_in_transaction(func):
  if ndb.in_transaction():
    return func()
  else:
    return transaction(func)