Transactions

A transaction is a set of Google Cloud Datastore operations on one or more entities. Each transaction is guaranteed to be atomic, which means that transactions are never partially applied. Either all of the operations in the transaction are applied, or none of them are applied.

Using transactions

Transactions have a maximum duration of 60 seconds with a 10 second idle expiration time after 30 seconds.

An operation may fail when:

  • Too many concurrent modifications are attempted on the same entity group.
  • The transaction exceeds a resource limit.
  • Cloud Datastore encounters an internal error.

In all these cases, the Cloud Datastore API returns an error.

Transactions are an optional feature of Cloud Datastore; you're not required to use transactions to perform Cloud Datastore operations.

An application can execute a set of statements and Cloud Datastore operations in a single transaction, such that if any statement or operation raises an exception, none of the Cloud Datastore operations in the set are applied. The application defines the actions to perform in the transaction.

The following snippet shows how to perform a transaction using the Cloud Datastore API. It transfers money from one account to another.

C#

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

private void TransferFunds(Key fromKey, Key toKey, long amount)
{
    using (var transaction = _db.BeginTransaction())
    {
        var entities = transaction.Lookup(fromKey, toKey);
        entities[0]["balance"].IntegerValue -= amount;
        entities[1]["balance"].IntegerValue += amount;
        transaction.Update(entities);
        transaction.Commit();
    }
}

Go

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

type BankAccount struct {
	Balance int
}

const amount = 50
keys := []*datastore.Key{to, from}
tx, err := client.NewTransaction(ctx)
if err != nil {
	log.Fatalf("client.NewTransaction: %v", err)
}
accs := make([]BankAccount, 2)
if err := tx.GetMulti(keys, accs); err != nil {
	tx.Rollback()
	log.Fatalf("tx.GetMulti: %v", err)
}
accs[0].Balance += amount
accs[1].Balance -= amount
if _, err := tx.PutMulti(keys, accs); err != nil {
	tx.Rollback()
	log.Fatalf("tx.PutMulti: %v", err)
}
if _, err = tx.Commit(); err != nil {
	log.Fatalf("tx.Commit: %v", err)
}

Java

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

void transferFunds(Key fromKey, Key toKey, long amount) {
  Transaction txn = datastore.newTransaction();
  try {
    List<Entity> entities = txn.fetch(fromKey, toKey);
    Entity from = entities.get(0);
    Entity updatedFrom =
        Entity.newBuilder(from).set("balance", from.getLong("balance") - amount).build();
    Entity to = entities.get(1);
    Entity updatedTo = Entity.newBuilder(to).set("balance", to.getLong("balance") + amount)
        .build();
    txn.put(updatedFrom, updatedTo);
    txn.commit();
  } finally {
    if (txn.isActive()) {
      txn.rollback();
    }
  }
}

Node.js

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

function transferFunds (fromKey, toKey, amount) {
  const transaction = datastore.transaction();

  return transaction.run()
    .then(() => Promise.all([transaction.get(fromKey), transaction.get(toKey)]))
    .then((results) => {
      const accounts = results
        .map((result) => result[0]);

      accounts[0].balance -= amount;
      accounts[1].balance += amount;

      transaction.save([
        {
          key: fromKey,
          data: accounts[0]
        },
        {
          key: toKey,
          data: accounts[1]
        }
      ]);

      return transaction.commit();
    })
    .catch(() => transaction.rollback());
}

PHP

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

/**
 * Update two entities in a transaction.
 *
 * @param DatastoreClient $datastore
 * @param Key $fromKey
 * @param Key $toKey
 * @param $amount
 */
function transfer_funds(
    DatastoreClient $datastore,
    Key $fromKey,
    Key $toKey,
    $amount
) {
    $transaction = $datastore->transaction();
    // The option 'sort' is important here, otherwise the order of the result
    // might be different from the order of the keys.
    $result = $transaction->lookupBatch([$fromKey, $toKey], ['sort' => true]);
    if (count($result['found']) != 2) {
        $transaction->rollback();
    }
    $fromAccount = $result['found'][0];
    $toAccount = $result['found'][1];
    $fromAccount['balance'] -= $amount;
    $toAccount['balance'] += $amount;
    $transaction->updateBatch([$fromAccount, $toAccount]);
    $transaction->commit();
}

Python

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

