Transactions
The App Engine Datastore supports transactions . A transaction is an operation or set of operations that is atomic—either all of the operations in the transaction occur, or none of them occur. An application can perform multiple operations and calculations in a single transaction.
Contents
- Using transactions
- What can be done in a transaction
- Isolation and consistency
- Uses for transactions
- Transactional task enqueuing
Using transactions
A transaction is a set of 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. 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.
- The Datastore encounters an internal error.
In all these cases, the Datastore API returns an error.
Transactions are an optional feature of the Datastore; you're not required to use transactions to perform Datastore operations.
The
datastore.RunInTransaction
function runs the provided function in a transaction.
package counter
import (
"fmt"
"net/http"
"appengine"
"appengine/datastore"
)
func init() {
http.HandleFunc("/", handler)
}
type Counter struct {
Count int
}
func handler(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Counter", "mycounter", 0, nil)
count := new(Counter)
err := datastore.RunInTransaction(c, func(c appengine.Context) error {
// Note: this function's argument c shadows the variable c
// from the surrounding function.
err := datastore.Get(c, key, count)
if err != nil && err != datastore.ErrNoSuchEntity {
return err
}
count.Count++
_, err = datastore.Put(c, key, count)
return err
}, nil)
if err != nil {
c.Errorf("Transaction failed: %v", err)
http.Error(w, "Internal Server Error", 500)
return
}
fmt.Fprintf(w, "Current count: %d", count.Count)
}
If the function returns
nil
,
RunInTransaction
attempts to commit the transaction, returning
nil
if it succeeds. If the function returns a non-
nil
error value, any Datastore changes are not applied and
RunInTransaction
returns that same error.
If
RunInTransaction
cannot commit the transaction because of a conflict it tries again, giving up after three attempts. This means that the transaction function must be idempotent (it must have
the same result when executed multiple times).
What can be done in a transaction
The Datastore imposes restrictions on what can be done inside a single transaction.
All Datastore operations in a transaction must operate on entities in the same entity group if the transaction is a single group transaction, or on entities in a maximum of five entity groups if the transaction is a cross-group (XG) transaction . This includes querying for entities by ancestor, retrieving entities by key, updating entities, and deleting entities. Notice that each root entity belongs to a separate entity group, so a single transaction cannot create or operate on more than one root entity unless it is an XG transaction.
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, App Engine 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, App Engine 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 the Datastore Overview page.
Isolation and consistency
Outside of transactions, the Datastore's isolation level is closest to read committed. Inside of transactions, serializable isolation is enforced. 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 the Datastore at the time the transaction started. This does not include previous puts and deletes inside the transaction. Queries and gets inside a transaction are guaranteed to see a single, consistent snapshot of the 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 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 get 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
This example demonstrates one use of transactions: updating an entity with a new property value relative to its current value.
func increment(c appengine.Context, key *datastore.Key) error {
datastore.RunInTransaction(c, func(c appengine.Context) error {
count := new(Count)
if err := datastore.Get(c, key, count); err != nil {
return err
}
count.Count++
return datastore.Put(c, key, count)
}, nil)
}
This requires a transaction because the value 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
count
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. If the entity is updated during the transaction, then the transaction is retried until all steps are completed without interruption.