Skip to content

Commit 44c2ccb

Browse files
committed
Use a shutdown task to clean up ORM resources
1 parent 66aefab commit 44c2ccb

File tree

9 files changed

+285
-33
lines changed

9 files changed

+285
-33
lines changed

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

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
import java.util.stream.Collectors;
1515

1616
import jakarta.enterprise.context.ApplicationScoped;
17-
import jakarta.enterprise.inject.Any;
1817
import jakarta.enterprise.inject.Default;
19-
import jakarta.enterprise.inject.Instance;
2018
import jakarta.inject.Singleton;
2119
import jakarta.persistence.AttributeConverter;
2220
import jakarta.transaction.TransactionManager;
@@ -37,11 +35,9 @@
3735
import org.jboss.jandex.DotName;
3836
import org.jboss.jandex.FieldInfo;
3937
import org.jboss.jandex.MethodInfo;
40-
import org.jboss.jandex.ParameterizedType;
4138
import org.jboss.jandex.Type;
4239
import org.objectweb.asm.ClassVisitor;
4340

44-
import io.agroal.api.AgroalDataSource;
4541
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
4642
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
4743
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
@@ -168,32 +164,15 @@ void generateJpaConfigBean(HibernateOrmRecorder recorder,
168164
.scope(Singleton.class)
169165
.unremovable()
170166
.setRuntimeInit()
171-
.supplier(recorder.jpaConfigSupplier(hibernateOrmRuntimeConfig))
172-
.destroyer(JPAConfig.Destroyer.class);
173-
174-
// Add a synthetic dependency from JPAConfig to any datasource/pool,
175-
// so that JPAConfig is destroyed before the datasource/pool.
176-
// The alternative would be adding an application destruction observer
177-
// (@Observes @BeforeDestroyed(ApplicationScoped.class)) to JPAConfig,
178-
// but that would force initialization of JPAConfig upon application shutdown,
179-
// which may cause cascading failures if the shutdown happened before JPAConfig was initialized.
180-
if (capabilities.isPresent(Capability.HIBERNATE_REACTIVE)) {
181-
configurator.addInjectionPoint(ParameterizedType.create(DotName.createSimple(Instance.class),
182-
new Type[] { ClassType.create(DotName.createSimple("io.vertx.sqlclient.Pool")) }, null),
183-
AnnotationInstance.builder(Any.class).build());
184-
} else {
185-
configurator.addInjectionPoint(ParameterizedType.create(DotName.createSimple(Instance.class),
186-
new Type[] { ClassType.create(DotName.createSimple(AgroalDataSource.class)) }, null),
187-
AnnotationInstance.builder(Any.class).build());
188-
}
167+
.supplier(recorder.jpaConfigSupplier(hibernateOrmRuntimeConfig));
189168

190169
syntheticBeanBuildItemBuildProducer.produce(configurator.done());
191170
}
192171

