diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml
index 5373067447..94c80a2a96 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml
@@ -25,6 +25,7 @@
+
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi/main/module.xml
index 95f34b6943..8b5632e30a 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi/main/module.xml
@@ -33,5 +33,6 @@
+
diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml
index f6a8c4bae1..257c617af0 100755
--- a/federation/ldap/pom.xml
+++ b/federation/ldap/pom.xml
@@ -74,6 +74,11 @@
junit
test
+
+ org.jboss.spec.javax.transaction
+ jboss-transaction-api_1.2_spec
+ provided
+
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProvider.java
index db85a825c9..e35e5b0734 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProvider.java
@@ -17,6 +17,8 @@
package org.keycloak.connections.jpa;
+import org.jboss.logging.Logger;
+
import javax.persistence.EntityManager;
/**
@@ -24,6 +26,7 @@ import javax.persistence.EntityManager;
*/
public class DefaultJpaConnectionProvider implements JpaConnectionProvider {
+ private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProvider.class);
private final EntityManager em;
public DefaultJpaConnectionProvider(EntityManager em) {
@@ -37,6 +40,7 @@ public class DefaultJpaConnectionProvider implements JpaConnectionProvider {
@Override
public void close() {
+ logger.trace("DefaultJpaConnectionProvider close()");
em.close();
}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
index 3c74264f8d..d029e16767 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
@@ -29,12 +29,22 @@ import java.util.Map;
import javax.naming.InitialContext;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
+import javax.persistence.SynchronizationType;
import javax.sql.DataSource;
+import javax.transaction.InvalidTransactionException;
+import javax.transaction.Synchronization;
+import javax.transaction.SystemException;
+import javax.transaction.Transaction;
+import javax.transaction.TransactionManager;
+import javax.transaction.UserTransaction;
import org.hibernate.ejb.AvailableSettings;
+import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform;
+import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
+import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -45,6 +55,7 @@ import org.keycloak.provider.ServerInfoAwareProviderFactory;
import org.keycloak.models.dblock.DBLockManager;
import org.keycloak.ServerStartupError;
import org.keycloak.timer.TimerProvider;
+import org.keycloak.transaction.JtaTransactionManagerLookup;
/**
* @author Stian Thorgersen
@@ -60,16 +71,29 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
private volatile EntityManagerFactory emf;
private Config.Scope config;
-
- private Map operationalInfo;
+
+ private Map operationalInfo;
+
+ private boolean jtaEnabled;
+ private JtaTransactionManagerLookup jtaLookup;
+
+ private KeycloakSessionFactory factory;
@Override
public JpaConnectionProvider create(KeycloakSession session) {
+ logger.trace("Create JpaConnectionProvider");
lazyInit(session);
- EntityManager em = emf.createEntityManager();
+ EntityManager em = null;
+ if (!jtaEnabled) {
+ logger.trace("enlisting EntityManager in JpaKeycloakTransaction");
+ em = emf.createEntityManager();
+ } else {
+
+ em = emf.createEntityManager(SynchronizationType.SYNCHRONIZED);
+ }
em = PersistenceExceptionConverter.create(em);
- session.getTransactionManager().enlist(new JpaKeycloakTransaction(em));
+ if (!jtaEnabled) session.getTransactionManager().enlist(new JpaKeycloakTransaction(em));
return new DefaultJpaConnectionProvider(em);
}
@@ -92,85 +116,112 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
@Override
public void postInit(KeycloakSessionFactory factory) {
+ this.factory = factory;
+ checkJtaEnabled(factory);
}
+ protected void checkJtaEnabled(KeycloakSessionFactory factory) {
+ jtaLookup = (JtaTransactionManagerLookup) factory.getProviderFactory(JtaTransactionManagerLookup.class);
+ if (jtaLookup != null) {
+ if (jtaLookup.getTransactionManager() != null) {
+ jtaEnabled = true;
+ }
+ }
+ }
+
private void lazyInit(KeycloakSession session) {
if (emf == null) {
synchronized (this) {
if (emf == null) {
- logger.debug("Initializing JPA connections");
+ KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
+ logger.debug("Initializing JPA connections");
- Map properties = new HashMap();
+ Map properties = new HashMap();
- String unitName = "keycloak-default";
+ String unitName = "keycloak-default";
- String dataSource = config.get("dataSource");
- if (dataSource != null) {
- if (config.getBoolean("jta", false)) {
- properties.put(AvailableSettings.JTA_DATASOURCE, dataSource);
+ String dataSource = config.get("dataSource");
+ if (dataSource != null) {
+ if (config.getBoolean("jta", jtaEnabled)) {
+ properties.put(AvailableSettings.JTA_DATASOURCE, dataSource);
+ } else {
+ properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource);
+ }
} else {
- properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource);
- }
- } else {
- properties.put(AvailableSettings.JDBC_URL, config.get("url"));
- properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver"));
+ properties.put(AvailableSettings.JDBC_URL, config.get("url"));
+ properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver"));
- String user = config.get("user");
- if (user != null) {
- properties.put(AvailableSettings.JDBC_USER, user);
- }
- String password = config.get("password");
- if (password != null) {
- properties.put(AvailableSettings.JDBC_PASSWORD, password);
- }
- }
-
- String schema = getSchema();
- if (schema != null) {
- properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema);
- }
-
- MigrationStrategy migrationStrategy = getMigrationStrategy();
- boolean initializeEmpty = config.getBoolean("initializeEmpty", true);
- File databaseUpdateFile = getDatabaseUpdateFile();
-
- properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
- properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
-
- Connection connection = getConnection();
- try{
- prepareOperationalInfo(connection);
-
- String driverDialect = detectDialect(connection);
- if (driverDialect != null) {
- properties.put("hibernate.dialect", driverDialect);
+ String user = config.get("user");
+ if (user != null) {
+ properties.put(AvailableSettings.JDBC_USER, user);
+ }
+ String password = config.get("password");
+ if (password != null) {
+ properties.put(AvailableSettings.JDBC_PASSWORD, password);
+ }
}
- migration(migrationStrategy, initializeEmpty, schema, databaseUpdateFile, connection, session);
-
- int globalStatsInterval = config.getInt("globalStatsInterval", -1);
- if (globalStatsInterval != -1) {
- properties.put("hibernate.generate_statistics", true);
+ String schema = getSchema();
+ if (schema != null) {
+ properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema);
}
- logger.trace("Creating EntityManagerFactory");
- emf = JpaUtils.createEntityManagerFactory(session, unitName, properties, getClass().getClassLoader());
- logger.trace("EntityManagerFactory created");
+ MigrationStrategy migrationStrategy = getMigrationStrategy();
+ boolean initializeEmpty = config.getBoolean("initializeEmpty", true);
+ File databaseUpdateFile = getDatabaseUpdateFile();
- if (globalStatsInterval != -1) {
- startGlobalStats(session, globalStatsInterval);
+ properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
+ properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
+
+ Connection connection = getConnection();
+ try {
+ prepareOperationalInfo(connection);
+
+ String driverDialect = detectDialect(connection);
+ if (driverDialect != null) {
+ properties.put("hibernate.dialect", driverDialect);
+ }
+
+ migration(migrationStrategy, initializeEmpty, schema, databaseUpdateFile, connection, session);
+
+ int globalStatsInterval = config.getInt("globalStatsInterval", -1);
+ if (globalStatsInterval != -1) {
+ properties.put("hibernate.generate_statistics", true);
+ }
+
+ logger.trace("Creating EntityManagerFactory");
+ logger.tracev("***** create EMF jtaEnabled {0} ", jtaEnabled);
+ if (jtaEnabled) {
+ properties.put(org.hibernate.cfg.AvailableSettings.JTA_PLATFORM, new AbstractJtaPlatform() {
+ @Override
+ protected TransactionManager locateTransactionManager() {
+ return jtaLookup.getTransactionManager();
+ }
+
+ @Override
+ protected UserTransaction locateUserTransaction() {
+ return null;
+ }
+ });
+ }
+ emf = JpaUtils.createEntityManagerFactory(session, unitName, properties, getClass().getClassLoader(), jtaEnabled);
+ logger.trace("EntityManagerFactory created");
+
+ if (globalStatsInterval != -1) {
+ startGlobalStats(session, globalStatsInterval);
+ }
+ } finally {
+ // Close after creating EntityManagerFactory to prevent in-mem databases from closing
+ if (connection != null) {
+ try {
+ connection.close();
+ } catch (SQLException e) {
+ logger.warn("Can't close connection", e);
+ }
+ }
}
- } finally {
- // Close after creating EntityManagerFactory to prevent in-mem databases from closing
- if (connection != null) {
- try {
- connection.close();
- } catch (SQLException e) {
- logger.warn("Can't close connection", e);
- }
- }
- }
+ });
}
}
}
@@ -182,19 +233,19 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
}
protected void prepareOperationalInfo(Connection connection) {
- try {
- operationalInfo = new LinkedHashMap<>();
- DatabaseMetaData md = connection.getMetaData();
- operationalInfo.put("databaseUrl",md.getURL());
- operationalInfo.put("databaseUser", md.getUserName());
- operationalInfo.put("databaseProduct", md.getDatabaseProductName() + " " + md.getDatabaseProductVersion());
- operationalInfo.put("databaseDriver", md.getDriverName() + " " + md.getDriverVersion());
+ try {
+ operationalInfo = new LinkedHashMap<>();
+ DatabaseMetaData md = connection.getMetaData();
+ operationalInfo.put("databaseUrl", md.getURL());
+ operationalInfo.put("databaseUser", md.getUserName());
+ operationalInfo.put("databaseProduct", md.getDatabaseProductName() + " " + md.getDatabaseProductVersion());
+ operationalInfo.put("databaseDriver", md.getDriverName() + " " + md.getDriverVersion());
logger.debugf("Database info: %s", operationalInfo.toString());
- } catch (SQLException e) {
- logger.warn("Unable to prepare operational info due database exception: " + e.getMessage());
- }
- }
+ } catch (SQLException e) {
+ logger.warn("Unable to prepare operational info due database exception: " + e.getMessage());
+ }
+ }
protected String detectDialect(Connection connection) {
@@ -334,11 +385,11 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
public String getSchema() {
return config.get("schema");
}
-
+
@Override
- public Map getOperationalInfo() {
- return operationalInfo;
- }
+ public Map getOperationalInfo() {
+ return operationalInfo;
+ }
private MigrationStrategy getMigrationStrategy() {
String migrationStrategy = config.get("migrationStrategy");
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProvider.java
index 7c48499798..d080542032 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProvider.java
@@ -23,14 +23,13 @@ import java.sql.SQLException;
import liquibase.Liquibase;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
-import liquibase.exception.LockException;
-import liquibase.lockservice.LockService;
import org.jboss.logging.Logger;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.connections.jpa.JpaConnectionProviderFactory;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.dblock.DBLockProvider;
+import org.keycloak.models.utils.KeycloakModelUtils;
/**
* @author Marek Posolda
@@ -57,6 +56,7 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
this.session = session;
}
+
private void lazyInit() {
if (!initialized) {
LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
@@ -92,35 +92,41 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
@Override
public void waitForLock() {
- lazyInit();
+ KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
- while (maxAttempts > 0) {
- try {
- lockService.waitForLock();
- factory.setHasLock(true);
- this.maxAttempts = DEFAULT_MAX_ATTEMPTS;
- return;
- } catch (LockRetryException le) {
- // Indicates we should try to acquire lock again in different transaction
- safeRollbackConnection();
- restart();
- maxAttempts--;
- } catch (RuntimeException re) {
- safeRollbackConnection();
- safeCloseConnection();
- throw re;
+ lazyInit();
+
+ while (maxAttempts > 0) {
+ try {
+ lockService.waitForLock();
+ factory.setHasLock(true);
+ this.maxAttempts = DEFAULT_MAX_ATTEMPTS;
+ return;
+ } catch (LockRetryException le) {
+ // Indicates we should try to acquire lock again in different transaction
+ safeRollbackConnection();
+ restart();
+ maxAttempts--;
+ } catch (RuntimeException re) {
+ safeRollbackConnection();
+ safeCloseConnection();
+ throw re;
+ }
}
- }
+ });
+
}
@Override
public void releaseLock() {
- lazyInit();
+ KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
+ lazyInit();
- lockService.releaseLock();
- lockService.reset();
- factory.setHasLock(false);
+ lockService.releaseLock();
+ lockService.reset();
+ factory.setHasLock(false);
+ });
}
@Override
@@ -136,21 +142,25 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
@Override
public void destroyLockInfo() {
- lazyInit();
+ KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
+ lazyInit();
- try {
- this.lockService.destroy();
- dbConnection.commit();
- logger.debug("Destroyed lock table");
- } catch (DatabaseException | SQLException de) {
- logger.error("Failed to destroy lock table");
- safeRollbackConnection();
- }
+ try {
+ this.lockService.destroy();
+ dbConnection.commit();
+ logger.debug("Destroyed lock table");
+ } catch (DatabaseException | SQLException de) {
+ logger.error("Failed to destroy lock table");
+ safeRollbackConnection();
+ }
+ });
}
@Override
public void close() {
- safeCloseConnection();
+ KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
+ safeCloseConnection();
+ });
}
private void safeRollbackConnection() {
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
index 5ac7d2f6a9..0385f6aa70 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
@@ -21,6 +21,8 @@ import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
import org.hibernate.jpa.boot.spi.Bootstrap;
+import org.jboss.logging.Logger;
+import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
import org.keycloak.connections.jpa.entityprovider.ProxyClassLoader;
import org.keycloak.models.KeycloakSession;
@@ -46,8 +48,9 @@ public class JpaUtils {
return (schema==null) ? tableName : schema + "." + tableName;
}
- public static EntityManagerFactory createEntityManagerFactory(KeycloakSession session, String unitName, Map properties, ClassLoader classLoader) {
- PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(classLoader), PersistenceUnitTransactionType.RESOURCE_LOCAL);
+ public static EntityManagerFactory createEntityManagerFactory(KeycloakSession session, String unitName, Map properties, ClassLoader classLoader, boolean jta) {
+ PersistenceUnitTransactionType txType = jta ? PersistenceUnitTransactionType.JTA : PersistenceUnitTransactionType.RESOURCE_LOCAL;
+ PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(classLoader), txType);
List persistenceUnits = parser.doResolve(properties);
for (ParsedPersistenceXmlDescriptor persistenceUnit : persistenceUnits) {
if (persistenceUnit.getName().equals(unitName)) {
@@ -58,6 +61,7 @@ public class JpaUtils {
}
// Now build the entity manager factory, supplying a proxy classloader, so Hibernate will be able
// to find and load the extra provided entities. Set the provided classloader as parent classloader.
+ persistenceUnit.setTransactionType(txType);
return Bootstrap.getEntityManagerFactoryBuilder(persistenceUnit, properties,
new ProxyClassLoader(providedEntities, classLoader)).build();
}
diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java b/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java
index 0e2dcbea97..a18d8cfd96 100755
--- a/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java
+++ b/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java
@@ -23,6 +23,21 @@ package org.keycloak.models;
*/
public interface KeycloakTransactionManager extends KeycloakTransaction {
+ enum JTAPolicy {
+ /**
+ * Do not interact with JTA at all
+ *
+ */
+ NOT_SUPPORTED,
+ /**
+ * A new JTA Transaction will be created when Keycloak TM begin() is called. If an existing JTA transaction
+ * exists, it is suspended and resumed after the Keycloak transaction finishes.
+ */
+ REQUIRES_NEW,
+ }
+
+ JTAPolicy getJTAPolicy();
+ void setJTAPolicy(JTAPolicy policy);
void enlist(KeycloakTransaction transaction);
void enlistAfterCompletion(KeycloakTransaction transaction);
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index bc20d49042..b8adc6271d 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -44,8 +44,14 @@ import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.common.util.CertificateUtils;
import org.keycloak.common.util.PemUtils;
+import org.keycloak.transaction.JtaTransactionManagerLookup;
import javax.crypto.spec.SecretKeySpec;
+import javax.naming.InitialContext;
+import javax.sql.DataSource;
+import javax.transaction.InvalidTransactionException;
+import javax.transaction.SystemException;
+import javax.transaction.Transaction;
import java.io.IOException;
import java.io.StringWriter;
import java.security.Key;
@@ -56,6 +62,7 @@ import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
+import java.sql.DriverManager;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -63,6 +70,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
+import java.util.function.Function;
/**
* Set of helper methods, which are useful in various model implementations.
@@ -303,6 +311,7 @@ public final class KeycloakModelUtils {
}
}
+
public static String getMasterRealmAdminApplicationClientId(String realmName) {
return realmName + "-realm";
}
@@ -651,4 +660,33 @@ public final class KeycloakModelUtils {
}
}
}
+
+ public static void suspendJtaTransaction(KeycloakSessionFactory factory, Runnable runnable) {
+ JtaTransactionManagerLookup lookup = (JtaTransactionManagerLookup)factory.getProviderFactory(JtaTransactionManagerLookup.class);
+ Transaction suspended = null;
+ try {
+ if (lookup != null) {
+ if (lookup.getTransactionManager() != null) {
+ try {
+ suspended = lookup.getTransactionManager().suspend();
+ } catch (SystemException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ runnable.run();
+ } finally {
+ if (suspended != null) {
+ try {
+ lookup.getTransactionManager().resume(suspended);
+ } catch (InvalidTransactionException e) {
+ throw new RuntimeException(e);
+ } catch (SystemException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ }
+
+ }
}
diff --git a/services/src/main/java/org/keycloak/transaction/JtaTransactionManagerLookup.java b/server-spi/src/main/java/org/keycloak/transaction/JtaTransactionManagerLookup.java
similarity index 100%
rename from services/src/main/java/org/keycloak/transaction/JtaTransactionManagerLookup.java
rename to server-spi/src/main/java/org/keycloak/transaction/JtaTransactionManagerLookup.java
diff --git a/services/src/main/java/org/keycloak/transaction/TransactionManagerLookupSpi.java b/server-spi/src/main/java/org/keycloak/transaction/TransactionManagerLookupSpi.java
similarity index 100%
rename from services/src/main/java/org/keycloak/transaction/TransactionManagerLookupSpi.java
rename to server-spi/src/main/java/org/keycloak/transaction/TransactionManagerLookupSpi.java
diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index d2431d84b0..5ab0346aa3 100755
--- a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -61,4 +61,5 @@ org.keycloak.authorization.AuthorizationSpi
org.keycloak.models.cache.authorization.CachedStoreFactorySpi
org.keycloak.protocol.oidc.TokenIntrospectionSpi
org.keycloak.policy.PasswordPolicySpi
-org.keycloak.policy.PasswordPolicyManagerSpi
\ No newline at end of file
+org.keycloak.policy.PasswordPolicyManagerSpi
+org.keycloak.transaction.TransactionManagerLookupSpi
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
index 539bafbb5a..b1ae4ddac1 100644
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
@@ -27,6 +27,7 @@ import org.keycloak.scripting.ScriptingProvider;
import org.keycloak.storage.UserStorageManager;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
+import javax.transaction.TransactionManager;
import java.util.*;
/**
@@ -51,7 +52,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
public DefaultKeycloakSession(DefaultKeycloakSessionFactory factory) {
this.factory = factory;
- this.transactionManager = new DefaultKeycloakTransactionManager();
+ this.transactionManager = new DefaultKeycloakTransactionManager(this);
federationManager = new UserFederationManager(this);
context = new DefaultKeycloakContext(this);
}
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
index 45bef3c3ea..d5474dcb69 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
@@ -49,7 +49,6 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
private Map, String> provider = new HashMap<>();
private volatile Map, Map> factoriesMap = new HashMap<>();
protected CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>();
- private TransactionManager tm;
// TODO: Likely should be changed to int and use Time.currentTime() to be compatible with all our "time" reps
protected long serverStartupTimestamp;
@@ -97,8 +96,6 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
// make the session factory ready for hot deployment
ProviderManagerRegistry.SINGLETON.setDeployer(this);
- JtaTransactionManagerLookup lookup = (JtaTransactionManagerLookup)getProviderFactory(JtaTransactionManagerLookup.class);
- if (lookup != null) tm = lookup.getTransactionManager();
}
protected Map, Map> getFactoriesCopy() {
Map, Map> copy = new HashMap<>();
@@ -282,9 +279,6 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
public KeycloakSession create() {
KeycloakSession session = new DefaultKeycloakSession(this);
- if (tm != null) {
- session.getTransactionManager().enlist(new JtaTransactionWrapper(tm));
- }
return session;
}
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
index 53f92f4fb8..81379a1cdf 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
@@ -16,9 +16,13 @@
*/
package org.keycloak.services;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.KeycloakTransactionManager;
+import org.keycloak.transaction.JtaTransactionManagerLookup;
+import org.keycloak.transaction.JtaTransactionWrapper;
+import javax.transaction.TransactionManager;
import java.util.LinkedList;
import java.util.List;
@@ -34,6 +38,12 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
private List afterCompletion = new LinkedList();
private boolean active;
private boolean rollback;
+ private KeycloakSession session;
+ private JTAPolicy jtaPolicy = JTAPolicy.REQUIRES_NEW;
+
+ public DefaultKeycloakTransactionManager(KeycloakSession session) {
+ this.session = session;
+ }
@Override
public void enlist(KeycloakTransaction transaction) {
@@ -62,12 +72,31 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
prepare.add(transaction);
}
+ @Override
+ public JTAPolicy getJTAPolicy() {
+ return jtaPolicy;
+ }
+
+ @Override
+ public void setJTAPolicy(JTAPolicy policy) {
+ jtaPolicy = policy;
+
+ }
+
@Override
public void begin() {
if (active) {
throw new IllegalStateException("Transaction already active");
}
+ if (jtaPolicy == JTAPolicy.REQUIRES_NEW) {
+ JtaTransactionManagerLookup jtaLookup = session.getProvider(JtaTransactionManagerLookup.class);
+ TransactionManager tm = jtaLookup.getTransactionManager();
+ if (tm != null) {
+ enlist(new JtaTransactionWrapper(tm));
+ }
+ }
+
for (KeycloakTransaction tx : transactions) {
tx.begin();
}
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 6cdf52eab2..68e080663b 100644
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -45,10 +45,13 @@ import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
import org.keycloak.services.util.JsonConfigProvider;
import org.keycloak.services.util.ObjectMapperResolver;
import org.keycloak.timer.TimerProvider;
+import org.keycloak.transaction.JtaTransactionManagerLookup;
import org.keycloak.util.JsonSerialization;
import org.keycloak.common.util.SystemEnvProperties;
import javax.servlet.ServletContext;
+import javax.transaction.SystemException;
+import javax.transaction.Transaction;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;
@@ -155,11 +158,28 @@ public class KeycloakApplication extends Application {
// Migrate model, bootstrap master realm, import realms and create admin user. This is done with acquired dbLock
protected ExportImportManager migrateAndBootstrap() {
ExportImportManager exportImportManager;
+ logger.debug("Calling migrateModel");
migrateModel();
+ logger.debug("bootstrap");
KeycloakSession session = sessionFactory.create();
try {
session.getTransactionManager().begin();
+ JtaTransactionManagerLookup lookup = (JtaTransactionManagerLookup) sessionFactory.getProviderFactory(JtaTransactionManagerLookup.class);
+ if (lookup != null) {
+ if (lookup.getTransactionManager() != null) {
+ try {
+ Transaction transaction = lookup.getTransactionManager().getTransaction();
+ logger.debugv("bootstrap current transaction? {0}", transaction != null);
+ if (transaction != null) {
+ logger.debugv("bootstrap current transaction status? {0}", transaction.getStatus());
+ }
+ } catch (SystemException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
exportImportManager = new ExportImportManager(session);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 3986988f87..26daf84019 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -184,6 +184,8 @@ public class UsersResource {
return ErrorResponse.exists("User is read only!");
} catch (ModelException me) {
return ErrorResponse.exists("Could not update user!");
+ } catch (Exception me) { // JPA may be committed by JTA which can't
+ return ErrorResponse.exists("Could not update user!");
}
}
diff --git a/services/src/main/java/org/keycloak/transaction/JtaTransactionWrapper.java b/services/src/main/java/org/keycloak/transaction/JtaTransactionWrapper.java
index ecc3071a0d..e387ff4599 100644
--- a/services/src/main/java/org/keycloak/transaction/JtaTransactionWrapper.java
+++ b/services/src/main/java/org/keycloak/transaction/JtaTransactionWrapper.java
@@ -16,7 +16,9 @@
*/
package org.keycloak.transaction;
+import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.storage.UserStorageManager;
import javax.transaction.InvalidTransactionException;
import javax.transaction.NotSupportedException;
@@ -31,16 +33,22 @@ import javax.transaction.UserTransaction;
* @version $Revision: 1 $
*/
public class JtaTransactionWrapper implements KeycloakTransaction {
+ private static final Logger logger = Logger.getLogger(JtaTransactionWrapper.class);
protected TransactionManager tm;
protected Transaction ut;
protected Transaction suspended;
+ protected Exception ended;
public JtaTransactionWrapper(TransactionManager tm) {
this.tm = tm;
try {
+
suspended = tm.suspend();
+ logger.debug("new JtaTransactionWrapper");
+ logger.debugv("was existing? {0}", suspended != null);
tm.begin();
ut = tm.getTransaction();
+ //ended = new Exception();
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -53,16 +61,20 @@ public class JtaTransactionWrapper implements KeycloakTransaction {
@Override
public void commit() {
try {
- ut.commit();
+ logger.debug("JtaTransactionWrapper commit");
+ tm.commit();
} catch (Exception e) {
throw new RuntimeException(e);
+ } finally {
+ end();
}
}
@Override
public void rollback() {
try {
- ut.rollback();
+ logger.debug("JtaTransactionWrapper rollback");
+ tm.rollback();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
@@ -74,7 +86,7 @@ public class JtaTransactionWrapper implements KeycloakTransaction {
@Override
public void setRollbackOnly() {
try {
- ut.setRollbackOnly();
+ tm.setRollbackOnly();
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -83,7 +95,7 @@ public class JtaTransactionWrapper implements KeycloakTransaction {
@Override
public boolean getRollbackOnly() {
try {
- return ut.getStatus() == Status.STATUS_MARKED_ROLLBACK;
+ return tm.getStatus() == Status.STATUS_MARKED_ROLLBACK;
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -92,15 +104,28 @@ public class JtaTransactionWrapper implements KeycloakTransaction {
@Override
public boolean isActive() {
try {
- return ut.getStatus() == Status.STATUS_ACTIVE;
+ return tm.getStatus() == Status.STATUS_ACTIVE;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
+ /*
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (ended != null) {
+ logger.error("TX didn't close at position", ended);
+ }
+
+ }
+ */
protected void end() {
+ ended = null;
+ logger.debug("JtaTransactionWrapper end");
if (suspended != null) {
try {
+ logger.debug("JtaTransactionWrapper resuming suspended");
tm.resume(suspended);
} catch (Exception e) {
throw new RuntimeException(e);
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 77cba5e1ef..55b31a09f9 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -18,5 +18,4 @@
org.keycloak.exportimport.ClientDescriptionConverterSpi
org.keycloak.wellknown.WellKnownSpi
org.keycloak.services.clientregistration.ClientRegistrationSpi
-org.keycloak.transaction.TransactionManagerLookupSpi
diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml
index 326fa7f26f..c823e4f3a3 100755
--- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml
+++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml
@@ -29,7 +29,7 @@
sa
-
+
h2