Timeouty JTA w świecie Javy EE

Autor
Damian
Terlecki
7 minut
JEE

System może działać bez zarzutu przez lata, pewnego dnia jednak, zaczynają się pojawiać timeouty JTA (Java Transaction API):

[WebLogic] Transaction Rolledback.: weblogic.transaction.internal.TimedOutException: Transaction timed out after 301 seconds
(...) No further JDBC access is allowed within this transaction....

[WebSphere] TimeoutManage I WTRN0006W: Transaction (...) has timed out after 120 seconds.

[JBoss/WildFly] javax.transaction.RollbackException: ARJUNA016102: The transaction is not active!

Chociaż jest to specyficzne dla niektórych przypadków wykorzystania, występowanie timeoutów JTA nie jest tak rzadkie i może się zdarzyć na produkcji. Szczególnie gdy ilość danych do przetworzenia drastycznie wzrośnie względem limitów określonych na samym początku budowania systmu. Na podobny problem często można natknąć się, zmieniając serwer aplikacyjny.

Zażegnanie tego problemu w Springu to zazwyczaj bułka z masłem. Na poziomie klasy/interfejsu/metody możemy to skonfigurować za pomocą adnotacji @org.springframework.transaction.annotation.Transactional(timeout = SECONDS) . Interfejs jest ten sam, niezależnie od tego, czy chcemy użyć JTA dostarczonego przez kontener, osadzonego, czy menadżera transakcji dla pojedynczego źródła danych (JDBC).

W świecie Javy EE nie jest to jednak takie proste. Odpowiednik javax.transaction.Transactional w obecnym stanie nie umożliwia ustawienia limitu czasu transakcji, taka specyfikacja nie została jeszcze przygotowana. W związku z tym domyślny limit czasu transakcji, jak i możliwość skonfigurowania go na poziomie klasy/metody pozostaje w rękach implementatorów kontenerów.

Przykład i wartości domyślne

Zobaczmy, jak to działa w środowisku JavaEE. Zdefiniujemy najprostszy Singleton Bean, który zostanie utworzony podczas uruchamiania aplikacji:

package mypackage;

import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.ejb.Startup;

