parent
bb0eb899a7
commit
1f929c78af
5 changed files with 73 additions and 29 deletions
|
@ -26,6 +26,7 @@ import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
@ -146,6 +147,8 @@ import org.keycloak.models.map.storage.jpa.user.entity.JpaUserFederatedIdentityE
|
||||||
import org.keycloak.models.map.user.MapUserCredentialEntity;
|
import org.keycloak.models.map.user.MapUserCredentialEntity;
|
||||||
import org.keycloak.models.map.user.MapUserCredentialEntityImpl;
|
import org.keycloak.models.map.user.MapUserCredentialEntityImpl;
|
||||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.keycloak.transaction.JtaTransactionManagerLookup;
|
import org.keycloak.transaction.JtaTransactionManagerLookup;
|
||||||
|
|
||||||
|
@ -161,6 +164,8 @@ public class JpaMapStorageProviderFactory implements
|
||||||
|
|
||||||
public static final String HIBERNATE_DEFAULT_SCHEMA = "hibernate.default_schema";
|
public static final String HIBERNATE_DEFAULT_SCHEMA = "hibernate.default_schema";
|
||||||
|
|
||||||
|
private static final long DEFAULT_LOCK_TIMEOUT = 10000;
|
||||||
|
|
||||||
private volatile EntityManagerFactory emf;
|
private volatile EntityManagerFactory emf;
|
||||||
private final Set<Class<?>> validatedModels = ConcurrentHashMap.newKeySet();
|
private final Set<Class<?>> validatedModels = ConcurrentHashMap.newKeySet();
|
||||||
private Config.Scope config;
|
private Config.Scope config;
|
||||||
|
@ -290,10 +295,10 @@ public class JpaMapStorageProviderFactory implements
|
||||||
EntityManager em = emf.createEntityManager();
|
EntityManager em = emf.createEntityManager();
|
||||||
|
|
||||||
// This is a workaround for Hibernate not supporting javax.persistence.lock.timeout
|
// This is a workaround for Hibernate not supporting javax.persistence.lock.timeout
|
||||||
// config option for Postgresql/CockroachDB - https://hibernate.atlassian.net/browse/HHH-16071
|
// config option for Postgresql/CockroachDB - https://hibernate.atlassian.net/browse/HHH-16181
|
||||||
if ("postgresql".equals(databaseShortName) || "cockroachdb".equals(databaseShortName)) {
|
if ("postgresql".equals(databaseShortName) || "cockroachdb".equals(databaseShortName)) {
|
||||||
Long lockTimeout = config.getLong("lockTimeout");
|
Long lockTimeout = config.getLong("lockTimeout", DEFAULT_LOCK_TIMEOUT);
|
||||||
if (lockTimeout != null) {
|
if (lockTimeout >= 0) {
|
||||||
em.unwrap(SessionImpl.class)
|
em.unwrap(SessionImpl.class)
|
||||||
.doWork(connection -> {
|
.doWork(connection -> {
|
||||||
// 'SET LOCAL lock_timeout = ...' can't be used with parameters in a prepared statement, leads to an
|
// 'SET LOCAL lock_timeout = ...' can't be used with parameters in a prepared statement, leads to an
|
||||||
|
@ -307,8 +312,6 @@ public class JpaMapStorageProviderFactory implements
|
||||||
resultSet.close();
|
resultSet.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
logger.warnf("Database %s used without lockTimeout option configured. This can result in deadlock where one connection waits for a pessimistic write lock forever.", databaseShortName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return em;
|
return em;
|
||||||
|
@ -403,10 +406,12 @@ public class JpaMapStorageProviderFactory implements
|
||||||
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
|
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
|
||||||
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
|
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
|
||||||
properties.put("hibernate.dialect", config.get("driverDialect"));
|
properties.put("hibernate.dialect", config.get("driverDialect"));
|
||||||
Integer lockTimeout = config.getInt("lockTimeout");
|
Long lockTimeout = config.getLong("lockTimeout", DEFAULT_LOCK_TIMEOUT);
|
||||||
if (lockTimeout != null) {
|
if (lockTimeout >= 0) {
|
||||||
// This property does not work for PostgreSQL/CockroachDB - https://hibernate.atlassian.net/browse/HHH-16071
|
// This property does not work for PostgreSQL/CockroachDB - https://hibernate.atlassian.net/browse/HHH-16181
|
||||||
properties.put("javax.persistence.lock.timeout", lockTimeout);
|
properties.put(AvailableSettings.JPA_LOCK_TIMEOUT, String.valueOf(lockTimeout));
|
||||||
|
} else {
|
||||||
|
logger.warnf("Database %s used without lockTimeout option configured. This can result in deadlock where one connection waits for a pessimistic write lock forever.", databaseShortName);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.trace("Creating EntityManagerFactory");
|
logger.trace("Creating EntityManagerFactory");
|
||||||
|
@ -536,4 +541,15 @@ public class JpaMapStorageProviderFactory implements
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigMetadata() {
|
||||||
|
return ProviderConfigurationBuilder.create()
|
||||||
|
.property()
|
||||||
|
.name("lockTimeout")
|
||||||
|
.type("long")
|
||||||
|
.defaultValue(10000L)
|
||||||
|
.helpText("The maximum time to wait in milliseconds when waiting for acquiring a pessimistic read lock. If set to negative there is no timeout configured.")
|
||||||
|
.add().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -322,6 +322,11 @@ class KeycloakProcessor {
|
||||||
unitProperties.setProperty(AvailableSettings.JPA_TRANSACTION_TYPE, PersistenceUnitTransactionType.JTA.name());
|
unitProperties.setProperty(AvailableSettings.JPA_TRANSACTION_TYPE, PersistenceUnitTransactionType.JTA.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfigValue lockTimeoutConfigValue = getConfig().getConfigValue("kc.spi-map-storage-jpa-lock-timeout");
|
||||||
|
if (lockTimeoutConfigValue != null && lockTimeoutConfigValue.getValue() != null) {
|
||||||
|
unitProperties.setProperty(AvailableSettings.JPA_LOCK_TIMEOUT, lockTimeoutConfigValue.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
unitProperties.setProperty(AvailableSettings.QUERY_STARTUP_CHECKING, Boolean.FALSE.toString());
|
unitProperties.setProperty(AvailableSettings.QUERY_STARTUP_CHECKING, Boolean.FALSE.toString());
|
||||||
|
|
||||||
String dbKind = defaultDataSource.getDbKind();
|
String dbKind = defaultDataSource.getDbKind();
|
||||||
|
|
|
@ -54,6 +54,8 @@ import org.keycloak.testsuite.model.KeycloakModelParameters;
|
||||||
import org.testcontainers.containers.PostgreSQLContainer;
|
import org.testcontainers.containers.PostgreSQLContainer;
|
||||||
import org.testcontainers.utility.DockerImageName;
|
import org.testcontainers.utility.DockerImageName;
|
||||||
|
|
||||||
|
import static org.keycloak.testsuite.model.transaction.StorageTransactionTest.LOCK_TIMEOUT_SYSTEM_PROPERTY;
|
||||||
|
|
||||||
public class JpaMapStorage extends KeycloakModelParameters {
|
public class JpaMapStorage extends KeycloakModelParameters {
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(JpaMapStorage.class.getName());
|
private static final Logger LOG = Logger.getLogger(JpaMapStorage.class.getName());
|
||||||
|
@ -96,7 +98,7 @@ public class JpaMapStorage extends KeycloakModelParameters {
|
||||||
.config("password", POSTGRES_DB_PASSWORD)
|
.config("password", POSTGRES_DB_PASSWORD)
|
||||||
.config("driver", "org.postgresql.Driver")
|
.config("driver", "org.postgresql.Driver")
|
||||||
.config("driverDialect", "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect")
|
.config("driverDialect", "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect")
|
||||||
.config("lockTimeout", "1000");
|
.config("lockTimeout", "${" + LOCK_TIMEOUT_SYSTEM_PROPERTY + ":}");
|
||||||
|
|
||||||
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||||
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||||
|
|
|
@ -56,6 +56,8 @@ import org.testcontainers.utility.DockerImageName;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.keycloak.testsuite.model.transaction.StorageTransactionTest.LOCK_TIMEOUT_SYSTEM_PROPERTY;
|
||||||
|
|
||||||
public class JpaMapStorageCockroachdb extends KeycloakModelParameters {
|
public class JpaMapStorageCockroachdb extends KeycloakModelParameters {
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(JpaMapStorageCockroachdb.class.getName());
|
private static final Logger LOG = Logger.getLogger(JpaMapStorageCockroachdb.class.getName());
|
||||||
|
@ -97,7 +99,7 @@ public class JpaMapStorageCockroachdb extends KeycloakModelParameters {
|
||||||
.config("password", COCKROACHDB_DB_PASSWORD)
|
.config("password", COCKROACHDB_DB_PASSWORD)
|
||||||
.config("driver", "org.postgresql.Driver")
|
.config("driver", "org.postgresql.Driver")
|
||||||
.config("driverDialect", "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect")
|
.config("driverDialect", "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect")
|
||||||
.config("lockTimeout", "1000");
|
.config("lockTimeout", "${" + LOCK_TIMEOUT_SYSTEM_PROPERTY + ":}");
|
||||||
|
|
||||||
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||||
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RealmProvider;
|
import org.keycloak.models.RealmProvider;
|
||||||
import org.keycloak.models.map.storage.MapStorageProvider;
|
import org.keycloak.models.map.storage.MapStorageProvider;
|
||||||
|
import org.keycloak.models.map.storage.MapStorageSpi;
|
||||||
import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory;
|
import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory;
|
||||||
import org.keycloak.testsuite.model.KeycloakModelTest;
|
import org.keycloak.testsuite.model.KeycloakModelTest;
|
||||||
import org.keycloak.testsuite.model.RequireProvider;
|
import org.keycloak.testsuite.model.RequireProvider;
|
||||||
|
@ -43,6 +44,11 @@ import static org.keycloak.testsuite.model.util.KeycloakAssertions.assertExcepti
|
||||||
@RequireProvider(RealmProvider.class)
|
@RequireProvider(RealmProvider.class)
|
||||||
public class StorageTransactionTest extends KeycloakModelTest {
|
public class StorageTransactionTest extends KeycloakModelTest {
|
||||||
|
|
||||||
|
// System variable is used to simplify configuration for more storages that support pessimistic locking.
|
||||||
|
// Instead of searching which storage is used and then configure its factory, we can just configure
|
||||||
|
// lockTimeout like this: .config("lockTimeout", "${keycloak.model.tests.lockTimeout:}") and
|
||||||
|
// system property will be picked when factory is reinitialized.
|
||||||
|
public static final String LOCK_TIMEOUT_SYSTEM_PROPERTY = "keycloak.model.tests.lockTimeout";
|
||||||
private String realmId;
|
private String realmId;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -123,6 +129,11 @@ public class StorageTransactionTest extends KeycloakModelTest {
|
||||||
// LockObjectForModification is currently used only in map-jpa
|
// LockObjectForModification is currently used only in map-jpa
|
||||||
@RequireProvider(value = MapStorageProvider.class, only = JpaMapStorageProviderFactory.PROVIDER_ID)
|
@RequireProvider(value = MapStorageProvider.class, only = JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||||
public void testLockObjectForModification() {
|
public void testLockObjectForModification() {
|
||||||
|
String originalTimeoutValue = System.getProperty(LOCK_TIMEOUT_SYSTEM_PROPERTY);
|
||||||
|
try {
|
||||||
|
System.setProperty(LOCK_TIMEOUT_SYSTEM_PROPERTY, "300");
|
||||||
|
reinitializeKeycloakSessionFactory();
|
||||||
|
|
||||||
TransactionController tx1 = new TransactionController(getFactory());
|
TransactionController tx1 = new TransactionController(getFactory());
|
||||||
TransactionController tx2 = new TransactionController(getFactory());
|
TransactionController tx2 = new TransactionController(getFactory());
|
||||||
TransactionController tx3 = new TransactionController(getFactory());
|
TransactionController tx3 = new TransactionController(getFactory());
|
||||||
|
@ -146,6 +157,14 @@ public class StorageTransactionTest extends KeycloakModelTest {
|
||||||
tx3.begin();
|
tx3.begin();
|
||||||
tx3.runStep(session -> LockObjectsForModification.lockRealmsForModification(session, () -> session.realms().getRealm(realmId)));
|
tx3.runStep(session -> LockObjectsForModification.lockRealmsForModification(session, () -> session.realms().getRealm(realmId)));
|
||||||
tx3.commit();
|
tx3.commit();
|
||||||
|
} finally {
|
||||||
|
if (originalTimeoutValue == null) {
|
||||||
|
System.clearProperty(LOCK_TIMEOUT_SYSTEM_PROPERTY);
|
||||||
|
} else {
|
||||||
|
System.setProperty(LOCK_TIMEOUT_SYSTEM_PROPERTY, originalTimeoutValue);
|
||||||
|
}
|
||||||
|
reinitializeKeycloakSessionFactory();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue