Skip to content

Commit 620beda

Browse files
committed
Observe shutdown event to close persistent units
instead of relaying on a Destroyer. This way other components e.g. datasource should still be available.
1 parent 25e8bfd commit 620beda

File tree

4 files changed

+135
-36
lines changed

4 files changed

+135
-36
lines changed

extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.hibernate.orm.deployment;
22

3+
import java.lang.annotation.Annotation;
34
import java.util.ArrayList;
45
import java.util.Arrays;
56
import java.util.List;
@@ -8,30 +9,30 @@
89
import java.util.stream.Collectors;
910

1011
import jakarta.enterprise.context.ApplicationScoped;
11-
import jakarta.enterprise.inject.Any;
1212
import jakarta.enterprise.inject.Default;
13-
import jakarta.enterprise.inject.Instance;
1413
import jakarta.inject.Singleton;
14+
import jakarta.interceptor.Interceptor;
1515
import jakarta.persistence.AttributeConverter;
1616
import jakarta.transaction.TransactionManager;
1717

1818
import org.hibernate.Session;
1919
import org.hibernate.SessionFactory;
2020
import org.hibernate.StatelessSession;
21-
import org.jboss.jandex.AnnotationInstance;
2221
import org.jboss.jandex.AnnotationTarget.Kind;
2322
import org.jboss.jandex.AnnotationValue;
2423
import org.jboss.jandex.ClassType;
2524
import org.jboss.jandex.DotName;
2625
import org.jboss.jandex.FieldInfo;
27-
import org.jboss.jandex.ParameterizedType;
2826
import org.jboss.jandex.Type;
2927

30-
import io.agroal.api.AgroalDataSource;
3128
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
29+
import io.quarkus.arc.Arc;
30+
import io.quarkus.arc.ArcContainer;
31+
import io.quarkus.arc.InstanceHandle;
3232
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
3333
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
3434
import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem;
35+
import io.quarkus.arc.deployment.ObserverRegistrationPhaseBuildItem;
3536
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
3637
import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator;
3738
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
@@ -47,6 +48,8 @@
4748
import io.quarkus.deployment.annotations.ExecutionTime;
4849
import io.quarkus.deployment.annotations.Record;
4950
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
51+
import io.quarkus.gizmo.MethodDescriptor;
52+
import io.quarkus.gizmo.ResultHandle;
5053
import io.quarkus.hibernate.orm.PersistenceUnit;
5154
import io.quarkus.hibernate.orm.runtime.HibernateOrmRecorder;
5255
import io.quarkus.hibernate.orm.runtime.HibernateOrmRuntimeConfig;
@@ -56,10 +59,12 @@
5659
import io.quarkus.hibernate.orm.runtime.RequestScopedStatelessSessionHolder;
5760
import io.quarkus.hibernate.orm.runtime.TransactionSessions;
5861
import io.quarkus.hibernate.orm.runtime.cdi.QuarkusArcBeanContainer;
62+
import io.quarkus.runtime.ShutdownEvent;
5963

