Google App Engine, Spring, retrying transactions

The xml transaction configuration file.

<?xml version="1.0" encoding="UTF-8"?>

<beans:beans
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xsi:schemaLocation="
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <!-- the transactionManager is declared in the                          -->
    <!-- src/main/resources/spring/jdo-<whatever>-context.xml file.         -->
    <!-- the transactional advice; what 'happens'; used by the              -->
    <!-- <aop:advisor/> bean.                                               -->
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- for read only, propagation=NOT_SUPPORTED suspends any      -->
            <!-- current transaction, if there is one. I.e., read only      -->
            <!-- queries are run without a transaction.  With this,    -->
            <!-- we can do queries on multiple unowned entities.            -->
            <tx:method
                name="find*"
                read-only="true"
                propagation="NOT_SUPPORTED"
            />

            <!-- always starts a transaction.  if any exception is thrown   -->
            <!-- the transaction is rolled back.                            -->
            <tx:method
                name="*"
                rollback-for="Throwable"
                propagation="REQUIRED"
            />
        </tx:attributes>
    </tx:advice>

    <!-- ordinarily you'd only wrap the service layer classes with -->
    <!-- transactions but I needed transactions around the dao classes -->
    <!-- for testing.  but that's when I was doing things differently -->
    <!-- and now I don't know if I still need them around the dao classes. -->
    <!-- it shouldn't hurt to leave them this way; the inner transactions -->
    <!-- (the daos) are combined with the outer ones (the services). -->
    <aop:config>
        <aop:pointcut
            id="waitlistServiceOperation"
            expression="execution(* com.objecteffects.waitlist.service.api.*.*(..))"
        />

        <aop:pointcut
            id="waitlistDaoOperation"
            expression="execution(* com.objecteffects.waitlist.db.api.dao.*.*(..))"
        />

        <aop:advisor
            advice-ref="transactionAdvice"
            pointcut-ref="waitlistServiceOperation"
            order="99"
        />

        <aop:advisor
            advice-ref="transactionAdvice"
            pointcut-ref="waitlistDaoOperation"
            order="99"
        />

        <aop:aspect id="retryAspect" ref="transactionRetryer">
            <aop:around
                method="retry"
                pointcut-ref="waitlistServiceOperation"
            />

            <aop:around
                method="retry"
                pointcut-ref="waitlistDaoOperation"
            />
        </aop:aspect>
    </aop:config>

    <beans:bean id="transactionRetryer" class="com.objecteffects.waitlist.db.transaction.TransactionRetryer">
        <beans:property
            name="order"
            value="1"
        />
    </beans:bean>
</beans:beans>

The dummy dao interface with its dummy method.

package com.objecteffects.waitlist.db.api.dao;

import javax.jdo.JDOCanRetryException;

/**
 * This interface is used for testing transactions.  It needs to be in
 * this package because the xml configuration file for transactions
 * says to proxy all classes in this package.
 *
 * @author Rusty Wright
 */
public interface IDummyDao {
    /**
     * always throws the exception.
     * 
     * @throws JDOCanRetryException
     */
    void dummy() throws JDOCanRetryException;

    /**
     * throws the exception the first time it's called and doesn't
     * throw it after that on subsequent calls.
     * 
     * @throws JDOCanRetryException
     */
    void dummyOnce() throws JDOCanRetryException;
}

The dummy dao implementation.
All it does is throw an exception.

package com.objecteffects.waitlist.db.transaction;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Repository;

import com.objecteffects.waitlist.db.api.dao.IDummyDao;

/**
 * @author Rusty Wright
 */
@Repository("dummyDao")
public class DummyDao implements IDummyDao {
    private final transient Logger log = LoggerFactory.getLogger(getClass());

    static int count = 0;

    public void dummy() {
        this.log.debug("called");

        throw (new javax.jdo.JDOCanRetryException("urk"));
    }

    public void dummyOnce() {
        if (DummyDao.count++ == 0)
            throw (new javax.jdo.JDOCanRetryException("urk"));
    }
}

The test.

package com.objecteffects.waitlist.db.transaction;

import javax.annotation.Resource;
import javax.jdo.JDOCanRetryException;

import org.junit.Test;
import org.junit.runner.RunWith;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;

import com.objecteffects.gaeutils.LocalDatastoreTestCase;
import com.objecteffects.waitlist.db.api.dao.IDummyDao;

