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 <[email protected]> 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<[email protected]> 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 <[email protected]> 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<[email protected]> 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 [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.