HRJ
HRJ

Reputation: 17797

When is the ConcurrentModificationException thrown in GAE?

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

Answers (2)

HRJ
HRJ

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

Kiril
Kiril

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

Related Questions