Todd, I think there's actually a misunderstanding here - we don't guarantee uniqueness of a key name. A String key is encoded into a datastore Key. The datastore is, at its lowest layer, a key-value store. Uniqueness is guaranteed because if you save an entity using a Key that is already used in the datastore, it will overwrite the value stored at the current Key. It's sometimes easy to conceptualize the datastore as a giant, distributed Hashtable.
That being said, yes, if you must make sure no value exists at the current key location, there most definitely exists a race condition. On Wed, Jan 20, 2010 at 8:30 AM, Todd Vierling <[email protected]> wrote: > I've read (only in snippets, and in passing reference in the GAE > documentation) that using an unencoded String key, aka "key name", > guarantees uniqueness of that user-provided field in the same way that > a Long field is unique (but generated by the backend). However, it's > been really tough figuring out how to ensure that an exception is > thrown when there's an attempt to create a key name that already > exists. There's been questions like this in the past with rather > vague answers and no explicit documentation of successful results. > > My main confusion came from the fact that doing EntityManager.find > (...) to check for a previous instance would return null when there > was no existing entity with that key. That seemed like a race > condition to me, because there was no explicitly documented "lock" on > that key name. Further, I ran into some situations where simple > interleaving of requests would succeed, again supporting a possible > race condition. > > I finally found the *exact* procedure which must be followed in order > to guarantee uniqueness on a key name. Max et al., please check my > work and make sure this is right. JPA notation is used here, but it > should translate to JDO appropriately (have not tried yet). > > ===== > > 1. Get and begin a new transaction (EntityManager.getTransaction() -> > EntityTransaction.begin()) > > 2. Call EntityManager.find(ClassName.class, "keyname"); > > 3. If find(...) did not return null, roll back the transaction > manually and stop here; optionally throw your own exception. > (Creating and persisting a new intance after this point WILL silently > overwrite the old instance.) > > 4. Create the new instance, setting the String @Id field to "keyname". > > 5. Call EntityManager.persist(...) on the new instance. (JPA only: > Do not use the implicit persist-if-new logic of EntityManager.merge > (...).) > > 6. Call EntityTransaction.commit(). If another entity of the same > name was created during the course of this transaction, an exception > will be thrown at this point. (JPA: RollbackException, tracing to > original root ConcurrentModificationException in the low-level > Datastore API.) > > ===== > > The concurrency control seems to kick in only when EntityManager.find > (...) returns null within a transaction -- and only when all such > accesses follow this pattern. It's like the null return is an > implicit transaction-level lock on that key name. That's why step 3 > exists above; creating a new instance and persisting it anyway will > just succeed. > > I did some extensive testing both in the dev server and production, > and it works as described above. Below is a sample snippet that > demonstrates the concept. Note that the object is not required to > have a @Version locking field. (My apologies for abusing Object.wait > (...) to sleep on the production servers; it was the only way to > ensure that two competing requests both saw no pre-existing instance > and attempted a persist.) > > ===== > import javax.persistence.*; > > @Entity > public class NameUniqueTest { > @Id > private String name; > > @Id > public String getName() { > return name; > } > > @SuppressWarnings("unused") > private void setName(String name) { > this.name = name; > } > > private String data; > > @Basic > public String getData() { > return data; > } > > public void setData(String data) { > this.data = data; > } > > public NameUniqueTest(String name) { > this.name = name; > } > } > ===== > Object waiter = new Object(); // this is part of testing; > don't do this :) > > String val = req.getQueryString(); > EntityManager em = emf.createEntityManager(); > EntityTransaction tx = em.getTransaction(); > > try { > synchronized (waiter) { // this is part of testing; don't > do this :) > tx.begin(); > LOG.warn(val + ": starting txn"); > > if (em.find(NameUniqueTest.class, "myname") != null) > throw new RuntimeException("instance already > exists"); > > LOG.info(val + ": instance did not exist; creating one > and sleeping 10s"); > NameUniqueTest nu = new NameUniqueTest("myname"); > nu.setData(val); > waitLock.wait(10000); // this is part of testing; > don't do this :) > > em.persist(nu); > LOG.info(val + ": persisted before commit, sleeping > 5s"); > waitLock.wait(5000); // this is part of testing; don't > do this :) > > tx.commit(); > } > } catch (RuntimeException e) { > LOG.error(val + ": caught exception", e); > } finally { > if (tx.isActive()) > tx.rollback(); > em.close(); > } > > -- > You received this message because you are subscribed to the Google Groups > "Google App Engine for Java" group. > To post to this group, send email to > [email protected]. > To unsubscribe from this group, send email to > [email protected]<google-appengine-java%[email protected]> > . > For more options, visit this group at > http://groups.google.com/group/google-appengine-java?hl=en. > > > > -- Ikai Lan Developer Programs Engineer, Google App Engine http://googleappengine.blogspot.com | http://twitter.com/app_engine -- You received this message because you are subscribed to the Google Groups "Google App Engine for Java" group. To post to this group, send email to [email protected]. To unsubscribe from this group, send email to [email protected]. For more options, visit this group at http://groups.google.com/group/google-appengine-java?hl=en.
