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.