Good point, I rewrote the code below to better use the available
connections
and improve the pattern.

   Regarding update 1 being committed and update 2 failing, the
first is already committed, yes.  I think one has to use a retry for
the
2nd update (using the task queue structure) for the 2nd operation to
eventually succeed, but on a longer timescale.

Here's a better approach to the problem:

import com.climbwithyourfeet.software.twotransaction.util.PMF;
import com.google.appengine.api.datastore.Key;
import java.util.ArrayList;
import javax.jdo.Transaction;
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;

/**
   Goal:
 *     Update 2 entities which reside in 2 different entity groups.
 *     The updates must both pass or both fail.
 *
 * Solution:
 *     Use a different transaction for each entity, configurable in
        jdoconfig.xml.
 *     (Note that it looks like 2 named PersistenceManagerFactory
        connections are possible, but not more than that, and there
        is only one transaction for a  PMF connection).
 *
 *     Solution is essentially check that transaction1 succeeds or
fails before
 *     committing transaction2.
 *
 *     The case which needs additional fail-over is the case in which
       transaction 1 is committed successfully but transaction2 fails.
 *     In this case a retry of transaction2 should be invoked and must
       eventually succeed.
 *
 *     For that reason, any code using this pattern should design
       the logic so that the logic in the 2nd transaction can be
consistent
       on a longer timescale.
 *
 * @author nichole
 */
