Make lockTimeout more friendly for JPA map storage

Closes #16616
This commit is contained in:
Michal Hajas 2023-02-10 11:09:05 +01:00
parent bb0eb899a7
commit 1f929c78af
5 changed files with 73 additions and 29 deletions

View file

@ -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();
}
} }

View file

@ -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();

View file

@ -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)

View file

@ -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)

View file

@ -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