Here's an implementation.  You might want to add checks for read
staleness, and think about using a task
structure for the operations to make retry easier.

The unit test structure is from from 
http://code.google.com/appengine/docs/java/howto/unittesting.html

package com.climbwithyourfeet.events.dao;

import java.util.ArrayList;
import javax.jdo.Transaction;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import java.util.logging.Logger;
import com.google.appengine.tools.development.LocalDatastoreTestCase;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class TwoOperationPseudoTransactionTest extends
LocalDatastoreTestCase {

    private Logger log = Logger.getLogger(this.getClass().getName());

    private UserGameCredits userGameCredits = new UserGameCredits();

    private UserAccount userAccount = new UserAccount();

    public TwoOperationPseudoTransactionTest() {
        super();
    }

    @Before
    public void setUp() throws Exception {
        super.setUp();
    }

    @After
    public void tearDown() throws Exception {
        super.tearDown();
    }

    public class UserGameCredits {
        public boolean add(int credits) {
            return true;
        }
    }

    public class UserAccount {
        public double getBalance() {
            return 123456789.00;
        }
        public boolean add(int credits) {
            return true;
        }
        public boolean subtractAmount(double balance, double amount,
long timestamp) {
            return true;
        }
    }

    private void handleRetry(UserGameCredits userGameCredits,
UserAccount userAccount) {
    }

    @Test
    public void testCredits() throws Exception {

        /*
         * Goal:
         *     Update 2 entities which reside in 2 different entity
groups.
         *     The updates must both pass or both fail.
         *
         * Example:
         *     The updates are 2 operations wrapped in a try/catch/
finally block.
         *     The entities are UserGameCredits and UserAccount and
are in diff entity groups.
         *     One transaction, the current transaction, is available
for the application,
         *        so only one transaction-wrapped-operation can be
rolled back in the
         *        finally clause if needed.
         *
         *     GameCredits update has higher priority as the user may
need to see
         *        it immediately. The payment processing may take
longer - so UserAccount consistency
         *        can have slightly less priority.
         */

        PersistenceManagerFactory pmfInstance =
JDOHelper.getPersistenceManagerFactory("transactions-optional");

        PersistenceManager pm = null;
        Transaction tx = null;

        final List<Boolean> completedOp1 = new ArrayList<Boolean>();
        final List<Boolean> completedOp2 = new ArrayList<Boolean>();

        int credits = 10;
        final double cost = 1;
        final double accountBalance = userAccount.getBalance();
        final long timestamp = System.currentTimeMillis();

        try {

            // change to simulate pass or fail
            boolean testShouldPass = false;

            pm = pmfInstance.getPersistenceManager();

            tx = pm.currentTransaction();
            tx.setIsolationLevel("read-committed");
            tx.begin();

            tx.setSynchronization(new
javax.transaction.Synchronization() {
                public void beforeCompletion() {
                    // before commit or rollback
                    log.info("before transaction");
                }
                public void afterCompletion(int status) {
                    if (status ==
javax.transaction.Status.STATUS_ROLLEDBACK) {
                        // rollback
                        log.severe("rollback transaction:
userGameCredits failed to update.  submitting retry");
                        handleRetry(userGameCredits, userAccount);
                    } else if (status ==
javax.transaction.Status.STATUS_COMMITTED) {
                        // commit
                        log.info("commit: userGameCredits are
updated");
                        completedOp1.add(Boolean.TRUE);
                        // TODO: possibly replace this w/ start in a
task
                        // The update task should have logic to assert
state before applying operation
                        boolean done =
userAccount.subtractAmount(accountBalance, cost, timestamp);
                        completedOp2.add(done);
                    }
                }

            });

            log.info("updating user game credits");
            //pm.refresh(userAccount);
            userGameCredits.add(10);

            pm.flush();

            if (testShouldPass) {
                log.info("committing");
                tx.commit();
            } else {
                log.info("rollback");
                tx.rollback();
            }


        } finally {
            if ((tx != null) && tx.isActive()){
                tx.rollback();
                log.info("both operations failed.  submitting retry");
                handleRetry(userGameCredits, userAccount);
            } else {
                if (!completedOp1.isEmpty() &&
completedOp1.get(0).booleanValue()) {
                    // TODO: if using task for userAccount update
check completedOp2 and use new task API to see if it is listed, else
start task here.
                    if (!completedOp2.isEmpty() &&
completedOp1.get(0).booleanValue()) {
                        log.info("both operations succeeded");
                    }
                }
            }
            if (pm != null) {
                pm.close();
            }
        }

    }

}


On Jul 11, 3:41 pm, mscwd01 <[email protected]> wrote:
> The try/catch/finally method seems better than the convoluted method
> mentioned in the blog. One question though, what is the "success flag"
> you mentioned? I'm confused as to how I can have two transactions in
> one try/catch and make sure both succeed. Maybe a quick code sample
> would help if you have the time.
>
> Thanks
>
> On Jul 11, 5:01 pm, Nichole <[email protected]> wrote:
>
>
>
>
>
>
>
> > I might add that you could add a try/catch/finally: use a transaction
> > on the first operation
> >    in the try block that can be rolled back if a succeeded flag is
> > false in the finally block;
> >    after the 2nd operation in the try block the succeeded gets set to
> > true; and if you
> >    use the referred pattern in the blog above on the 2nd operation,
> > you might want to
> >    include pre-conditions and the operation to be performed (in other
> > words, if it's a banking operation, you want to
> >    know that you expected the amount to be x before you add y).
>
> > On Jul 10, 9:15 pm, Didier Durand <[email protected]> wrote:
>
> > > Hi,
>
> > > Have a look at this to understand the 
> > > issues:http://blog.notdot.net/2009/9/Distributed-Transactions-on-App-Engine
>
> > > regards
>
> > > didier
>
> > > On Jul 10, 11:09 pm, mscwd01 <[email protected]> wrote:
>
> > > > Hey
>
> > > > I'm using JDO and need to update two entities both of which reside in
> > > > their own entity group. As I cannot use a transaction, I'd like to
> > > > determine how others achieve this. It is imperative that both entities
> > > > are updated or none at all.
>
> > > > Thanks

-- 
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