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.

Reply via email to