O componente de upload apresenta a mensagem Transfer error occurred e no log o arjuna apresenta uma mensagem de WARN informando que o tempo da transação esgotou.

Ambiente

  • JBoss EAP 6.4.20
  • RichFaces 3.3.X
  • JBoss Seam 2.2.X

Erro

Tela do componente rich:fileUpload:

Transfer error occurred

No log do servidor há mensagens parecidas com essas:

15:28:43,239 WARN  [com.arjuna.ats.arjuna] (Transaction Reaper) ARJUNA012117: TransactionReaper::check timeout for TX 0:ffffac1d2001:519735cd:5ff8a379:17d in state  RUN
15:28:43,241 WARN  [com.arjuna.ats.arjuna] (Transaction Reaper Worker 0) ARJUNA012095: Abort of action id 0:ffffac1d2001:519735cd:5ff8a379:17d invoked while multiple threads active within it.
15:28:43,241 WARN  [com.arjuna.ats.arjuna] (Transaction Reaper Worker 0) ARJUNA012108: CheckedAction::check - atomic action 0:ffffac1d2001:519735cd:5ff8a379:17d aborting with 1 threads active!
15:28:43,242 WARN  [com.arjuna.ats.arjuna] (Transaction Reaper Worker 0) ARJUNA012121: TransactionReaper::doCancellations worker Thread[Transaction Reaper Worker 0,5,main] successfully canceled TX 0:ffffac1d2001:519735cd:5ff8a379:17d


Requests no developer tools. Retorna http 200, mas pelo payload dá pra ver que redireciona para a tela de erro:

FireFox Developer Tools

Qual é o problema?

O upload demora mais que o tempo da transação. Mas o upload continua mesmo após o timeout da transação. Quando a aplicação vai usar o arquivo e precisa da transação, temos uma exceção: Caused by: javax.resource.ResourceException: IJ000459: Transaction is not active: tx=TransactionImple < ac, BasicAction: 0:ffffac1d2001:519735cd:5ff8a379:17d status: ActionStatus.ABORTED.

Solução

O upload do arquivo é tratado pela classe org.richfaces.component.FileUploadPhaselistener. O contexto do Seam e demarcação de transação são feitos pela classe org.jboss.seam.jsf.SeamPhaseListener.

É preciso definir o tempo da transação antes do FileUploadPhaselistener e SeamPhaseListener. Abaixo um exemplo de filtro (javax.servlet.Filter) que muda o tempo da transação para requests com Content-Type do tipo multipart/form-data.

Filtro ChangeTransactionTimeoutForContentTypeMultipartFormData.java

import java.io.IOException;

import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Filtro que altera o tempo da transação para requests http com Content-Type multipart/form-data.
 * <p>O tempo da transação é definido pelo atributo {@code transactionTimeoutSeconds} no web.xml.</p>
 * <p>Exemplo de uso</p>
 * <pre>{@code
 * <filter>
 *     <filter-name>ChangeTransactionTimeoutForContentTypeMultipartFormData</filter-name>
 *     <filter-class>br.gov.ce.fortaleza.sefin.controller.ChangeTransactionTimeoutForContentTypeMultipartFormData</filter-class>
 *     <init-param>
 *         <param-name>transactionTimeoutSeconds</param-name>
 *         <param-value>1200</param-value>
 *     </init-param>
 * </filter>
 * <filter-mapping>
 *     <filter-name>ChangeTransactionTimeoutForContentTypeMultipartFormData</filter-name>
 *     <url-pattern>/*</url-pattern>
 * </filter-mapping>
 * }</pre>
 */
public class ChangeTransactionTimeoutForContentTypeMultipartFormData implements Filter {

    private static final Logger log = LoggerFactory.getLogger(ChangeTransactionTimeoutForContentTypeMultipartFormData.class);

    private Integer transactionTimeoutSeconds = null;

    @Override
    public void init(FilterConfig config) {
        String timeout = config.getInitParameter("transactionTimeoutSeconds");
        if (StringUtils.isNotBlank(timeout)) {
            log.info("Tempo da transacao definido para {} segundos", timeout);
            this.transactionTimeoutSeconds = Integer.parseInt(timeout);
        }
    }

    @Override
    public void destroy() {
        log.info("destroy");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (transactionTimeoutSeconds != null && request.getContentType() != null && request.getContentType().toLowerCase().indexOf("multipart/form-data") > -1) {
            log.info("doFilter, transactionTimeoutSeconds: {}", transactionTimeoutSeconds);

            InitialContext initContext;
            try {
                initContext = new InitialContext();
            } catch (NamingException e) {
                throw new RuntimeException("Falha ao obter instancia de InitialContext", e);
            }

            try {
                UserTransaction ut = null;
                try {
                    ut = (UserTransaction) initContext.lookup("java:comp/UserTransaction");
                } catch (NameNotFoundException nnfe) {
                    try {
                        // Embedded JBoss has no java:comp/UserTransaction
                        ut = (UserTransaction) initContext.lookup("UserTransaction");
                    } catch (Exception e) {
                        throw new ServletException("Falha ao obter instancia de UserTransaction", nnfe);
                    }
                }

                ut.setTransactionTimeout(transactionTimeoutSeconds);
            } catch (NamingException e) {
                throw new RuntimeException(e);
            } catch (SystemException e) {
                log.warn("Unable to modify tx timeout due to system exception.", e);
            }
        }

        chain.doFilter(request, response); // vai para o próximo filtro
    }

}

Configuração no arquivo web.xml

<!-- ChangeTransactionTimeoutForContentTypeMultipartFormData -->
<filter>
    <filter-name>ChangeTransactionTimeoutForContentTypeMultipartFormData</filter-name>
    <filter-class>br.gov.ce.fortaleza.sefin.controller.ChangeTransactionTimeoutForContentTypeMultipartFormData</filter-class>
    <init-param>
        <param-name>transactionTimeoutSeconds</param-name>
        <param-value>1200</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>ChangeTransactionTimeoutForContentTypeMultipartFormData</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

⛑️ Se você souber de uma outra solução, melhor ou não, deixa aí nos comentários!

Outros

Classes importantes para ativar o log durante o debug

  • org.jboss.seam.web.ExceptionFilter
  • org.jboss.seam.exception.Exceptions

Tempo de transação global no JBoss (timeout)

No arquivo standalone*xml ou domain.xml, no subssystem urn:jboss:domain:transactions, definido em segundos:

<coordinator-environment enable-statistics="true" default-timeout="300"/>

Referências