views:

193

answers:

2
  • Say you retrieve a set of records from the datastore (something like: select * from MyClass where reserved='false').

  • how do i ensure that another user doesn't set the reserved is still false?

I've looked in the Transaction documentation and got shocked from google's solution which is to catch the exception and retry in a loop.

Any solution that I'm missing - it's hard to believe that there's no way to have an atomic operation in this environment.

(btw - i could use 'syncronize' inside the servlet but i think it's not valid as there's no way to ensure that there's only one instance of the servlet object, isn't it? same applies to static variable solution)

Any idea on how to solve?

(here's the google solution:

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

look at:

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'

Horrible solution, isn't it?

+3  A: 

You are correct that you cannot use synchronize or a static variable.

You are incorrect that it is impossible to have an atomic action in the App Engine environment. (See what atomic means here) When you do a transaction, it is atomic - either everything happens, or nothing happens. It sounds like what you want is some kind of global locking mechanism. In the RDBMS world, that might be something like "select for update" or setting your transaction isolation level to serialized transactions. Neither one of those types of options are very scalable. Or as you would say, they are both horrible solutions :)

If you really want global locking in app engine, you can do it, but it will be ugly and seriously impair scalability. All you need to do is create some kind of CurrentUser entity, where you store the username of the current user who has a global lock. Before you let a user do anything, you would need to first check that no user is already listed as the CurrentUser, and then write that user's key into the CurrentUser entity. The check and the write would have to be in a transaction. This way, only one user will ever be "Current" and therefore have the global lock.

Peter Recore
Hopefully it's obvious to bach that this operation (grabbing the CurrentUser in a transaction) will be worse than just doing the original update operation in a transaction in the first place... if not, after reading this comment, I hope it will be :)
Jason Hall
thanks for your replies. - Where does it written that synchronize can't be used in GAE? - I'm not looking for "atomic" in the DB world sense but in programming languages sense - I actually don't really need more than 1 JVM running my application as it's a local application.
bach
using a while loop in all clients where inside that loop there's a call to the datastore will eat all my DB/CPU qouata, sin't it?
bach
No, because the loop only iterates as long as there are conflicts. App Engine in particular can handle 5 to 10 updates per second to each entity group before conflicts cause substantial overhead.
Nick Johnson
Maybe I'm missing something but what if between the select query and the update you have to call expensive operations which take time? (like choosing from all the selected elements the best fit)Then each client is in the atomic transaction area for long time and therefore it'll get a higher rate of exceptions and looping...
bach
@bach: choose your best fit outside the transaction. Inside the transaction, perhaps validate that it still a "good enough" fit and then try to acquire it. If that fails, repeat. OK, so the "best fit" at the time you did the query might not still be the "best fit" by the time you're in the transaction. If this matters for your application (for example if you're writing financial software that needs to make trades based on sub-second accuracy of market data), then I recommend you do not use App Engine.
Steve Jessop
@bach: What do you mean by "it's a local application"?
Jason Hall
@Jason: "local" = all clients are from the same geographical region
bach
@bach, you have neither control over how many JVM's are active, nor what clients each one might serve. So an assumption like "I'll only have one JVM" should not be relied on.
Peter Recore
@bach - if you need to do a "expensive" operation while you have records "locked", that is going to be bad regardless of whether you are doing it in app engine or someplace else. In either case everyone who doesn't have the lock has to wait. What would be an example of a "non horrible" solution to this problem in a non app engine environment?
Peter Recore
@Peter: bach has sort of provided an example in another question. Suppose you have some robot which is continually flooding the DB with updates, at a rate higher than the time it takes to complete the transaction. Then mutual exclusion blocks out the robot long enough to get some work done. You can argue whether you should have a robot running at that frequency in the first place, and whether a transaction should take that long, but in point of fact a blocking lock would compensate for it. As would your suggestion, or an entity meaning "don't update for X seconds from my creation time".
Steve Jessop
A: 

Do you mean like this:

   public void func(Data data2) {

        String query = "select from " + objectA.class.getName()
                + " where reserved == false";
        List<objectA> Table = (List<objectA>) pm.newQuery(
                query).execute();

        for (objectA row : Table)
        {
            Data data1 = row.getData1();
            row.setWeight(JUtils.CalcWeight(data1, data2));
        }

        Collections.sort(Table, new objectA.SortByWeight());

        int retries = 0;
        int NUM_RETRIES = 10;
        for (int i = 0; i < Table.size() ; i++)
        {   
            retries++;      
            pm.currentTransaction().begin(); //    <---- BEGIN
            ObjectA obj = pm.getObjectById(Table.get(i).class, Table.get(i).getKey());
            if (obj .getReserved() == false)   // <--- CHECK if still reserved
                obj.setReserved(true);
            else
                break;      

            try
            {
                pm.currentTransaction().commit();
                break;
            } 
            catch (JDOCanRetryException ex)
            {
                if (j == (NUM_RETRIES - 1))
                {
                    throw ex;
                }
                i--; //so we retry again on the same object
            }
        }
        }
bach
ok now i think I've nailed it!
bach