6064
@BuildSteps(onlyIf = HibernateOrmEnabled.class)
6165
public class HibernateOrmCdiProcessor {
6266

67+
private static final int JPA_CONFIG_SHUTDOWN_PRIORITY = Interceptor.Priority.LIBRARY_AFTER + 100;
6368
private static final List<DotName> SESSION_FACTORY_EXPOSED_TYPES = Arrays.asList(ClassNames.ENTITY_MANAGER_FACTORY,
6469
ClassNames.SESSION_FACTORY);
6570
private static final List<DotName> SESSION_EXPOSED_TYPES = Arrays.asList(ClassNames.ENTITY_MANAGER, ClassNames.SESSION);
@@ -131,7 +136,6 @@ public void transform(TransformationContext transformationContext) {
131136
@BuildStep
132137
@Record(ExecutionTime.RUNTIME_INIT)
133138
void generateJpaConfigBean(HibernateOrmRecorder recorder,
134-
Capabilities capabilities,
135139
HibernateOrmRuntimeConfig hibernateOrmRuntimeConfig,
136140
BuildProducer<SyntheticBeanBuildItem> syntheticBeanBuildItemBuildProducer) {
137141
ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem
@@ -140,28 +144,46 @@ void generateJpaConfigBean(HibernateOrmRecorder recorder,
140144
.scope(Singleton.class)
141145
.unremovable()
142146
.setRuntimeInit()
143-
.supplier(recorder.jpaConfigSupplier(hibernateOrmRuntimeConfig))
144-
.destroyer(JPAConfig.Destroyer.class);
145-
146-
// Add a synthetic dependency from JPAConfig to any datasource/pool,
147-
// so that JPAConfig is destroyed before the datasource/pool.
148-
// The alternative would be adding an application destruction observer
149-
// (@Observes @BeforeDestroyed(ApplicationScoped.class)) to JPAConfig,
150-
// but that would force initialization of JPAConfig upon application shutdown,
151-
// which may cause cascading failures if the shutdown happened before JPAConfig was initialized.
152-
if (capabilities.isPresent(Capability.HIBERNATE_REACTIVE)) {
153-
configurator.addInjectionPoint(ParameterizedType.create(DotName.createSimple(Instance.class),
154-
new Type[] { ClassType.create(DotName.createSimple("io.vertx.sqlclient.Pool")) }, null),
155-
AnnotationInstance.builder(Any.class).build());
156-
} else {
157-
configurator.addInjectionPoint(ParameterizedType.create(DotName.createSimple(Instance.class),
158-
new Type[] { ClassType.create(DotName.createSimple(AgroalDataSource.class)) }, null),
159-
AnnotationInstance.builder(Any.class).build());
160-
}
147+
.supplier(recorder.jpaConfigSupplier(hibernateOrmRuntimeConfig));
161148

162149
syntheticBeanBuildItemBuildProducer.produce(configurator.done());
163150
}
164151

152+
@BuildStep
153+
@Record(ExecutionTime.RUNTIME_INIT)
154+
void generateJpaConfigBeanObserver(
155+
HibernateOrmRecorder recorder,
156+
ObserverRegistrationPhaseBuildItem observerRegistrationPhase,
157+
BuildProducer<ObserverRegistrationPhaseBuildItem.ObserverConfiguratorBuildItem> observerConfigurationRegistry) {
158+
observerConfigurationRegistry.produce(
159+
new ObserverRegistrationPhaseBuildItem.ObserverConfiguratorBuildItem(observerRegistrationPhase.getContext()
160+
.configure()
161+
.beanClass(DotName.createSimple("io.quarkus.hibernate.orm.runtime.JPAConfig"))
162+
.observedType(ShutdownEvent.class)
163+
.priority(JPA_CONFIG_SHUTDOWN_PRIORITY)
164+
.notify(mc -> {
165+
// Essentially do the following:
166+
// Arc.container().instance( JPAConfig.class ).get().shutdown();
167+
ResultHandle arcContainer = mc.invokeStaticMethod(
168+
MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class));
169+
ResultHandle jpaConfigInstance = mc.invokeInterfaceMethod(
170+
MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class,
171+
Class.class, Annotation[].class),
172+
arcContainer,
173+
mc.loadClassFromTCCL(JPAConfig.class),
174+
mc.newArray(Annotation.class, 0));
175+
ResultHandle jpaConfig = mc.invokeInterfaceMethod(
176+
MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class),
177+
jpaConfigInstance);
178+
179+
mc.invokeVirtualMethod(
180+
MethodDescriptor.ofMethod(JPAConfig.class, "shutdown", void.class),
181+
jpaConfig);
182+
183+
mc.returnValue(null);
184+
})));
185+
}
186+
165187
// These beans must be initialized at runtime because their initialization
166188
// depends on runtime configuration (to activate/deactivate a persistence unit)
167189
@Record(ExecutionTime.RUNTIME_INIT)

extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/JPAConfig.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@
1010
import java.util.concurrent.CompletableFuture;
1111
import java.util.concurrent.ExecutionException;
1212

13-
import jakarta.enterprise.context.spi.CreationalContext;
1413
import jakarta.inject.Inject;
1514
import jakarta.persistence.EntityManagerFactory;
1615
import jakarta.persistence.Persistence;
1716

1817
import org.jboss.logging.Logger;
1918

20-
import io.quarkus.arc.BeanDestroyer;
2119
import io.quarkus.hibernate.orm.runtime.boot.QuarkusPersistenceUnitDescriptor;
2220

