2
votes

The datastore documentation says that:

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, App Engine throws an exception

I have three test cases about performing a transacted datastore put of a new Entity("TestEntity",1) in two concurrent transactions.

  • test1: put the entity in the first transaction, put the entity in the second transaction, and commit both transactions. This test passes (i.e. throws ConcurrentModificationException) when run in developement server and standalone unit tests, but fails (i.e. executes without throwing an exception) when run in production server.

  • test2: put the entity in the first transaction and commit it, then put the entity in the second transaction and commit it. This test always fails.

  • test3: attempt to get the (non-existing) entity in both transactions, then do test2. This test always passes by throwing ConcurrentModificationException.

From these tests I conclude that neither beginTransaction nor put guarantee that "the initial check" is performed, and I need to pay a get in order to guarantee the integrity of the transaction. Is that correct?

@Test(expected=ConcurrentModificationException.class)
//put1 put2 commit1 commit2
public void test1() {
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
    Transaction txn1 = ds.beginTransaction();
    Transaction txn2 = ds.beginTransaction();
    ds.put(txn1,new Entity("TestEntity",1));
    ds.put(txn2,new Entity("TestEntity",1));
    txn1.commit();
    txn2.commit();
}

@Test(expected=ConcurrentModificationException.class)
//put1 commit1 put2 commit2
public void test2()  {
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
    Transaction txn1 = ds.beginTransaction();
    Transaction txn2 = ds.beginTransaction();
    ds.put(txn1,new Entity("TestEntity",1));
    txn1.commit();
    ds.put(txn2,new Entity("TestEntity",1)); 
    txn2.commit(); 
}

@Test(expected=ConcurrentModificationException.class)
//get1 get2 put1 commit1 put2 commit2
public void test3() throws InterruptedException, ExecutionException {
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
    ds.delete(KeyFactory.createKey("TestEntity", 1));
    Transaction txn1 = ds.beginTransaction();
    Transaction txn2 = ds.beginTransaction();
    Assert.assertNull(getTestEntity(ds, txn1));
    Assert.assertNull(getTestEntity(ds, txn2));
    ds.put(txn1,new Entity("TestEntity",1));
    txn1.commit();
    ds.put(txn2,new Entity("TestEntity",1));
    txn2.commit();
}
1
For a minute I thought this question was going to be about a write only databases.... I've got a really performant one I was going to sell you :) - Gus

1 Answers

4
votes

You're correct in a sense. (MVCC) Consistency doesn't require a timestamp until the first event where the transaction observes a snapshot of mutable data.

Think about it: how could a transaction that mutated a single record without any reads violate consistency? Since it issued no reads, it essentially makes no assumptions. Therefore there are no assumptions that could be broken to result in a write-write conflict.

The bottom line is you're getting the safety benefits of consistency, no matter what. Enforcing that consistency simply doesn't require a timestamp until you issue the first transactional read.