2
votes

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?

1

1 Answers

1
votes

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:

  1. The RunInTransaction starts. It calls the provided function.
    • Within that function, the inc() operation returns nil.
  2. The RunInTransaction receives that result, and tries to finish the commit. But for some reason, it fails. (Where it can fail.) So RunInTransaction receives a ErrConcurrentTransaction from the datastore.
  3. The RunInTransaction tries again. It calls the provided function.
    • Within that function, the inc() operation returns nil.
  4. The RunInTransaction receives that result, and tries to finish the commit. But for some reason, it fails yet again. Hey, maybe it's busy.
  5. RunInTransaction patiently tries the function one more time.
    • Within that function, the inc() operation returns nil.
  6. The RunInTransaction receives that result, and tries to finish the commit. This time, the underlying datastore allows the commit. Hurrah!
  7. RunInTransaction returns nil to its caller.

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.