@Startup
@Singleton(name = "StartupBean")
public class StartupBean {
    @PostConstruct
    public void init() {
        System.out.println("Starting the initialization");
        try {
            Thread.sleep(31_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Finished initializing");
    }
}

Pod względem czytelności, w tym beanie brakuje wiele mówiącej adnotacji @TransactionManagement(TransactionManagementType.CONTAINER). Kwestia ta może nie być zbyt intuicyjna dla osób zaczynających podróż z Javą biznesową, ale:

Jeśli ta adnotacja nie jest używana, zakłada się, że transakcjami komponentu zarządza kontener.

Ponadto na poziomie metody brakuje również adnotacji @TransactionAttribute(TransactionAttributeType.REQUIRED):

Jeśli nie dodano adnotacji TransactionAttribute, a komponent bean używa demarkacji transakcji zarządzanej przez kontener, przyjmowana jest semantyka atrybutu transakcji REQUIRED.

Podczas tworzenia nowych beanów należy pamiętać o tym sposobie działania. Ostatecznie, próbując wdrożyć tego beana na serwerze aplikacyjnym WebLogic 12.2, otrzymuję:

weblogic.ejb.container.InternalException: Transaction marked rollback or not expected transaction status: 4

i WebLogic nie jest w stanie wdrożyć aplikacji (pozostaje w stanie STATE_NEW). Dzieje się tak, ponieważ metoda @PostConstruct jest również objęta transakcją JTA, która w tym przypadku ma domyślny limit czasu 30 sekund.

Aby wyłączyć JTA, możemy użyć adnotacji @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) na poziomie metody/klasy, bądź adnotacji @TransactionManagement(TransactionManagementType.BEAN) na poziomie klasy.

Timeouty

Z drugiej strony, jeśli naprawdę mamy logikę transakcyjną w podobnej metodzie (prawdopodobnie już nie w @PostConstruct), będziemy zainteresowani konfiguracją limitu czasu transakcji, na podobnej zasadzie co w Springu. W takim przypadku musimy zapoznać się z dokumentacją konkretnej implementacji kontenera JEE.

W przypadku WebLogica limit czasu JTA skonfigurować możemy na poziomie domeny poprzez konsolę WebLogic.

Zrzut ekranu konsoli WebLogica z konfiguracją JTA na poziomie domeny

Spowoduje to dodanie kilku wpisów do pliku config.xml znajdującego się w folderze config w miejscu instalacji domeny:

<!--...-->
  </security-configuration>
  <jta>
    <timeout-seconds>31</timeout-seconds>
    <abandon-timeout-seconds>86400</abandon-timeout-seconds>
    <forget-heuristics>true</forget-heuristics>
    <before-completion-iteration-limit>10</before-completion-iteration-limit>
    <max-transactions>10000</max-transactions>
    <max-unique-name-statistics>1000</max-unique-name-statistics>
    <checkpoint-interval-seconds>300</checkpoint-interval-seconds>
    <parallel-xa-enabled>true</parallel-xa-enabled>
    <unregister-resource-grace-period>30</unregister-resource-grace-period>
    <two-phase-enabled>true</two-phase-enabled>
    <clusterwide-recovery-enabled>false</clusterwide-recovery-enabled>
    <tightly-coupled-transactions-enabled>false</tightly-coupled-transactions-enabled>
    <tlog-write-when-determiner-exists-enabled>false</tlog-write-when-determiner-exists-enabled>
  </jta>
<!--...-->

Bardziej szczegółowej konfiguracji (na poziomie beana) można dokonać za pomocą deskryptora weblogic-ejb-jar.xml umieszczonego w katalogu WEB-INF archiwum WAR lub w katalogu META-INF archiwum JAR. Konfiguracja jest dopasowywana poprzez wartość elementu ejb-name zdefiniowaną również przez wartość elementu name adnotacji beana.

<?xml version = '1.0' encoding = 'UTF-8'?>
<weblogic-ejb-jar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://www.bea.com/ns/weblogic/weblogic-ejb-jar http://www.bea.com/ns/weblogic/weblogic-ejb-jar/1.0/weblogic-ejb-jar.xsd"
                  xmlns="http://www.bea.com/ns/weblogic/weblogic-ejb-jar">
    <weblogic-enterprise-bean>
        <ejb-name>StartupBean</ejb-name>
        <transaction-descriptor>
            <trans-timeout-seconds>60</trans-timeout-seconds>
        </transaction-descriptor>
    </weblogic-enterprise-bean>
</weblogic-ejb-jar>

W przypadku dostawców innych kontenerów sytuacja może się nieco różnić. Przykładowo przewodnik instalacji Oracle Commerce Platform daje nam ogólny pogląd na JBossa, WebLogica i WebSphere. JBoss/WildFly ma dodatkową adnotację @org.jboss.ejb3.annotation.TransactionTimeout na podobieństwo do Springa. Zbieżnie do WebLogica, JBoss/Wildfly zapewnia analogiczny element konfiguracyjny <tx:trans-timeout> w deskryptorze jboss-ejb3.xml. Z kolei w kontenerze WebSphere element ten nosi nazwę <global-transaction> i jest umieszczony w deskryptorze ibm-ejb-jar-ext.xml.

Ostatecznie, aby uzyskać większą kontrolę, możemy wykorzystać BMT (Bean Managed Transaction). W takim przypadku użylibyśmy javax.transaction.TransactionManager.setTransactionTimeout(int seconds) lub javax.transaction.UserTransaction.setTransactionTimeout(int seconds) w celu ustawienia limitu czasu konkretnych transakcji.

Podsumowanie

Możemy wyciągnąć kilka kluczowych wniosków:

  • API @Transactional różni się między Javą EE i Springiem;
  • beany EJB mają domyślnie włączone CMT (Container Managed Transactions), chyba że jawnie zadeklarujemy to inaczej;
  • kontrolowanie limitu czasu CMT jest specyficzne dla implementacji, najlepiej sprawdzić dokumentację kontenera.