2321
public class JPAConfig {
@@ -120,18 +118,17 @@ public Set<String> getDeactivatedPersistenceUnitNames() {
120118
return deactivatedPersistenceUnitNames;
121119
}
122120

123-
public static class Destroyer implements BeanDestroyer<JPAConfig> {
124-
@Override
125-
public void destroy(JPAConfig instance, CreationalContext<JPAConfig> creationalContext, Map<String, Object> params) {
126-
for (LazyPersistenceUnit factory : instance.persistenceUnits.values()) {
127-
try {
128-
factory.close();
129-
} catch (Exception e) {
130-
LOGGER.warn("Unable to close the EntityManagerFactory: " + factory, e);
131-
}
121+
void shutdown() {
122+
LOGGER.trace("Starting to shut down Hibernate ORM persistence units.");
123+
for (LazyPersistenceUnit factory : this.persistenceUnits.values()) {
124+
try {
125+
factory.close();
126+
} catch (Exception e) {
127+
LOGGER.warn("Unable to close the EntityManagerFactory: " + factory, e);
132128
}
133-
instance.persistenceUnits.clear();
134129
}
130+
this.persistenceUnits.clear();
131+
LOGGER.trace("Finished shutting down Hibernate ORM persistence units.");
135132
}
136133

137134
static final class LazyPersistenceUnit {

integration-tests/hibernate-search-orm-elasticsearch-outbox-polling/pom.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@
5050
<artifactId>quarkus-junit5</artifactId>
5151
<scope>test</scope>
5252
</dependency>
53+
<dependency>
54+
<groupId>io.quarkus</groupId>
55+
<artifactId>quarkus-junit5-internal</artifactId>
56+
<scope>test</scope>
57+
</dependency>
5358
<dependency>
5459
<groupId>io.quarkus</groupId>
5560
<artifactId>quarkus-test-h2</artifactId>
@@ -171,6 +176,32 @@
171176
<configuration>
172177
<skip>false</skip>
173178
</configuration>
179+
<executions>
180+
<execution>
181+
<id>default-test</id>
182+
<goals>
183+
<goal>test</goal>
184+
</goals>
185+
<configuration>
186+
<!-- These tests seem to leak metaspace memory
187+
so we run them in a separate, short-lived JVM -->
188+
<excludedGroups>devmode</excludedGroups>
189+
</configuration>
190+
</execution>
191+
<execution>
192+
<id>devmode-test</id>
193+
<goals>
194+
<goal>test</goal>
195+
</goals>
196+
<configuration>
197+
<!-- These tests seem to leak metaspace memory
198+
so we run them in a separate, short-lived JVM -->
199+
<groups>devmode</groups>
200+
<!-- We could consider setting reuseForks=false if we
201+
really need to run every single test in its own JVM -->
202+
</configuration>
203+
</execution>
204+
</executions>
174205
</plugin>
175206
<plugin>
176207
<artifactId>maven-failsafe-plugin</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package io.quarkus.it.hibernate.search.orm.elasticsearch.coordination.outboxpolling;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.hamcrest.Matchers.is;
5+
6+
import org.junit.jupiter.api.Tag;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.RegisterExtension;
9+
10+
import io.quarkus.test.QuarkusDevModeTest;
11+
import io.restassured.RestAssured;
12+
13+
@Tag("devmode")
14+
public class HibernateSearchDevModeTest {
15+
16+
@RegisterExtension
17+
static final QuarkusDevModeTest config = new QuarkusDevModeTest()
18+
.withApplicationRoot((jar) -> jar
19+
.addClasses(HibernateSearchOutboxPollingTestResource.class, Person.class, OutboxPollingTestUtils.class)
20+
.addAsResource("application.properties", "application.properties"))
21+
.setLogRecordPredicate(r -> true);
22+
23+
@Test
24+
public void smoke() {
25+
String[] schemaManagementStrategies = { "drop-and-create-and-drop", "drop-and-create" };
26+
27+
RestAssured.when().put("/test/hibernate-search-outbox-polling/check-agents-running").then()
28+
.statusCode(200)
29+
.body(is("OK"));
30+
31+
for (int i = 0; i < 3; i++) {
32+
int current = i;
33+
config.modifyResourceFile(
34+
"application.properties",
35+
s -> s.replace(
36+
"quarkus.hibernate-search-orm.schema-management.strategy="
37+
+ schemaManagementStrategies[current % 2],
38+
"quarkus.hibernate-search-orm.schema-management.strategy="
39+
+ schemaManagementStrategies[(current + 1) % 2]));
40+
41+
RestAssured.when().put("/test/hibernate-search-outbox-polling/check-agents-running").then()
42+
.statusCode(200)
43+
.body(is("OK"));
44+
}
45+
46+
assertThat(config.getLogRecords()).noneSatisfy(
47+
r -> assertThat(r.getMessage()).contains("Unable to shut down Hibernate Search"));
48+
}
49+
}

0 commit comments

Comments
 (0)