/**
 * @author Rusty Wright
 */
@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners({
    DependencyInjectionTestExecutionListener.class
})
@ContextConfiguration(locations = {
        "classpath:spring/jdo-tx-context.xml",
        "classpath:spring/applicationContext.xml",
        "classpath:spring/jdo-gae-context.xml",
        "classpath:gae-utils.xml"
})
public class TransactionRetryerTest extends LocalDatastoreTestCase {
    private final transient Logger log = LoggerFactory.getLogger(getClass());

    @Resource(name="dummyDao")
    private IDummyDao dummyDao;

    /** */
    @Test(expected = JDOCanRetryException.class)
    public void testDummy() {
        this.log.debug("called");

        this.dummyDao.dummy();

    }

    /** */
    @Test
    public void testDummyOnce() {
        this.log.debug("called");

        this.dummyDao.dummyOnce();

    }
}

The transaction retryer.

package com.objecteffects.waitlist.db.transaction;

import java.util.ConcurrentModificationException;

import org.aspectj.lang.ProceedingJoinPoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.core.Ordered;

/**
 * @author Rusty Wright
 */
public class TransactionRetryer implements Ordered {
    private final transient Logger log = LoggerFactory.getLogger(getClass());

    private int order;
    private int maxRetries = 3;

    /**
     * @param pjp
     * @return next caller's return value
     * @throws Throwable
     */
    public Object retry(final ProceedingJoinPoint pjp) throws Throwable {
        this.log.debug("called");

        Throwable exception = new Throwable("oops");

        int retryCount = 0;

        while (retryCount++ < this.maxRetries) {
            try {
                return (pjp.proceed());
            }
            catch (final JDOCanRetryException ex) {
                exception = ex;

                // retry
            }
            catch (final JDOException ex) {
                /**
                 * to quote Google's documentation: If any action
                 * fails due to the requested entity group being in
                 * use by another process, JDO throws a
                 * JDODataStoreException or a JDOException, caused by a
                 * java.util.ConcurrentModificationException.
                 */
                if (!(ex.getCause() instanceof ConcurrentModificationException))
                    throw (ex); // fail

                exception = ex;

                // retry
            }

            this.log.debug("retryCount: {}, exception: {}",
                    Integer.valueOf(retryCount),
                    ExceptionUtils.getFullStackTrace(exception));
        }

        throw (exception);
    }

    /**
     * @param _order
     */
    public void setOrder(final int _order) {
        this.order = _order;
    }

    /**
     * @return the order
     */
    public int getOrder() {
        return (this.order);
    }

    /**
     * @param _maxRetries the maxRetries to set
     */
    public void setMaxRetries(final int _maxRetries) {
        if (_maxRetries <= 0)
            throw (new IllegalArgumentException("maxRetries must be > 0"));

        this.maxRetries = _maxRetries;
    }
}

The logs.
Hover your mouse over this and in its top right corner are some gizmos where you can view source, or copy it to the clip board; that will make it easier to read with less line wrapping.

