keycloak-memoizeit
Changes
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml 1(+1 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi/main/module.xml 1(+1 -0)
federation/ldap/pom.xml 5(+5 -0)
model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java 215(+133 -82)
Details
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 5373067..94c80a2 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 @@
</resources>
<dependencies>
+ <module name="javax.transaction.api"/>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-server-spi"/>
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 95f34b6..8b5632e 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 @@
<module name="javax.ws.rs.api"/>
<module name="org.apache.httpcomponents"/>
<module name="org.jboss.resteasy.resteasy-jaxrs"/>
+ <module name="javax.transaction.api"/>
</dependencies>
</module>
federation/ldap/pom.xml 5(+5 -0)
diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml
index f6a8c4b..257c617 100755
--- a/federation/ldap/pom.xml
+++ b/federation/ldap/pom.xml
@@ -74,6 +74,11 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.jboss.spec.javax.transaction</groupId>
+ <artifactId>jboss-transaction-api_1.2_spec</artifactId>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
<build>
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 db85a82..e35e5b0 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 3c74264..d029e16 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -60,16 +71,29 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
private volatile EntityManagerFactory emf;
private Config.Scope config;
-
- private Map<String,String> operationalInfo;
+
+ private Map<String, String> 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<String, Object> properties = new HashMap<String, Object>();
+ Map<String, Object> properties = new HashMap<String, Object>();
- 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"));
-
- 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);
+ 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);
+ }
}
- 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");
-
- if (globalStatsInterval != -1) {
- startGlobalStats(session, globalStatsInterval);
+ 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);
+ }
+
+ 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<String,String> getOperationalInfo() {
- return operationalInfo;
- }
+ public Map<String, String> 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 7c48499..d080542 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -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();
-
- 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;
+ KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
+
+ 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 5ac7d2f..0385f6a 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<String, Object> properties, ClassLoader classLoader) {
- PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(classLoader), PersistenceUnitTransactionType.RESOURCE_LOCAL);
+ public static EntityManagerFactory createEntityManagerFactory(KeycloakSession session, String unitName, Map<String, Object> properties, ClassLoader classLoader, boolean jta) {
+ PersistenceUnitTransactionType txType = jta ? PersistenceUnitTransactionType.JTA : PersistenceUnitTransactionType.RESOURCE_LOCAL;
+ PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(classLoader), txType);
List<ParsedPersistenceXmlDescriptor> 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 0e2dcbe..a18d8cf 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 bc20d49..b8adc62 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/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 d2431d8..5ab0346 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 539bafb..b1ae4dd 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 45bef3c..d5474dc 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<Class<? extends Provider>, String> provider = new HashMap<>();
private volatile Map<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap = new HashMap<>();
protected CopyOnWriteArrayList<ProviderEventListener> 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<Class<? extends Provider>, Map<String, ProviderFactory>> getFactoriesCopy() {
Map<Class<? extends Provider>, Map<String, ProviderFactory>> 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 53f92f4..81379a1 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<KeycloakTransaction> afterCompletion = new LinkedList<KeycloakTransaction>();
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) {
@@ -63,11 +73,30 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
}
@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/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 3986988..26daf84 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/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 6cdf52e..68e0806 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/transaction/JtaTransactionWrapper.java b/services/src/main/java/org/keycloak/transaction/JtaTransactionWrapper.java
index ecc3071..e387ff4 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 77cba5e..55b31a0 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 326fa7f..c823e4f 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 @@
<password>sa</password>
</security>
</datasource>
- <datasource jndi-name="java:jboss/datasources/KeycloakDS" jta="false" pool-name="KeycloakDS" enabled="true" use-java-context="true">
+ <datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
<?KEYCLOAK_DS_CONNECTION_URL?>
<driver>h2</driver>
<security>