Blog

Friday, 5 July 2013

RichFaces push + Seam3 transactions = ARJUNA016051

In previous post I've described common reason of experiencing ARJUNA016051: thread is already associated with a transaction!. You may experience this also when using RichFaces Push component.

RichFaces Push component opens connection to RichFaces PushServlet and keeps it open for as long as the component is active (commonly as long as user stays on the page. This may also exceed transaction timeout limit and get your transaction rolled back by Transaction Reaper.
You need to disable regular TransactionServletListener and turn on a customizable listener that can exclude some paths from starting transactions. To disable standard listener add following context param to your web.xml:
<context-param>
    <param-name>org.jboss.seam.transaction.disableListener</param-name>
    <param-value>true</param-value>
</context-param>
Here is the code for customizable listener:
package pl.itcrowd.seam3.transaction;

import org.apache.commons.lang.StringUtils;
import org.jboss.seam.transaction.DefaultTransaction;
import org.jboss.seam.transaction.SeamTransaction;
import org.jboss.solder.exception.control.ExceptionToCatch;
import org.jboss.solder.logging.Logger;

import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

/**
 * Listener to begin / commit / rollback a transaction around each request.
 * <p/>
 * Configure which paths are included/excluded with context params in web.xml:
 * <p/>
 * <pre>
 * <context-param>
 *  <param-name>pl.itcrowd.seam.transaction.ConfigurableTransactionServletListener.includes</param-name>
 *  <param-value>.*\.jsf$,/photoServlet</param-value>
 * &lt/context-param>
 *  </pre>
 *
 * @author <a href="http://community.jboss.org/people/blabno">Bernard Labno</a>
 */
@WebListener
public class ConfigurableTransactionServletListener implements ServletRequestListener {

    /**
     * context-param to disable the listener.
     */
    public static final String DISABLE_LISTENER_PARAM = "pl.itcrowd.seam3.transaction.disableListener";

    private static final String EXCLUDES_KEY = "pl.itcrowd.seam3.transaction.excludes";

    private static final String INCLUDES_KEY = "pl.itcrowd.seam3.transaction.includes";

    private static final Logger LOG = Logger.getLogger(ConfigurableTransactionServletListener.class);

    @Inject
    Event<ExceptionToCatch> txException;

    @SuppressWarnings("CdiInjectionPointsInspection")
    @Inject
    @DefaultTransaction
    private SeamTransaction tx;

    @Override
    public void requestDestroyed(ServletRequestEvent sre)
    {
        final String listenerDisabledParam = sre.getServletContext().getInitParameter(DISABLE_LISTENER_PARAM);
        if (listenerDisabledParam != null && "true".equals(listenerDisabledParam.trim().toLowerCase())) {
            return;
        }
        final HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
        final boolean include = matches(request, getIncludes(sre));
        final boolean exclude = matches(request, getExcludes(sre));
        LOG.debugv("Request destroyed: {3} | include:{0}, exclude:{1}, url:{2}", include, exclude, request.getServletPath(), request.getRequestURI());
        if (include && !exclude) {
            try {
                switch (this.tx.getStatus()) {
                    case Status.STATUS_ACTIVE:
                        LOG.debugf("Committing a transaction for request %s", request.getRequestURI());
                        tx.commit();
                        break;
                    case Status.STATUS_MARKED_ROLLBACK:
                    case Status.STATUS_PREPARED:
                    case Status.STATUS_PREPARING:
                        LOG.debugf("Rolling back a transaction for request %s", request.getRequestURI());
                        tx.rollback();
                        break;
                    case Status.STATUS_COMMITTED:
                    case Status.STATUS_COMMITTING:
                    case Status.STATUS_ROLLING_BACK:
                    case Status.STATUS_UNKNOWN:
                    case Status.STATUS_ROLLEDBACK:
                    case Status.STATUS_NO_TRANSACTION:
                        break;
                }
            } catch (SystemException e) {
                LOG.warn("Error rolling back the transaction", e);
                this.txException.fire(new ExceptionToCatch(e));
            } catch (HeuristicRollbackException e) {
                LOG.warn("Error committing the transaction", e);
                this.txException.fire(new ExceptionToCatch(e));
            } catch (RollbackException e) {
                LOG.warn("Error committing the transaction", e);
                this.txException.fire(new ExceptionToCatch(e));
            } catch (HeuristicMixedException e) {
                LOG.warn("Error committing the transaction", e);
                this.txException.fire(new ExceptionToCatch(e));
            }
        }
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre)
    {
        final String listenerDisabledParam = sre.getServletContext().getInitParameter(DISABLE_LISTENER_PARAM);
        if (listenerDisabledParam != null && "true".equals(listenerDisabledParam.trim().toLowerCase())) {
            return;
        }
        final HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
        final boolean include = matches(request, getIncludes(sre));
        final boolean exclude = matches(request, getExcludes(sre));
        LOG.debugv("Request initialized: {3} | include:{0}, exclude:{1}, url:{2}", include, exclude, request.getServletPath(), request.getRequestURI());
        if (include && !exclude) {
            try {
                int status = tx.getStatus();
                if (status == Status.STATUS_MARKED_ROLLBACK || status == Status.STATUS_ROLLEDBACK) {
                    LOG.warn("Transaction was already started before the listener and is marked for rollback or rolled back from other thread,"
                        + " so doing rollback to disassociate it with current thread");
                    tx.rollback();
                } else if (status != Status.STATUS_NO_TRANSACTION) {
                    LOG.warnv("Transaction was already started before the listener. Transaction status: {0}", status);
                }
                status = tx.getStatus();
                if (status == Status.STATUS_ACTIVE) {
                    LOG.warn("Transaction was already started before the listener");
                } else {
                    LOG.debugf("Beginning transaction for request %s", request.getRequestURI());
                    this.tx.begin();
                }
            } catch (SystemException se) {
                LOG.warn("Error starting the transaction, or checking status", se);
                this.txException.fire(new ExceptionToCatch(se));
            } catch (NotSupportedException e) {
                LOG.warn("Error starting the transaction", e);
                this.txException.fire(new ExceptionToCatch(e));
            }
        }
    }

    private List<String> getExcludes(ServletRequestEvent sre)
    {
        return getPatterns(sre, EXCLUDES_KEY);
    }

    private List<String> getIncludes(ServletRequestEvent sre)
    {
        return getPatterns(sre, INCLUDES_KEY);
    }

    private List<String> getPatterns(ServletRequestEvent sre, String key)
    {
        final Object attribute = sre.getServletContext().getAttribute(key);
        final List<String> patterns;
        if (null == attribute || !(attribute instanceof List)) {
            final String initParameter = sre.getServletContext().getInitParameter(key);
            patterns = new ArrayList<String>();
            if (!StringUtils.isBlank(initParameter)) {
                final StringTokenizer tokenizer = new StringTokenizer(initParameter, ",");
                while (tokenizer.hasMoreElements()) {
                    patterns.add(tokenizer.nextToken());
                }
            }
            sre.getServletContext().setAttribute(key, patterns);
        } else {
            //noinspection unchecked
            patterns = (List<String>) attribute;
        }
        return patterns;
    }

    private boolean matches(HttpServletRequest servletRequest, List<String> patterns)
    {
        final String servletPath = servletRequest.getServletPath();
        for (String pattern : patterns) {
            if (servletPath.matches(pattern)) {
                return true;
            }
        }
        return patterns.isEmpty();
    }
}
Now exclude RichFaces push servlet from starting transaction:
<context-param>
    <param-name>pl.itcrowd.seam3.transaction.excludes</param-name>
    <param-value>/__richfaces_push</param-value>
</context-param>

2 comments:

  1. very nicely done Bernard... It was this problem that caused both Cody and I to move to Delta Spike transaction handling. had i thought of this i would have had to give much more thought as to the timing of this migration. Did you write your own or use this same method to override the Seam Catch filter? If i recall correctly that one was not asynchronous either.

    ReplyDelete
    Replies
    1. natan, it is rather request listener Ethan catch filtr.
      And yes I had to implement my own.

      Delete