Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Google Appengine sharded counters without transactions

I'm going through the Sharded Counters example in Java: http://code.google.com/appengine/articles/sharding_counters.html

I have a question about the implementation of the increment method. In python it explicitly wraps the get() and increment in a transaction. In the Java example it just retrieves it and sets it. I'm not sure I fully understand the Datastore and transactions but it seems like the critical update section should be wrapped in a datastore transaction. Am I missing something?

Original code:

  public void increment() {
    PersistenceManager pm = PMF.get().getPersistenceManager();

    Random generator = new Random();
    int shardNum = generator.nextInt(NUM_SHARDS);

    try {
      Query shardQuery = pm.newQuery(SimpleCounterShard.class);
      shardQuery.setFilter("shardNumber == numParam");
      shardQuery.declareParameters("int numParam");

      List<SimpleCounterShard> shards =
          (List<SimpleCounterShard>) shardQuery.execute(shardNum);
      SimpleCounterShard shard;

      // If the shard with the passed shard number exists, increment its count
      // by 1. Otherwise, create a new shard object, set its count to 1, and
      // persist it.
      if (shards != null && !shards.isEmpty()) {
        shard = shards.get(0);
        shard.setCount(shard.getCount() + 1);
      } else {
        shard = new SimpleCounterShard();
        shard.setShardNumber(shardNum);
        shard.setCount(1);
      }

      pm.makePersistent(shard);
    } finally {
      pm.close();
    }
  }
}

Transactional code (I believe you need to run this in a transaction to gurantee correctness under concurrent transactions?) :

public void increment() { 
    PersistenceManager pm = PMF.get().getPersistenceManager(); 
    Random generator = new Random(); 
    int shardNum = generator.nextInt(NUM_SHARDS); 
    try { 
      Query shardQuery = pm.newQuery(SimpleCounterShard.class); 
      shardQuery.setFilter("shardNumber == numParam"); 
      shardQuery.declareParameters("int numParam"); 
      List<SimpleCounterShard> shards = 
          (List<SimpleCounterShard>) shardQuery.execute(shardNum); 
      SimpleCounterShard shard; 
      // If the shard with the passed shard number exists, increment its count 
      // by 1. Otherwise, create a new shard object, set its count to 1, and 
      // persist it. 
      if (shards != null && !shards.isEmpty()) { 
            Transaction tx = pm.currentTransaction(); 
        try { 
            tx.begin(); 
            //I believe in a transaction objects need to be loaded by ID (can't use the outside queried entity) 
             Key shardKey = KeyFactory.Builder(SimpleCounterShard.class.getSimpleName(), shards.get(0).getID()) 
            shard =  pm.getObjectById(SimpleCounterShard.class, shardKey); 
            shard.setCount(shard.getCount() + 1); 
            tx.commit(); 
        } finally { 
            if (tx.isActive()) { 
                tx.rollback(); 
            } 
        } 
      } else { 
        shard = new SimpleCounterShard(); 
        shard.setShardNumber(shardNum); 
        shard.setCount(1); 
      } 
      pm.makePersistent(shard); 
    } finally { 
      pm.close(); 
    } 
  } 
like image 236
Dougnukem Avatar asked Sep 23 '10 20:09

Dougnukem


1 Answers

This section straight out of the docs shows that you are exactly right about needing a transaction:

http://code.google.com/appengine/docs/java/datastore/transactions.html#Uses_For_Transactions

This example demonstrates one use of transactions: updating an entity with a new property value relative to its current value.

    Key k = KeyFactory.createKey("Employee", "k12345");
    Employee e = pm.getObjectById(Employee.class, k);
    e.counter += 1;
    pm.makePersistent(e);

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 will use the value of counter prior to the other user's update, and the save will overwrite 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 an exception. The application can repeat the transaction to use the new data.

Its very close to what that sharded example is doing and, like you, I was unable to find any reason why sharded counters would be different.

like image 105
Bert F Avatar answered Oct 15 '22 13:10

Bert F