Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change how JPAConfig cleans up its resources #45451

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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<DotName> SESSION_FACTORY_EXPOSED_TYPES = Arrays.asList(ClassNames.ENTITY_MANAGER_FACTORY,
ClassNames.SESSION_FACTORY);
private static final List<DotName> SESSION_EXPOSED_TYPES = Arrays.asList(ClassNames.ENTITY_MANAGER, ClassNames.SESSION);
Expand Down Expand Up @@ -131,7 +136,6 @@ public void transform(TransformationContext transformationContext) {
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
void generateJpaConfigBean(HibernateOrmRecorder recorder,
Capabilities capabilities,
HibernateOrmRuntimeConfig hibernateOrmRuntimeConfig,
BuildProducer<SyntheticBeanBuildItem> syntheticBeanBuildItemBuildProducer) {
ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem
Expand All @@ -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<ObserverRegistrationPhaseBuildItem.ObserverConfiguratorBuildItem> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -120,18 +118,17 @@ public Set<String> getDeactivatedPersistenceUnitNames() {
return deactivatedPersistenceUnitNames;
}

public static class Destroyer implements BeanDestroyer<JPAConfig> {
@Override
public void destroy(JPAConfig instance, CreationalContext<JPAConfig> creationalContext, Map<String, Object> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-h2</artifactId>
Expand Down Expand Up @@ -171,6 +176,32 @@
<configuration>
<skip>false</skip>
</configuration>
<executions>
<execution>
<id>default-test</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<!-- These tests seem to leak metaspace memory
so we run them in a separate, short-lived JVM -->
<excludedGroups>devmode</excludedGroups>
</configuration>
</execution>
<execution>
<id>devmode-test</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<!-- These tests seem to leak metaspace memory
so we run them in a separate, short-lived JVM -->
<groups>devmode</groups>
<!-- We could consider setting reuseForks=false if we
really need to run every single test in its own JVM -->
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Loading