def transfer_funds(client, from_key, to_key, amount):
    with client.transaction():
        from_account = client.get(from_key)
        to_account = client.get(to_key)

        from_account['balance'] -= amount
        to_account['balance'] += amount

        client.put_multi([from_account, to_account])

Ruby

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

def transfer_funds from_key, to_key, amount
  datastore.transaction do |tx|
    from = tx.find from_key
    from["balance"] -= amount
    to = tx.find to_key
    to["balance"] += amount
    tx.save from, to
  end
end

Note that in order to keep our examples more succinct we sometimes omit the rollback if the transaction fails. In production code it is important to ensure that every transaction is either explicitly committed or rolled back.

What can be done in a transaction

All Cloud Datastore operations in a transaction can operate on a maximum of twenty-five entity groups. This includes querying for entities by ancestor, retrieving entities by key, updating entities, and deleting entities.

When two or more transactions simultaneously attempt to modify entities in one or more common entity groups, only the first transaction to commit its changes can succeed; all the others will fail on commit. Because of this design, using entity groups limits the number of concurrent writes you can do on any entity in the groups. When a transaction starts, Cloud Datastore uses optimistic concurrency control by checking the last update time for the entity groups used in the transaction. Upon commiting a transaction for the entity groups, Cloud Datastore again checks the last update time for the entity groups used in the transaction. If it has changed since our initial check, an error is returned. For an explanation of entity groups, see Ancestor paths.

Isolation and consistency

Outside of transactions, Cloud Datastore's isolation level is closest to read committed. Inside of transactions, serializable isolation is enforced. This means that another transaction cannot concurrently modify the data that is read or modified by this transaction. Read the serializable isolation wiki and the Transaction Isolation article for more information on isolation levels.

In a transaction, all reads reflect the current, consistent state of Cloud Datastore at the time the transaction started. Queries and lookups inside a transaction are guaranteed to see a single, consistent snapshot of Cloud Datastore as of the beginning of the transaction. Entities and index rows in the transaction's entity group are fully updated so that queries return the complete, correct set of result entities, without the false positives or false negatives described in Transaction Isolation that can occur in queries outside of transactions.

This consistent snapshot view also extends to reads after writes inside transactions. Unlike with most databases, queries and gets inside a Cloud Datastore transaction do not see the results of previous writes inside that transaction. Specifically, if an entity is modified or deleted within a transaction, a query or lookup returns the original version of the entity as of the beginning of the transaction, or nothing if the entity did not exist then.

Uses for transactions

One use of transactions is updating an entity with a new property value relative to its current value. The transferFunds example above does that for two entities, by withdrawing money from one account and transferring it to another. The Cloud Datastore API does not automatically retry transactions, but you can add your own logic to retry them, for instance to handle conflicts when another request updates the same entity at the same time.

C#

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

        /// <summary>
        /// Retry the action when a Grpc.Core.RpcException is thrown.
        /// </summary>
        private T RetryRpc<T>(Func<T> action)
        {
            List<Grpc.Core.RpcException> exceptions = null;
            var delayMs = _retryDelayMs;
            for (int tryCount = 0; tryCount < _retryCount; ++tryCount)
            {
                try
                {
                    return action();
                }
                catch (Grpc.Core.RpcException e)
                {
                    if (exceptions == null)
                        exceptions = new List<Grpc.Core.RpcException>();
                    exceptions.Add(e);
                }
                System.Threading.Thread.Sleep(delayMs);
                delayMs *= 2;  // Exponential back-off.
            }
            throw new AggregateException(exceptions);
        }

        private void RetryRpc(Action action)
        {
            RetryRpc(() => { action(); return 0; });
        }

        [Fact]
        public void TestTransactionalRetry()
        {
            int tryCount = 0;
            var keys = UpsertBalances();
            RetryRpc(() =>
            {
                using (var transaction = _db.BeginTransaction())
                {
                    TransferFunds(keys[0], keys[1], 10, transaction);
                    // Insert a conflicting transaction on the first try.
                    if (tryCount++ == 0)
                        TransferFunds(keys[1], keys[0], 5);
                    transaction.Commit();
                }
            });
            Assert.Equal(2, tryCount);
        }

Go

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

type BankAccount struct {
	Balance int
}

const amount = 50
_, err := client.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
	keys := []*datastore.Key{to, from}
	accs := make([]BankAccount, 2)
	if err := tx.GetMulti(keys, accs); err != nil {
		return err
	}
	accs[0].Balance += amount
	accs[1].Balance -= amount
	_, err := tx.PutMulti(keys, accs)
	return err
})