193172
// These beans must be initialized at runtime because their initialization
194173
// depends on runtime configuration (to activate/deactivate a persistence unit)
195-
@Record(ExecutionTime.RUNTIME_INIT)
196174
@BuildStep
175+
@Record(ExecutionTime.RUNTIME_INIT)
197176
void generateDataSourceBeans(HibernateOrmRecorder recorder,
198177
List<PersistenceUnitDescriptorBuildItem> persistenceUnitDescriptors,
199178
ImpliedBlockingPersistenceUnitTypeBuildItem impliedBlockingPersistenceUnitType,

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
import io.quarkus.deployment.builditem.LogCategoryBuildItem;
9494
import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem;
9595
import io.quarkus.deployment.builditem.ServiceStartBuildItem;
96+
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
9697
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
9798
import io.quarkus.deployment.builditem.TransformedClassesBuildItem;
9899
import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem;
@@ -674,12 +675,13 @@ public PersistenceProviderSetUpBuildItem setupPersistenceProvider(HibernateOrmRe
674675
@Consume(SyntheticBeansRuntimeInitBuildItem.class)
675676
@Record(RUNTIME_INIT)
676677
public ServiceStartBuildItem startPersistenceUnits(HibernateOrmRecorder recorder, BeanContainerBuildItem beanContainer,
678+
ShutdownContextBuildItem shutdownContextBuildItem,
677679
List<JdbcDataSourceBuildItem> dataSourcesConfigured,
678680
JpaModelBuildItem jpaModel,
679681
List<JdbcDataSourceSchemaReadyBuildItem> schemaReadyBuildItem,
680682
List<PersistenceProviderSetUpBuildItem> persistenceProviderSetUp) throws Exception {
681683
if (hasEntities(jpaModel)) {
682-
recorder.startAllPersistenceUnits(beanContainer.getValue());
684+
recorder.startAllPersistenceUnits(beanContainer.getValue(), shutdownContextBuildItem);
683685
}
684686

685687
return new ServiceStartBuildItem("Hibernate ORM");

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import io.quarkus.hibernate.orm.runtime.proxies.PreGeneratedProxies;
3434
import io.quarkus.hibernate.orm.runtime.schema.SchemaManagementIntegrator;
3535
import io.quarkus.hibernate.orm.runtime.tenant.DataSourceTenantConnectionResolver;
36+
import io.quarkus.runtime.ShutdownContext;
3637
import io.quarkus.runtime.annotations.Recorder;
3738

3839
/**
@@ -100,8 +101,16 @@ public Supplier<JPAConfig> jpaConfigSupplier(HibernateOrmRuntimeConfig config) {
100101
return () -> new JPAConfig(config);
101102
}
102103

103-
public void startAllPersistenceUnits(BeanContainer beanContainer) {
104-
beanContainer.beanInstance(JPAConfig.class).startAll();
104+
public void startAllPersistenceUnits(BeanContainer beanContainer, ShutdownContext shutdownContext) {
105+
JPAConfig jpaConfig = beanContainer.beanInstance(JPAConfig.class);
106+
// NOTE:
107+
// - We register the shutdown task before we start any PUs,
108+
// This way we'll be able to clean up even if one of the PUs fails to start while others already did.
109+
// - The step that starts the PUs returns the ServiceStartBuildItem, this in turn ensures that
110+
// the shutdown task that triggers the ShutdownEvent will be registered after this one,
111+
// and users will have access to the "ORM stuff" in their listeners.
112+
shutdownContext.addShutdownTask(jpaConfig::shutdown);
113+
jpaConfig.startAll();
105114
}
106115

107116
public Function<SyntheticCreationalContext<SessionFactory>, SessionFactory> sessionFactorySupplier(

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

Lines changed: 13 additions & 7 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 {
@@ -129,18 +127,22 @@ public boolean getRequestScopedSessionEnabled() {
129127
return this.requestScopedSessionEnabled;
130128
}
131129

132-
public static class Destroyer implements BeanDestroyer<JPAConfig> {
133-
@Override
134-
public void destroy(JPAConfig instance, CreationalContext<JPAConfig> creationalContext, Map<String, Object> params) {
135-
for (LazyPersistenceUnit factory : instance.persistenceUnits.values()) {
130+
void shutdown() {
131+
LOGGER.trace("Starting to shut down Hibernate ORM persistence units.");
132+
for (LazyPersistenceUnit factory : this.persistenceUnits.values()) {
133+
if (factory.isStarted()) {
136134
try {
135+
LOGGER.tracef("Closing Hibernate ORM persistence unit: %s.", factory.name);
137136
factory.close();
138137
} catch (Exception e) {
139138
LOGGER.warn("Unable to close the EntityManagerFactory: " + factory, e);
140139
}
140+
} else {
141+
LOGGER.tracef("Skipping Hibernate ORM persistence unit, that failed to start: %s.", factory.name);
141142
}
142-
instance.persistenceUnits.clear();
143143
}
144+
this.persistenceUnits.clear();
145+
LOGGER.trace("Finished shutting down Hibernate ORM persistence units.");
144146
}
145147

146148
static final class LazyPersistenceUnit {
@@ -175,6 +177,10 @@ public synchronized void close() {
175177
emf.close();
176178
}
177179
}
180+
181+
boolean isStarted() {
182+
return !closed && value != null;
183+
}
178184
}
179185

180186
}

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,80 @@
1+
package io.quarkus.it.hibernate.search.orm.elasticsearch.coordination.outboxpolling;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.nio.file.Files;
6+
import java.nio.file.Paths;
7+
8+
import org.junit.jupiter.api.BeforeAll;
9+
import org.junit.jupiter.api.Tag;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.test.QuarkusDevModeTest;
14+
import io.restassured.RestAssured;
15+
16+
@Tag("devmode")
17+
public class HibernateSearchDevModeFailingOrmTest {
18+
19+
@RegisterExtension
20+
static final QuarkusDevModeTest config = new QuarkusDevModeTest()
21+
.withApplicationRoot((jar) -> jar
22+
.addClasses(HibernateSearchOutboxPollingTestResource.class, Person.class, OutboxPollingTestUtils.class)
23+
.addAsResource("application-dev-mode.properties", "application.properties"))
24+
.setLogRecordPredicate(r -> true);
25+
26+
static String APPLICATION_PROPERTIES;
27+
28+
@BeforeAll
29+
static void beforeAll() throws Exception {
30+
APPLICATION_PROPERTIES = Files
31+
.readString(
32+
Paths.get(HibernateSearchDevModeFailingOrmTest.class.getResource("/application-dev-mode.properties")
33+
.toURI()));
34+
}
35+
36+
@Test
37+
public void smoke() {
38+
RestAssured.when().put("/test/hibernate-search-outbox-polling/check-agents-running").then()
39+
.statusCode(200);
40+
41+
// now add a property that will fail the datasource:
42+
config.modifyResourceFile(
43+
"application.properties",
44+
s -> APPLICATION_PROPERTIES + """
45+
quarkus.datasource.jdbc.min-size=20
46+
""");
47+
RestAssured.when().put("/test/hibernate-search-outbox-polling/check-agents-running").then()
48+
.statusCode(500);
49+
50+
// add any change to get the shutdown of a failed app completed:
51+
config.modifyResourceFile("application.properties", s -> APPLICATION_PROPERTIES);
52+
53+
RestAssured.when().put("/test/hibernate-search-outbox-polling/check-agents-running").then()
54+
.statusCode(200);
55+
56+
// At this point we've tried starting the app 3 times: one initial, one failing, one successful live-reloads
57+
// Hence we expect the following things logged:
58+
// initial run:
59+
// - profile activated (after a successful startup)
60+
// - ORM message after a successful shutdown caused by a following live-reload (closing a PU)
61+
// first reload:
62+
// - ORM message telling us that the PU closing won't happen as the PU failed to start
63+
// second reload:
64+
// - profile activated (after a successful startup)
65+
// - no ORM shutdown message, as that will happen after the test body finishes.
66+
assertThat(config.getLogRecords()).satisfiesOnlyOnce(
67+
r -> {
68+
assertThat(r.getMessage()).contains("Closing Hibernate ORM persistence unit");
69+
assertThat(r.getParameters()).containsExactly("<default>");
70+
});
71+
assertThat(config.getLogRecords()).satisfiesOnlyOnce(
72+
r -> {
73+
assertThat(r.getMessage()).contains("Skipping Hibernate ORM persistence unit, that failed to start");
74+
assertThat(r.getParameters()).containsExactly("<default>");
75+
});
76+
assertThat(config.getLogRecords().stream()
77+
.filter(r -> r.getMessage().contains("Profile%s %s activated. %s")))
78+
.hasSize(2);
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.quarkus.it.hibernate.search.orm.elasticsearch.coordination.outboxpolling;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.nio.file.Files;
6+
import java.nio.file.Paths;
7+
8+
import org.junit.jupiter.api.BeforeAll;
9+
import org.junit.jupiter.api.Tag;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.test.QuarkusDevModeTest;
14+
import io.restassured.RestAssured;
15+
16+
@Tag("devmode")
17+
public class HibernateSearchDevModeFailingSearchTest {
18+
19+
@RegisterExtension
20+
static final QuarkusDevModeTest config = new QuarkusDevModeTest()
21+
.withApplicationRoot((jar) -> jar
22+
.addClasses(HibernateSearchOutboxPollingTestResource.class, Person.class, OutboxPollingTestUtils.class)
23+
.addAsResource("application-dev-mode.properties", "application.properties"))
24+
.setLogRecordPredicate(r -> true);
25+
26+
static String APPLICATION_PROPERTIES;
27+
28+
@BeforeAll
29+
static void beforeAll() throws Exception {
30+
APPLICATION_PROPERTIES = Files
31+
.readString(Paths
32+
.get(HibernateSearchDevModeFailingSearchTest.class.getResource("/application-dev-mode.properties")
33+
.toURI()));
34+
}
35+
36+
@Test
37+
public void smoke() {
38+
RestAssured.when().put("/test/hibernate-search-outbox-polling/check-agents-running").then()
39+
.statusCode(200);
40+
41+
// now add a property that will fail the search, but since search is started through ORM integrator.. we are actually failing ORM startup:
42+
config.modifyResourceFile(
43+
"application.properties",
44+
s -> APPLICATION_PROPERTIES.replace(
45+
"quarkus.hibernate-search-orm.elasticsearch.hosts=${elasticsearch.hosts:localhost:9200}",
46+
"quarkus.hibernate-search-orm.elasticsearch.hosts=not-a-localhost:9211"));
47+
RestAssured.when().put("/test/hibernate-search-outbox-polling/check-agents-running").then()
48+
.statusCode(500);
49+
50+
// and any change to get the shutdown of a failed app completed:
51+
config.modifyResourceFile("application.properties", s -> APPLICATION_PROPERTIES);
52+
53+
RestAssured.when().put("/test/hibernate-search-outbox-polling/check-agents-running").then()
54+
.statusCode(200);
55+
56+
// At this point we've tried starting the app 3 times: one initial, one failing, one successful live-reloads
57+
// Hence we expect the following things logged:
58+
// initial run:
59+
// - profile activated (after a successful startup)
60+
// - ORM message after a successful shutdown caused by a following live-reload (closing a PU)
61+
// first reload:
62+
// - ORM message telling us that the PU closing won't happen as the PU failed to start
63+
// second reload:
64+
// - profile activated (after a successful startup)
65+
// - no ORM shutdown message, as that will happen after the test body finishes.
66+
assertThat(config.getLogRecords()).satisfiesOnlyOnce(
67+
r -> {
68+
assertThat(r.getMessage()).contains("Closing Hibernate ORM persistence unit");
69+
assertThat(r.getParameters()).containsExactly("<default>");
70+
});
71+
assertThat(config.getLogRecords()).satisfiesOnlyOnce(
72+
r -> {
73+
assertThat(r.getMessage()).contains("Skipping Hibernate ORM persistence unit, that failed to start");
74+
assertThat(r.getParameters()).containsExactly("<default>");
75+
});
76+
assertThat(config.getLogRecords().stream()
77+
.filter(r -> r.getMessage().contains("Profile%s %s activated. %s")))
78+
.hasSize(2);
79+
}
80+
}

0 commit comments

Comments
 (0)