0
votes

I have deployed an Stateful EJB 3 on Wildfly that participates in a JTA transaction. This EJB is an extension User provider for Keycloak.

One of the EJB operations is invoked at some point and could produce a RuntimeException. I need the global transaction to commit so I added a try-catch block in the ejb method.

@Stateful
@Local(EjbUserStorageProvider.class)
public class EjbUserStorageProvider implements UserStorageProvider,
        UserLookupProvider,
        UserRegistrationProvider,
        UserQueryProvider,
        CredentialInputUpdater,
        CredentialInputValidator,
        OnUserCache
{

    ...

    @Override
    public int getUsersCount(RealmModel realm) {
        try {
            Object count = em.createNamedQuery("getUserCount").getSingleResult();
            return ((Number)count).intValue();
        } catch(RuntimeException e) {
            e.printStackTrace();
            return 0;
        }
    }
}

The problem is that, despite capturing the exception, the transaction is rolledback.

This is the problematic bit:

org.keycloak.models.utils.KeycloakModelUtils

    public static void runJobInTransaction(KeycloakSessionFactory factory, KeycloakSessionTask task) {
        KeycloakSession session = factory.create();
        KeycloakTransaction tx = session.getTransactionManager();
        try {
            tx.begin();
            task.run(session);

            if (tx.isActive()) {
                if (tx.getRollbackOnly()) {
                    tx.rollback();
                } else {
                    tx.commit();
                }
            }
        } catch (RuntimeException re) {
            if (tx.isActive()) {
                tx.rollback();
            }
            throw re;
        } finally {
            session.close();
        }
    }

For some reason tx.getRollbackOnly() returns true so the JTA transaction is rolledback.

How could I prevent the transaction from being marked as rollback only?

UPDATE

