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.