Java

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

int retries = 5;
while (true) {
  try {
    transferFunds(fromKey, toKey, 10);
    break;
  } catch (DatastoreException e) {
    if (retries == 0) {
      throw e;
    }
    --retries;
  }
}
// Retry handling can also be configured and automatically applied using google-cloud-java.

Node.js

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

function transferFundsWithRetry () {
  const maxTries = 5;
  let currentAttempt = 1;
  let delay = 100;

  function tryRequest () {
    return transferFunds(fromKey, toKey, 10)
      .catch((err) => {
        if (currentAttempt <= maxTries) {
          // Use exponential backoff
          return new Promise((resolve, reject) => {
            setTimeout(() => {
              currentAttempt++;
              delay *= 2;
              tryRequest().then(resolve, reject);
            }, delay);
          });
        }
        return Promise.reject(err);
      });
  }

  return tryRequest(1, 5);
}

PHP

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

$retries = 5;
for ($i = 0; $i < $retries; $i++) {
    try {
        transfer_funds($datastore, $fromKey, $toKey, 10);
    } catch (Google\Cloud\Exception\ConflictException $e) {
        // if $i >= $retries, the failure is final
        continue;
    }
    // Succeeded!
    break;
}

Python

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

for _ in range(5):
    try:
        transfer_funds(client, account1.key, account2.key, 50)
        break
    except google.cloud.exceptions.Conflict:
        continue
else:
    print('Trasaction failed.')

Ruby

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

(1..5).each do |i|
  begin
    return transfer_funds from_key, to_key, amount
  rescue Google::Cloud::Error => e
    raise e if i == 5
  end
end

This requires a transaction because the value of balance in an entity may be updated by another user after this code fetches the object, but before it saves the modified object. Without a transaction, the user's request uses the value of balance prior to the other user's update, and the save overwrites the new value. With a transaction, the application is told about the other user's update.

Another common use for transactions is to fetch an entity with a named key, or create it if it doesn't yet exist (this example builds on the TaskList example from creating an entity):

C#

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

Entity task;
using (var transaction = _db.BeginTransaction())
{
    task = transaction.Lookup(_sampleTask.Key);
    if (task == null)
    {
        transaction.Insert(_sampleTask);
        transaction.Commit();
    }
}

Go

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

_, err := client.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
	var task Task
	if err := tx.Get(key, &task); err != datastore.ErrNoSuchEntity {
		return err
	}
	_, err := tx.Put(key, &Task{
		Category:    "Personal",
		Done:        false,
		Priority:    4,
		Description: "Learn Cloud Datastore",
	})
	return err
})

Java

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

