Workarounds to make Listeners and non-autocommit work on Quarkus

Closes #13200
This commit is contained in:
Alexander Schwartz 2022-07-19 19:16:34 +02:00 committed by Hynek Mlnařík
parent 2f216ad505
commit 4d19099c66
5 changed files with 67 additions and 23 deletions

View file

@ -23,28 +23,38 @@ import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
import org.keycloak.common.Profile;
import org.keycloak.models.map.storage.jpa.hibernate.listeners.JpaAutoFlushListener;
import org.keycloak.models.map.storage.jpa.hibernate.listeners.JpaEntityVersionListener;
import org.keycloak.models.map.storage.jpa.hibernate.listeners.JpaOptimisticLockingListener;
/**
* Adding listeners to Hibernate's entity manager for the JPA Map store.
* This used to be a JPA map storage specific configuration via the Hibernate option <code>hibernate.integrator_provider</code>
* which worked in an Undertow setup but not in a Quarkus setup.
* As this will be called for both the legacy store and the new JPA Map store, it first checks if the JPA Map store has been enabled.
* A follow-up issue to track this is here: <a href="https://github.com/keycloak/keycloak/issues/13219">#13219</a>
*/
public class EventListenerIntegrator implements Integrator {
@Override
public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactoryImplementor,
SessionFactoryServiceRegistry sessionFactoryServiceRegistry) {
final EventListenerRegistry eventListenerRegistry =
sessionFactoryServiceRegistry.getService( EventListenerRegistry.class );
if (Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE)) {
final EventListenerRegistry eventListenerRegistry =
sessionFactoryServiceRegistry.getService(EventListenerRegistry.class);
eventListenerRegistry.appendListeners(EventType.PRE_INSERT, JpaOptimisticLockingListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, JpaOptimisticLockingListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_DELETE, JpaOptimisticLockingListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_INSERT, JpaOptimisticLockingListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, JpaOptimisticLockingListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_DELETE, JpaOptimisticLockingListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_INSERT, JpaEntityVersionListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, JpaEntityVersionListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_DELETE, JpaEntityVersionListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_INSERT, JpaEntityVersionListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, JpaEntityVersionListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_DELETE, JpaEntityVersionListener.INSTANCE);
// replace auto-flush listener
eventListenerRegistry.setListeners(EventType.AUTO_FLUSH, JpaAutoFlushListener.INSTANCE);
// replace auto-flush listener
eventListenerRegistry.setListeners(EventType.AUTO_FLUSH, JpaAutoFlushListener.INSTANCE);
}
}
@Override

View file

@ -152,14 +152,6 @@ public class JpaMapStorageProviderFactory implements
public static final String HIBERNATE_DEFAULT_SCHEMA = "hibernate.default_schema";
public static Map<String, Object> configureHibernateProperties() {
Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.integrator_provider", new KeycloakIntegratorProvider());
return properties;
}
private volatile EntityManagerFactory emf;
private final Set<Class<?>> validatedModels = ConcurrentHashMap.newKeySet();
private Config.Scope config;
@ -268,12 +260,16 @@ public class JpaMapStorageProviderFactory implements
// check the session for a cached provider before creating a new one.
JpaMapStorageProvider provider = session.getAttribute(this.sessionProviderKey, JpaMapStorageProvider.class);
if (provider == null) {
provider = new JpaMapStorageProvider(this, session, emf.createEntityManager(), this.sessionTxKey);
provider = new JpaMapStorageProvider(this, session, getEntityManager(), this.sessionTxKey);
session.setAttribute(this.sessionProviderKey, provider);
}
return provider;
}
protected EntityManager getEntityManager() {
return emf.createEntityManager();
}
@Override
public void init(Config.Scope config) {
this.config = config;
@ -347,8 +343,6 @@ public class JpaMapStorageProviderFactory implements
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
properties.put("hibernate.dialect", config.get("driverDialect"));
properties.putAll(configureHibernateProperties());
logger.trace("Creating EntityManagerFactory");
ParsedPersistenceXmlDescriptor descriptor = PersistenceXmlParser.locateIndividualPersistenceUnit(
JpaMapStorageProviderFactory.class.getClassLoader()

View file

@ -0,0 +1,18 @@
#
# Copyright 2022 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.keycloak.models.map.storage.jpa.EventListenerIntegrator

View file

@ -245,7 +245,6 @@ class KeycloakProcessor {
} else {
descriptor = PersistenceXmlParser.locateIndividualPersistenceUnit(
Thread.currentThread().getContextClassLoader().getResource("default-map-jpa-persistence.xml"));
descriptor.getProperties().putAll(QuarkusJpaMapStorageProviderFactory.configureHibernateProperties());
}
producer.produce(new PersistenceXmlDescriptorBuildItem(descriptor));

View file

@ -26,9 +26,12 @@ import java.sql.Connection;
import java.sql.SQLException;
import java.util.Optional;
import javax.enterprise.inject.Instance;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.internal.SessionImpl;
import org.keycloak.config.StorageOptions;
import org.keycloak.models.ModelException;
import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory;
import io.quarkus.arc.Arc;
@ -52,10 +55,30 @@ public class QuarkusJpaMapStorageProviderFactory extends JpaMapStorageProviderFa
return getEntityManagerFactory("keycloak-default").orElseThrow(() -> new IllegalStateException("Failed to resolve the default entity manager factory"));
}
@Override
protected EntityManager getEntityManager() {
EntityManager em = super.getEntityManager();
try {
Connection connection = em.unwrap(SessionImpl.class).connection();
// In the Undertow setup, Hibernate sets the connection to non-autocommit, and in the Quarkus setup the XA transaction manager does this.
// For the Quarkus setup without a XA transaction manager, we didn't find a way to have this setup automatically.
// There is also no known option to configure this in the Agroal DB connection pool in a Quarkus setup:
// While the connection pool supports it, it hasn't been exposed as a Quarkus configuration option.
// At the same time, disabling autocommit is essential to keep the transactional boundaries of the application.
// The failure we've seen is the failed unique constraints that are usually deferred (for example, for client attributes).
// A follow-up issue to track this is here: https://github.com/keycloak/keycloak/issues/13222
if (connection.getAutoCommit()) {
connection.setAutoCommit(false);
}
} catch (SQLException e) {
throw new ModelException("unable to set non-auto-commit to false");
}
return em;
}
@Override
protected Connection getConnection() {
SessionFactoryImpl entityManagerFactory = getEntityManagerFactory().unwrap(SessionFactoryImpl.class);
try {
return entityManagerFactory.getJdbcServices().getBootstrapJdbcConnectionAccess().obtainConnection();
} catch (SQLException cause) {