2009-11-16 23:24:20.184 PST, DEBUG: [main] org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource.addTransactionalMethod.94: Adding transactional method [find*] with attribute [PROPAGATION_NOT_SUPPORTED,ISOLATION_DEFAULT,readOnly]
2009-11-16 23:24:20.184 PST, DEBUG: [main] org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource.addTransactionalMethod.94: Adding transactional method [*] with attribute [PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-Throwable]
2009-11-16 23:24:20.184 PST, DEBUG: [main] org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource.addTransactionalMethod.94: Adding transactional method [*] with attribute [PROPAGATION_NEVER,ISOLATION_DEFAULT]
2009-11-16 23:24:20.450 PST, DEBUG: [main] com.objecteffects.waitlist.db.transaction.TransactionRetryerTest.testDummy.44: called
2009-11-16 23:24:20.466 PST, DEBUG: [main] com.objecteffects.waitlist.db.transaction.TransactionRetryer.retry.25: called
2009-11-16 23:24:20.466 PST, DEBUG: [main] org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction.367: Creating new transaction with name [com.objecteffects.waitlist.db.api.dao.IDummyDao.dummy]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-Throwable
2009-11-16 23:24:20.481 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doBegin.310: Opened new PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@159ea8e] for JDO transaction
2009-11-16 23:24:20.481 PST, DEBUG: [main] com.objecteffects.waitlist.db.transaction.DummyDao.dummy.20: called
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback.856: Initiating transaction rollback
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doRollback.449: Rolling back JDO transaction on PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@159ea8e]
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doCleanupAfterCompletion.501: Closing JDO PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@159ea8e] after transaction
2009-11-16 23:24:20.497 PST, DEBUG: [main] com.objecteffects.waitlist.db.transaction.TransactionRetryer.retry.46: retryCount: 1, exception: javax.jdo.JDOCanRetryException: urk
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction.367: Creating new transaction with name [com.objecteffects.waitlist.db.api.dao.IDummyDao.dummy]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-Throwable
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doBegin.310: Opened new PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@170984c] for JDO transaction
2009-11-16 23:24:20.497 PST, DEBUG: [main] com.objecteffects.waitlist.db.transaction.DummyDao.dummy.20: called
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback.856: Initiating transaction rollback
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doRollback.449: Rolling back JDO transaction on PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@170984c]
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doCleanupAfterCompletion.501: Closing JDO PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@170984c] after transaction
2009-11-16 23:24:20.497 PST, DEBUG: [main] com.objecteffects.waitlist.db.transaction.TransactionRetryer.retry.46: retryCount: 2, exception: javax.jdo.JDOCanRetryException: urk
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction.367: Creating new transaction with name [com.objecteffects.waitlist.db.api.dao.IDummyDao.dummy]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-Throwable
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doBegin.310: Opened new PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@45aa2c] for JDO transaction
2009-11-16 23:24:20.497 PST, DEBUG: [main] com.objecteffects.waitlist.db.transaction.DummyDao.dummy.20: called
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback.856: Initiating transaction rollback
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doRollback.449: Rolling back JDO transaction on PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@45aa2c]
2009-11-16 23:24:20.497 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doCleanupAfterCompletion.501: Closing JDO PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@45aa2c] after transaction
2009-11-16 23:24:20.497 PST, DEBUG: [main] com.objecteffects.waitlist.db.transaction.TransactionRetryer.retry.46: retryCount: 3, exception: javax.jdo.JDOCanRetryException: urk
2009-11-16 23:24:20.513 PST, DEBUG: [main] com.objecteffects.waitlist.db.transaction.TransactionRetryerTest.testDummyOnce.56: called
2009-11-16 23:24:20.513 PST, DEBUG: [main] com.objecteffects.waitlist.db.transaction.TransactionRetryer.retry.25: called
2009-11-16 23:24:20.513 PST, DEBUG: [main] org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction.367: Creating new transaction with name [com.objecteffects.waitlist.db.api.dao.IDummyDao.dummyOnce]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-Throwable
2009-11-16 23:24:20.513 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doBegin.310: Opened new PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@1c19919] for JDO transaction
2009-11-16 23:24:20.513 PST, DEBUG: [main] org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback.856: Initiating transaction rollback
2009-11-16 23:24:20.513 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doRollback.449: Rolling back JDO transaction on PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@1c19919]
2009-11-16 23:24:20.513 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doCleanupAfterCompletion.501: Closing JDO PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@1c19919] after transaction
2009-11-16 23:24:20.513 PST, DEBUG: [main] com.objecteffects.waitlist.db.transaction.TransactionRetryer.retry.46: retryCount: 1, exception: javax.jdo.JDOCanRetryException: urk
2009-11-16 23:24:20.513 PST, DEBUG: [main] org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction.367: Creating new transaction with name [com.objecteffects.waitlist.db.api.dao.IDummyDao.dummyOnce]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-Throwable
2009-11-16 23:24:20.513 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doBegin.310: Opened new PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@413fc6] for JDO transaction
2009-11-16 23:24:20.513 PST, DEBUG: [main] org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit.765: Initiating transaction commit
2009-11-16 23:24:20.513 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doCommit.432: Committing JDO transaction on PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@413fc6]
2009-11-16 23:24:20.513 PST, DEBUG: [main] org.springframework.orm.jdo.JdoTransactionManager.doCleanupAfterCompletion.501: Closing JDO PersistenceManager [org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManager@413fc6] after transaction

The project that this is for is at wait list. It’s a work in progress.

Advertisements


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s