Entity task;
Transaction txn = datastore.newTransaction();
try {
  task = txn.get(taskKey);
  if (task == null) {
    task = Entity.newBuilder(taskKey).build();
    txn.put(task);
    txn.commit();
  }
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

Node.js

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

function getOrCreate (taskKey, taskData) {
  const taskEntity = {
    key: taskKey,
    data: taskData
  };

  const transaction = datastore.transaction();

  return transaction.run()
    .then(() => transaction.get(taskKey))
    .then((results) => {
      const task = results[0];
      if (task) {
        // The task entity already exists.
        return transaction.rollback();
      } else {
        // Create the task entity.
        transaction.save(taskEntity);
        return transaction.commit();
      }
    })
    .then(() => taskEntity)
    .catch(() => transaction.rollback());
}

PHP

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

$transaction = $datastore->transaction();
$existed = $transaction->lookup($task->key());
if ($existed === null) {
    $transaction->insert($task);
    $transaction->commit();
}

Python

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

with client.transaction():
    key = client.key('Task', datetime.datetime.utcnow().isoformat())

    task = client.get(key)

    if not task:
        task = datastore.Entity(key)
        task.update({
            'description': 'Example task'
        })
        client.put(task)

    return task

Ruby

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

task = nil
datastore.transaction do |tx|
  task = tx.find task_key
  if task.nil?
    task = datastore.entity task_key do |t|
      t["category"] = "Personal"
      t["done"] = false
      t["priority"] = 4
      t["description"] = "Learn Cloud Datastore"
    end
    tx.save task
  end
end

As before, a transaction is necessary to handle the case where another user is attempting to create or update an entity with the same string ID. Without a transaction, if the entity does not exist and two users attempt to create it, the second overwrites the first without knowing that it happened.

When a transaction fails, you can have your app retry the transaction until it succeeds, or you can let your users deal with the error by propagating it to your app's user interface level. You do not have to create a retry loop around every transaction.

Finally, you can use a transaction to read a consistent snapshot of Cloud Datastore. This can be useful when multiple reads are needed to render a page or export data that must be consistent. These kinds of transactions are often called read-only transactions, since they perform no writes. Read-only single-group transactions never fail due to concurrent modifications, so you don't have to implement retries upon failure. However, multi-entity-group transactions can fail due to concurrent modifications, so these should have retries.

C#

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

Entity taskList;
IReadOnlyList<Entity> tasks;
using (var transaction = _db.BeginTransaction())
{
    taskList = transaction.Lookup(taskListKey);
    var query = new Query("Task")
    {
        Filter = Filter.HasAncestor(taskListKey)
    };
    tasks = transaction.RunQuery(query).Entities;
    transaction.Commit();
}

Go

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

tx, err := client.NewTransaction(ctx)
if err != nil {
	log.Fatalf("client.NewTransaction: %v", err)
}
defer tx.Rollback() // Transaction only used for read.

ancestor := datastore.NameKey("TaskList", "default", nil)
query := datastore.NewQuery("Task").Ancestor(ancestor).Transaction(tx)
var tasks []Task
_, err = client.GetAll(ctx, query, &tasks)

Java

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

Entity taskList;
QueryResults<Entity> tasks;
Transaction txn = datastore.newTransaction();
try {
  taskList = txn.get(taskListKey);
  Query<Entity> query = Query.newEntityQueryBuilder()
      .setKind("Task")
      .setFilter(PropertyFilter.hasAncestor(taskListKey))
      .build();
  tasks = txn.run(query);
  txn.commit();
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

Node.js

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

function getTaskListEntities () {
  let taskList, taskListEntities;

  const transaction = datastore.transaction();
  const taskListKey = datastore.key(['TaskList', 'default']);

  return transaction.run()
    .then(() => datastore.get(taskListKey))
    .then((results) => {
      taskList = results[0];
      const query = datastore.createQuery('Task')
        .hasAncestor(taskListKey);
      return datastore.runQuery(query);
    })
    .then((results) => {
      taskListEntities = results[0];
      return transaction.commit();
    })
    .then(() => [taskList, taskListEntities])
    .catch(() => transaction.rollback());
}

PHP

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

$transaction = $datastore->transaction();
$taskListKey = $datastore->key('TaskList', 'default');
$query = $datastore->query()
    ->kind('Task')
    ->hasAncestor($taskListKey);
$result = $transaction->runQuery($query);
$taskListEntities = [];
/* @var Entity $task */
foreach ($result as $task) {
    $taskListEntities[] = $task;
}
$transaction->commit();

Python

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

with client.transaction():
    task_list_key = client.key('TaskList', 'default')

    task_list = client.get(task_list_key)

    query = client.query(kind='Task', ancestor=task_list_key)
    tasks_in_list = list(query.fetch())

    return task_list, tasks_in_list

Ruby

For more on installing and creating a Cloud Datastore client, refer to Cloud Datastore Client Libraries.

task_list_key = datastore.key "TaskList", "default"
datastore.transaction do |tx|
  task_list = tx.find task_list_key
  query = tx.query("Task").ancestor(task_list)
  tasks_in_list = tx.run query
end

Transactions and entity groups

An entity group is a set of entities connected through ancestry to a common root element. The organization of data into entity groups can limit what transactions can be performed:

  • All the data accessed by a transaction must be contained in at most 25 entity groups.
  • If you want to use queries within a transaction, your data must be organized into entity groups in such a way that you can specify ancestor filters that will match the right data.
  • There is a write throughput limit of about one transaction per second within a single entity group. This limitation exists because Cloud Datastore performs masterless, synchronous replication of each entity group over a wide geographic area to provide high reliability and fault tolerance.

In many applications, it is acceptable to use eventual consistency (i.e. a non-ancestor query spanning multiple entity groups, which may at times return slightly stale data) when obtaining a broad view of unrelated data, and then to use strong consistency (an ancestor query, or a lookup of a single entity) when viewing or editing a single set of highly related data. In such applications, it is usually a good approach to use a separate entity group for each set of highly related data. For more information, see Data Consistency.

What's next

Send feedback about...

Cloud Datastore Documentation