I tried using @Transactional but was ignored:

    @Transactional(value = Transactional.TxType.NEVER, dontRollbackOn = {RuntimeException.class, PersistenceException.class})
    @Override
    public int getUsersCount(RealmModel realm) {

I also used @TransactionAttribute(NOT_SUPPORTED) but in this case Keycloak can't start:

09:36:30,784 ERROR [org.jboss.as.ejb3.invocation] (ServerService Thread Pool -- 51) WFLYEJB0034: EJB Invocation failed on component EjbUserStorageProvider for method public int es.mma.edm.keycloak.storage.user.EjbUserStorageProvider.getUsersCount(org.keycloak.models.RealmModel): javax.ejb.ConcurrentAccessTimeoutException: WFLYEJB0228: EJB 3.1 FR 4.3.14.1 concurrent access timeout on EjbUserStorageProvider - could not obtain lock within 5000 MILLISECONDS
    at [email protected]//org.jboss.as.ejb3.component.stateful.StatefulSessionSynchronizationInterceptor.processInvocation(StatefulSessionSynchronizationInterceptor.java:94)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.as.ee.concurrent.ConcurrentContextInterceptor.processInvocation(ConcurrentContextInterceptor.java:45)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.invocation.InitialInterceptor.processInvocation(InitialInterceptor.java:40)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.invocation.ChainedInterceptor.processInvocation(ChainedInterceptor.java:53)
    at [email protected]//org.jboss.as.ee.component.interceptors.ComponentDispatcherInterceptor.processInvocation(ComponentDispatcherInterceptor.java:52)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.as.ejb3.component.stateful.StatefulComponentInstanceInterceptor.processInvocation(StatefulComponentInstanceInterceptor.java:59)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.as.ejb3.component.interceptors.AdditionalSetupInterceptor.processInvocation(AdditionalSetupInterceptor.java:54)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.as.ejb3.tx.CMTTxInterceptor.invokeInNoTx(CMTTxInterceptor.java:216)
    at [email protected]//org.jboss.as.ejb3.tx.CMTTxInterceptor.notSupported(CMTTxInterceptor.java:337)
    at [email protected]//org.jboss.as.ejb3.tx.CMTTxInterceptor.processInvocation(CMTTxInterceptor.java:142)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.as.ejb3.component.interceptors.CurrentInvocationContextInterceptor.processInvocation(CurrentInvocationContextInterceptor.java:41)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.as.ejb3.component.invocationmetrics.WaitTimeInterceptor.processInvocation(WaitTimeInterceptor.java:47)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.as.ejb3.security.SecurityContextInterceptor.processInvocation(SecurityContextInterceptor.java:100)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.as.ejb3.deployment.processors.StartupAwaitInterceptor.processInvocation(StartupAwaitInterceptor.java:22)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.as.ejb3.component.interceptors.ShutDownInterceptorFactory$1.processInvocation(ShutDownInterceptorFactory.java:64)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.as.ejb3.component.interceptors.LoggingInterceptor.processInvocation(LoggingInterceptor.java:67)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.as.ee.component.NamespaceContextInterceptor.processInvocation(NamespaceContextInterceptor.java:50)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.invocation.ContextClassLoaderInterceptor.processInvocation(ContextClassLoaderInterceptor.java:60)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.invocation.InterceptorContext.run(InterceptorContext.java:438)
    at [email protected]//org.wildfly.security.manager.WildFlySecurityManager.doChecked(WildFlySecurityManager.java:619)
    at [email protected]//org.jboss.invocation.AccessCheckingInterceptor.processInvocation(AccessCheckingInterceptor.java:57)
    at [email protected]//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at [email protected]//org.jboss.invocation.ChainedInterceptor.processInvocation(ChainedInterceptor.java:53)
    at [email protected]//org.jboss.as.ee.component.ViewService$View.invoke(ViewService.java:198)
    at [email protected]//org.jboss.as.ee.component.ViewDescription$1.processInvocation(ViewDescription.java:185)
    at [email protected]//org.jboss.as.ee.component.ProxyInvocationHandler.invoke(ProxyInvocationHandler.java:81)
    at deployment.edm-keycloak-user-storage.jar//es.mma.edm.keycloak.storage.user.EjbUserStorageProvider$$$view1.getUsersCount(Unknown Source)
    at [email protected]//org.keycloak.storage.UserStorageManager.getUsersCount(UserStorageManager.java:453)
    at [email protected]//org.keycloak.models.cache.infinispan.UserCacheSession.getUsersCount(UserCacheSession.java:543)
    at [email protected]//org.keycloak.models.cache.infinispan.UserCacheSession.getUsersCount(UserCacheSession.java:548)
    at [email protected]//org.keycloak.services.managers.ApplianceBootstrap.isNoMasterUser(ApplianceBootstrap.java:55)
    at [email protected]//org.keycloak.services.resources.KeycloakApplication$2.run(KeycloakApplication.java:168)
    at org.keycloak.keycloak-server-spi-private@4.8.3.Final-redhat-00001//org.keycloak.models.utils.KeycloakModelUtils.runJobInTransaction(KeycloakModelUtils.java:227)
    at [email protected]//org.keycloak.services.resources.KeycloakApplication.<init>(KeycloakApplication.java:164)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
    at [email protected]//org.jboss.resteasy.core.ConstructorInjectorImpl.construct(ConstructorInjectorImpl.java:154)
    at [email protected]//org.jboss.resteasy.spi.ResteasyProviderFactory.createProviderInstance(ResteasyProviderFactory.java:2757)
    at [email protected]//org.jboss.resteasy.spi.ResteasyDeployment.createApplication(ResteasyDeployment.java:363)
    at [email protected]//org.jboss.resteasy.spi.ResteasyDeployment.startInternal(ResteasyDeployment.java:276)
    at [email protected]//org.jboss.resteasy.spi.ResteasyDeployment.start(ResteasyDeployment.java:88)
    at [email protected]//org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.init(ServletContainerDispatcher.java:119)
    at [email protected]//org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.init(HttpServletDispatcher.java:36)
    at [email protected]//io.undertow.servlet.core.LifecyleInterceptorInvocation.proceed(LifecyleInterceptorInvocation.java:117)
    at [email protected]//org.wildfly.extension.undertow.security.RunAsLifecycleInterceptor.init(RunAsLifecycleInterceptor.java:78)
    at [email protected]//io.undertow.servlet.core.LifecyleInterceptorInvocation.proceed(LifecyleInterceptorInvocation.java:103)
    at [email protected]//io.undertow.servlet.core.ManagedServlet$DefaultInstanceStrategy.start(ManagedServlet.java:303)
    at [email protected]//io.undertow.servlet.core.ManagedServlet.createServlet(ManagedServlet.java:143)
    at [email protected]//io.undertow.servlet.core.DeploymentManagerImpl$2.call(DeploymentManagerImpl.java:583)
    at [email protected]//io.undertow.servlet.core.DeploymentManagerImpl$2.call(DeploymentManagerImpl.java:554)
    at [email protected]//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:42)
    at [email protected]//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
    at [email protected]//org.wildfly.extension.undertow.security.SecurityContextThreadSetupAction.lambda$create$0(SecurityContextThreadSetupAction.java:105)
    at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at [email protected]//io.undertow.servlet.core.DeploymentManagerImpl.start(DeploymentManagerImpl.java:596)
    at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentService.startContext(UndertowDeploymentService.java:97)
    at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentService$1.run(UndertowDeploymentService.java:78)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at [email protected]//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
    at [email protected]//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1985)
    at [email protected]//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1487)
    at [email protected]//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1378)
    at java.base/java.lang.Thread.run(Thread.java:834)
    at [email protected]//org.jboss.threads.JBossThread.run(JBossThread.java:485)
1
It seems that task.run(...) eventually executes the code shown here. Could there be some other piece of code higher up the stack that calls tx.setRollbackOnly()? Can you check what is the result of tx.getRollbackOnly() right after the statement that throws the exception (e.g. print it in the catch block)? Finally (and perhaps unrelated), is it appropriate to catch RuntimeException at that point and not a more specific exception? Why is a count query throwing? - Nikos Paraskevopoulos
If getUsersCount is just getting data from the database (read operation), why not suspend the transaction when it gets invoked by changing the TransactionAttribute to not-supported? - Warren Nocos
Unfortunately Keycloak needs a transaction for that call - codependent

1 Answers

0
votes

This is the expected behaviour, required by the JPA spec (section 3.1.1 page 79)

Runtime exceptions thrown by the methods of the EntityManager interface other than the LockTimeoutException will cause the current transaction to be marked for rollback if the persistence context is joined to that transaction.

To workaround this you need to declare the method to be executed in a new transaction, using REQUIRES_NEW as TransactionAttribute