Reputation: 17797
I am reading the official GAE documentation on transactions and I can't understand when a ConcurrentModificationException
is thrown.
Look at one of the examples which I am copy-pasting here:
int retries = 3;
while (true) {
Transaction txn = datastore.beginTransaction();
try {
Key boardKey = KeyFactory.createKey("MessageBoard", boardName);
Entity messageBoard = datastore.get(boardKey);
long count = (Long) messageBoard.getProperty("count");
++count;
messageBoard.setProperty("count", count);
datastore.put(messageBoard);
txn.commit();
break;
} catch (ConcurrentModificationException e) {
if (retries == 0) {
throw e;
}
// Allow retry to occur
--retries;
} finally {
if (txn.isActive()) {
txn.rollback();
}
}
}
Now, all the writes to the datastore (in this example) are wrapped under a transaction. So why would a ConcurrentModificationException
be thrown?
Does it happen when some other code which is not wrapped in a transaction updates the same entity that is being modified by the above code? If I ensure that all code that updates an Entity is always wrapped in a transaction, is it guaranteed that I won't get a ConcurrentModificationException
?
Upvotes: 6
Views: 948
Reputation: 17797
I found the answer on the GAE mailing list.
I had a misconceived notion of how transactions work in GAE. I had imagined that beginning a transaction will lock out any concurrent updates to the datastore until the transaction commits. That would have been a performance nightmare as all updates would block on this transaction and I am happy that this isn't the case.
Instead, what happens is, the first update wins, and if a collision is detected in subsequent updates, then an exception is thrown.
This surprised me at first, because it means many transactions will need a retry logic. But it seems similar to the PostgreSQL semantics for "serializable isolation" level, though in PostgreSQL you also have the option to lock individual rows and columns.
Upvotes: 1
Reputation: 40395
It seems that you're doing what they suggest you shouldn't do: http://code.google.com/appengine/docs/java/datastore/transactions.html#Uses_for_Transactions
Warning! The above sample depicts transactionally incrementing a counter only for the sake of simplicity. If your app has counters that are updated frequently, you should not increment them transactionally, or even within a single entity. A best practice for working with counters is to use a technique known as counter-sharding.
Perhaps the above warning doesn't apply, but what follows after it seems to hint at the issue you're seeing:
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 fails with a
ConcurrentModificationException
. The application can repeat the transaction to use the new data.
In other words: it seems that somebody is modifying your entity without using a transaction at the same time that you're updating the same entity with a transaction.
Note: In extremely rare cases, the transaction is fully committed even if a transaction returns a timeout or internal error exception. For this reason, it's best to make transactions idempotent whenever possible.
A fair warning: I'm not familiar with the library, but the above quotes were taken from the documentation showing sample transactions (which seems identical to what you've posted in the original question).
Upvotes: 0