diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java index fbc3f973e8953..b62d774548bda 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.hibernate.orm.deployment; +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -8,30 +9,30 @@ import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Any; import jakarta.enterprise.inject.Default; -import jakarta.enterprise.inject.Instance; import jakarta.inject.Singleton; +import jakarta.interceptor.Interceptor; import jakarta.persistence.AttributeConverter; import jakarta.transaction.TransactionManager; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.StatelessSession; -import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; -import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; -import io.agroal.api.AgroalDataSource; import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InstanceHandle; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem; +import io.quarkus.arc.deployment.ObserverRegistrationPhaseBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; @@ -47,6 +48,8 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; import io.quarkus.hibernate.orm.PersistenceUnit; import io.quarkus.hibernate.orm.runtime.HibernateOrmRecorder; import io.quarkus.hibernate.orm.runtime.HibernateOrmRuntimeConfig; @@ -56,10 +59,12 @@ import io.quarkus.hibernate.orm.runtime.RequestScopedStatelessSessionHolder; import io.quarkus.hibernate.orm.runtime.TransactionSessions; import io.quarkus.hibernate.orm.runtime.cdi.QuarkusArcBeanContainer; +import io.quarkus.runtime.ShutdownEvent; @BuildSteps(onlyIf = HibernateOrmEnabled.class) public class HibernateOrmCdiProcessor { + private static final int JPA_CONFIG_SHUTDOWN_PRIORITY = Interceptor.Priority.LIBRARY_AFTER + 100; private static final List SESSION_FACTORY_EXPOSED_TYPES = Arrays.asList(ClassNames.ENTITY_MANAGER_FACTORY, ClassNames.SESSION_FACTORY); private static final List SESSION_EXPOSED_TYPES = Arrays.asList(ClassNames.ENTITY_MANAGER, ClassNames.SESSION); @@ -131,7 +136,6 @@ public void transform(TransformationContext transformationContext) { @BuildStep @Record(ExecutionTime.RUNTIME_INIT) void generateJpaConfigBean(HibernateOrmRecorder recorder, - Capabilities capabilities, HibernateOrmRuntimeConfig hibernateOrmRuntimeConfig, BuildProducer syntheticBeanBuildItemBuildProducer) { ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem @@ -140,28 +144,46 @@ void generateJpaConfigBean(HibernateOrmRecorder recorder, .scope(Singleton.class) .unremovable() .setRuntimeInit() - .supplier(recorder.jpaConfigSupplier(hibernateOrmRuntimeConfig)) - .destroyer(JPAConfig.Destroyer.class); - - // Add a synthetic dependency from JPAConfig to any datasource/pool, - // so that JPAConfig is destroyed before the datasource/pool. - // The alternative would be adding an application destruction observer - // (@Observes @BeforeDestroyed(ApplicationScoped.class)) to JPAConfig, - // but that would force initialization of JPAConfig upon application shutdown, - // which may cause cascading failures if the shutdown happened before JPAConfig was initialized. - if (capabilities.isPresent(Capability.HIBERNATE_REACTIVE)) { - configurator.addInjectionPoint(ParameterizedType.create(DotName.createSimple(Instance.class), - new Type[] { ClassType.create(DotName.createSimple("io.vertx.sqlclient.Pool")) }, null), - AnnotationInstance.builder(Any.class).build()); - } else { - configurator.addInjectionPoint(ParameterizedType.create(DotName.createSimple(Instance.class), - new Type[] { ClassType.create(DotName.createSimple(AgroalDataSource.class)) }, null), - AnnotationInstance.builder(Any.class).build()); - } + .supplier(recorder.jpaConfigSupplier(hibernateOrmRuntimeConfig)); syntheticBeanBuildItemBuildProducer.produce(configurator.done()); } + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void generateJpaConfigBeanObserver( + HibernateOrmRecorder recorder, + ObserverRegistrationPhaseBuildItem observerRegistrationPhase, + BuildProducer observerConfigurationRegistry) { + observerConfigurationRegistry.produce( + new ObserverRegistrationPhaseBuildItem.ObserverConfiguratorBuildItem(observerRegistrationPhase.getContext() + .configure() + .beanClass(DotName.createSimple("io.quarkus.hibernate.orm.runtime.JPAConfig")) + .observedType(ShutdownEvent.class) + .priority(JPA_CONFIG_SHUTDOWN_PRIORITY) + .notify(mc -> { + // Essentially do the following: + // Arc.container().instance( JPAConfig.class ).get().shutdown(); + ResultHandle arcContainer = mc.invokeStaticMethod( + MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class)); + ResultHandle jpaConfigInstance = mc.invokeInterfaceMethod( + MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, + Class.class, Annotation[].class), + arcContainer, + mc.loadClassFromTCCL(JPAConfig.class), + mc.newArray(Annotation.class, 0)); + ResultHandle jpaConfig = mc.invokeInterfaceMethod( + MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class), + jpaConfigInstance); + + mc.invokeVirtualMethod( + MethodDescriptor.ofMethod(JPAConfig.class, "shutdown", void.class), + jpaConfig); + + mc.returnValue(null); + }))); + } + // These beans must be initialized at runtime because their initialization // depends on runtime configuration (to activate/deactivate a persistence unit) @Record(ExecutionTime.RUNTIME_INIT) diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/JPAConfig.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/JPAConfig.java index fd55b3440d127..d062383f39deb 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/JPAConfig.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/JPAConfig.java @@ -10,14 +10,12 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import jakarta.enterprise.context.spi.CreationalContext; import jakarta.inject.Inject; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Persistence; import org.jboss.logging.Logger; -import io.quarkus.arc.BeanDestroyer; import io.quarkus.hibernate.orm.runtime.boot.QuarkusPersistenceUnitDescriptor; public class JPAConfig { @@ -120,18 +118,17 @@ public Set getDeactivatedPersistenceUnitNames() { return deactivatedPersistenceUnitNames; } - public static class Destroyer implements BeanDestroyer { - @Override - public void destroy(JPAConfig instance, CreationalContext creationalContext, Map params) { - for (LazyPersistenceUnit factory : instance.persistenceUnits.values()) { - try { - factory.close(); - } catch (Exception e) { - LOGGER.warn("Unable to close the EntityManagerFactory: " + factory, e); - } + void shutdown() { + LOGGER.trace("Starting to shut down Hibernate ORM persistence units."); + for (LazyPersistenceUnit factory : this.persistenceUnits.values()) { + try { + factory.close(); + } catch (Exception e) { + LOGGER.warn("Unable to close the EntityManagerFactory: " + factory, e); } - instance.persistenceUnits.clear(); } + this.persistenceUnits.clear(); + LOGGER.trace("Finished shutting down Hibernate ORM persistence units."); } static final class LazyPersistenceUnit { diff --git a/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/pom.xml b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/pom.xml index 019a60502f8e4..2b53357189214 100644 --- a/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/pom.xml +++ b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/pom.xml @@ -50,6 +50,11 @@ quarkus-junit5 test + + io.quarkus + quarkus-junit5-internal + test + io.quarkus quarkus-test-h2 @@ -171,6 +176,32 @@ false + + + default-test + + test + + + + devmode + + + + devmode-test + + test + + + + devmode + + + + maven-failsafe-plugin diff --git a/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchDevModeTest.java b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchDevModeTest.java new file mode 100644 index 0000000000000..91162adbbc71e --- /dev/null +++ b/integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/src/test/java/io/quarkus/it/hibernate/search/orm/elasticsearch/coordination/outboxpolling/HibernateSearchDevModeTest.java @@ -0,0 +1,49 @@ +package io.quarkus.it.hibernate.search.orm.elasticsearch.coordination.outboxpolling; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +@Tag("devmode") +public class HibernateSearchDevModeTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(HibernateSearchOutboxPollingTestResource.class, Person.class, OutboxPollingTestUtils.class) + .addAsResource("application.properties", "application.properties")) + .setLogRecordPredicate(r -> true); + + @Test + public void smoke() { + String[] schemaManagementStrategies = { "drop-and-create-and-drop", "drop-and-create" }; + + RestAssured.when().put("/test/hibernate-search-outbox-polling/check-agents-running").then() + .statusCode(200) + .body(is("OK")); + + for (int i = 0; i < 3; i++) { + int current = i; + config.modifyResourceFile( + "application.properties", + s -> s.replace( + "quarkus.hibernate-search-orm.schema-management.strategy=" + + schemaManagementStrategies[current % 2], + "quarkus.hibernate-search-orm.schema-management.strategy=" + + schemaManagementStrategies[(current + 1) % 2])); + + RestAssured.when().put("/test/hibernate-search-outbox-polling/check-agents-running").then() + .statusCode(200) + .body(is("OK")); + } + + assertThat(config.getLogRecords()).noneSatisfy( + r -> assertThat(r.getMessage()).contains("Unable to shut down Hibernate Search")); + } +}