public class TwoOperationPseudoTransactionTest extends
LocalDatastoreTestCase {

    private final Logger log =
Logger.getLogger(this.getClass().getName());
    private String iden1 = "1234567";
    private String iden2 = "1123456";

    public TwoOperationPseudoTransactionTest() {
        super();
    }

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

        try {
            PersistenceManager pm = PMF.get().getPersistenceManager();
            Transaction tx = pm.currentTransaction();
            tx.begin();
            UserGameCredits e1 = new UserGameCredits(iden1);
            pm.makePersistent(e1);
            pm.flush();
            tx.commit();

            PersistenceManager pm2 =
PMF.get2().getPersistenceManager();
            tx = pm2.currentTransaction();
            tx.begin();
            UserAccount e2 = new UserAccount(iden2);
            pm2.makePersistent(e2);
            pm2.flush();
            tx.commit();
        } catch (Throwable t) {
            String msg = t.getMessage();
        }
    }

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

    @Test
    public void test2() throws Exception {

        PersistenceManager pm = PMF.get().getPersistenceManager();
        PersistenceManager pm2 = PMF.get2().getPersistenceManager();

        final Transaction tx = pm.currentTransaction();
        final Transaction tx2 = pm2.currentTransaction();

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

        try {

            // change to for tests
            final boolean commit1 = true;
            final boolean commit2 = false;

            tx.setSynchronization(new
javax.transaction.Synchronization() {
                public void beforeCompletion() {
                    log.info("before transaction 1");
                }
                public void afterCompletion(int status) {
                    switch (status) {
                        case
javax.transaction.Status.STATUS_MARKED_ROLLBACK :
                            // fall through
                        case
javax.transaction.Status.STATUS_ROLLEDBACK :
                            log.severe("rollback transaction 1");
                            break;
                        case
javax.transaction.Status.STATUS_COMMITTED:
                            log.info("committed transaction 1");
                            completedOp1.add(Boolean.TRUE);
                            break;
                        case javax.transaction.Status.STATUS_UNKNOWN:
                            // treat as rollback both?
                    }
                }
            });
            tx2.setSynchronization(new
javax.transaction.Synchronization() {
                public void beforeCompletion() {
                    log.info("before transaction 2");
                }
                public void afterCompletion(int status) {

                    switch (status) {
                        case
javax.transaction.Status.STATUS_MARKED_ROLLBACK :
                            // fall through
                        case
javax.transaction.Status.STATUS_ROLLEDBACK :
                            log.severe("rollback transaction 2");
                            if (!completedOp1.isEmpty() &&
completedOp1.get(0)) {
                                log.severe("1st transaction committed,
but 2nd did not");
                                //TODO this is the case we need to
apply application logic for
                                retry2ndOperation();
                            }
                            break;
                        case
javax.transaction.Status.STATUS_COMMITTED:
                            log.info("committed transaction 2");
                            completedOp2.add(Boolean.TRUE);
                            break;
                        case javax.transaction.Status.STATUS_UNKNOWN:
                            // treat as rollback both?
                    }
                }
            });

            tx.begin();

            Key key1 = UserGameCredits.createKey(iden1);
            UserGameCredits e1 =
pm.getObjectById(UserGameCredits.class, key1);
            e1.setVariable("updated");

            pm.flush();

            if (commit1) {
                tx.commit();
            }

            if (!completedOp1.isEmpty() && completedOp1.get(0)) {

                tx2.begin();

                Key key2 = UserAccount.createKey(iden2);
                UserAccount e2 = pm.getObjectById(UserAccount.class,
key2);
                e2.setVariable("updated");

                pm2.flush();

                // test structure that shouldn't be in place for real
code!
                // (the retry is in the sync code above)
                if (commit2) {
                    tx2.commit();
                } else {
                    log.severe("1st transaction committed, but 2nd did
not");
                    //TODO this is the case we need to apply
application logic for
                    retry2ndOperation();
                }
            }

        } catch (Throwable t) {

            String msg = t.getMessage();

        } finally {

            boolean rollback =
                 ((tx != null) && tx.isActive()) ||
                 ((tx2 != null) && tx2.isActive());

            if (rollback) {
                if (tx.isActive())
                    tx.rollback();
                if (tx2.isActive())
                    tx2.rollback();
                log.info("both  failed");
            } else {
                log.info("both  succeeded");
            }

            if (pm != null) {
                pm.close();
            }
            if (pm2 != null) {
                pm2.close();
            }
        }

    }

    private void retry2ndOperation() {
        log.info("This has to succeed eventually using the task queue
structure");
    }

}

On Jul 17, 7:23 am, mscwd01 <mscw...@gmail.com> wrote:
> Okay, so after testing this, I have one issue.
> If the second update fails (in the afterCompletion method) there is no
> way to rollback the transaction to prevent the first update (adding 10
> to userGameCredits object) taking effect - as the transaction is no
> longer active at this point.
> Am I missing something here or is there no way to rollback the
> transaction once inside the afterCompletion method?
>
> I am returned this exception if I try to rollback the transaction.
> Would doing what it says be acceptable or is setting
> NontransactionalRead and NontransactionalWrite to true a "bad" thing
> to do?
> org.datanucleus.jdo.exceptions.TransactionNotActiveException:
> Transaction is not active. You either need to define a transaction
> around this, or run your PersistenceManagerFactory with
> 'NontransactionalRead' and 'NontransactionalWrite' set to 'true'
>
> Thanks!
>
> On Jul 13, 2:53 am,Nichole<nichole.k...@gmail.com> wrote:
>
>
>
>
>
>
>
> > Yes, you'll need to add structure to the entity update method or
> > surrounding it to persist the state of the entity (and use real
> > entities!
> > The test structure is purely to provide runnable code to demonstrate
> > one way to approach the problem).
>
> >   For the 2nd update, if you recently fetched or refreshed the entity
> > and the persistenceManager is still open, then the entity is still
> > attached and all you need to use is persistenceManager.flush() before
> > you close it.  It doesn't do harm, however, to use
> > makePersistent(entity)
> > if the entity isn't a detached transient instance and you certainly
> > do
> > want to use makePersistent(entity) if it is.
>
> >    And yes, the return value of makePersistent(entity) is a good check
> > for the last operation being successful:  the return value is
> > "the parameter instance for parameters in the transient or persistent
> > state,
> > or the corresponding persistent instance for detached parameter
> > instances"
> > :)
>
> >http://download.oracle.com/docs/cd/E13222_01/wls/docs103/kodo/jdo-jav...
>
> > On Jul 12, 7:20 am, mscwd01 <mscw...@gmail.com> wrote:
>
> > > One final question. In the afterCompletion method when the userAccount
> > > has the amount subtracted, would you call pm.makePersistent() on the
> > > userAccount object? If you don't it wouldn't persist the change made
> > > to the userAccount surely? If this is the case would you just look to
> > > see that the object returned by makePersistent is not null to ensure
> > > the update to userAccount was saved successfully?
>
> > > Thanks
>
> > > On Jul 12, 9:37 am,Nichole<nichole.k...@gmail.com> wrote:
>
> > > > 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 
> > > > fromhttp://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
>
> ...
>
> read more »

-- 
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 google-appengine-java@googlegroups.com.
To unsubscribe from this group, send email to 
google-appengine-java+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/google-appengine-java?hl=en.

Reply via email to