I am writing a Google App Engine Golang app. In the Datastore Transaction documentation, there is a note:
Note: If your app receives an error when submitting a transaction, it does not always mean that the transaction failed. You can receive ErrConcurrentTransaction in cases where transactions have been committed and eventually will be applied successfully. Whenever possible, make your Datastore transactions idempotent so that if you repeat a transaction, the end result will be the same.
Which makes me believe that if a transaction returns ErrConcurrentTransaction, it means the Datastore will eventually complete the transaction. However, reading up on RunInTransaction we can see a note:
If f returns nil, RunInTransaction attempts to commit the transaction, returning nil if it succeeds. If the commit fails due to a conflicting transaction, RunInTransaction retries f, each time with a new transaction context. It gives up and returns ErrConcurrentTransaction after three failed attempts.
It looks like ErrConcurrentTransaction is a fail state for the RunInTransaction function which means the Transaction will never commit.
So, which is it? If RunInTransaction returns ErrConcurrentTransaction, what should my code assume? Did the Transaction succeed, will it succeed in the future, or did it fail?
Concrete scenario. Consider the following snippet:
err := datastore.RunInTransaction(c, func(c appengine.Context) error {
var err1 error
count, err1 = inc(c, datastore.NewKey(c, "Counter", "singleton", 0, nil))
return err1
}, nil)
// Here, if err is anything other than nil, the datastore-specific
// operations didn't commit to the datastore.
Here's one possible scenario when we run this snippet:
So in this scenario, your application observes ErrConcurrentTransactions. The first note you read is a general comment about the system as a whole: as a whole, your program may encounter ErrConcurrentTransactions. But that doesn't mean that the code you write will touch ErrConcurrentTransaction directly. Your code may not see this error at all. Yet RunInTransaction is running on behalf of your code, and RunInTransaction might see that error. But the transaction can still go forward because RunInTransaction will replay the function until it either succeeds, or the datastore is busy enough that it gives up.
If you get nil as the final return value from RunInTransaction, the datastore operations went through. But if you get non-nil, they didn't.
Note that in the scenario above, the function called by RunInTransaction is called multiple times as part of the retry protocol. So you've got to make sure that's ok in the functions you pass to RunInTransaction, because it will try using retry when the datastore is busy.