quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.53k stars 2.61k forks source link

Exception on application shutdown with quarkus-hibernate-search-orm-coordination-outbox-polling #34547

Open yrodiere opened 1 year ago

yrodiere commented 1 year ago

Describe the bug

When using the extension quarkus-hibernate-search-orm-coordination-outbox-polling and stopping the application in dev mode, exception gets thrown, caused by Hibernate Search not being able to create a connection to the database (it needs to in order to re-register agents).

Expected behavior

No exception.

Actual behavior

2023-07-05 13:57:12,576 ERROR [org.hib.sea.eng.rep.spi.RootFailureCollector] (Shutdown thread) HSEARCH000521: Hibernate Search encountered a failure during %1$s; continuing for now to list all problems, but the process will ultimately be aborted.
Context: %2$s
Failure: [Error Occurred After Shutdown]: org.hibernate.exception.GenericJDBCException: Unable to acquire JDBC Connection [This pool is closed and does not handle any more connections!] [n/a]
        at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:61)
        at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:108)
        at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:94)
        at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.acquireConnectionIfNeeded(LogicalConnectionManagedImpl.java:116)
        at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getPhysicalConnection(LogicalConnectionManagedImpl.java:143)
        at org.hibernate.engine.jdbc.internal.StatementPreparerImpl.connection(StatementPreparerImpl.java:51)
        at org.hibernate.engine.jdbc.internal.StatementPreparerImpl$1.doPrepare(StatementPreparerImpl.java:91)
        at org.hibernate.engine.jdbc.internal.StatementPreparerImpl$StatementPreparationTemplate.prepareStatement(StatementPreparerImpl.java:177)
        at org.hibernate.engine.jdbc.internal.StatementPreparerImpl.prepareStatement(StatementPreparerImpl.java:76)
        at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.lambda$list$0(JdbcSelectExecutorStandardImpl.java:102)
        at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.executeQuery(DeferredResultSetAccess.java:226)
        at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.getResultSet(DeferredResultSetAccess.java:163)
        at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.advanceNext(JdbcValuesResultSetImpl.java:254)
        at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.processNext(JdbcValuesResultSetImpl.java:134)
        at org.hibernate.sql.results.jdbc.internal.AbstractJdbcValues.next(AbstractJdbcValues.java:19)
        at org.hibernate.sql.results.internal.RowProcessingStateStandardImpl.next(RowProcessingStateStandardImpl.java:66)
        at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:178)
        at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:33)
        at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:361)
        at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:168)
        at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.list(JdbcSelectExecutorStandardImpl.java:93)
        at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:31)
        at org.hibernate.loader.ast.internal.LoaderHelper.loadByArrayParameter(LoaderHelper.java:215)
        at org.hibernate.loader.ast.internal.EntityBatchLoaderArrayParam.initializeEntities(EntityBatchLoaderArrayParam.java:141)
        at org.hibernate.loader.ast.internal.EntityBatchLoaderArrayParam.load(EntityBatchLoaderArrayParam.java:98)
        at org.hibernate.loader.ast.internal.EntityBatchLoaderArrayParam.load(EntityBatchLoaderArrayParam.java:170)
        at org.hibernate.persister.entity.AbstractEntityPersister.doLoad(AbstractEntityPersister.java:3481)
        at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:3471)
        at org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:581)
        at org.hibernate.event.internal.DefaultLoadEventListener.loadFromCacheOrDatasource(DefaultLoadEventListener.java:567)
        at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:536)
        at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:529)
        at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:202)
        at org.hibernate.event.internal.DefaultLoadEventListener.loadWithRegularProxy(DefaultLoadEventListener.java:282)
        at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:237)
        at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:106)
        at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:78)
        at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:138)
        at org.hibernate.internal.SessionImpl.fireLoadNoChecks(SessionImpl.java:1231)
        at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1219)
        at org.hibernate.loader.internal.IdentifierLoadAccessImpl.doLoad(IdentifierLoadAccessImpl.java:194)
        at org.hibernate.loader.internal.IdentifierLoadAccessImpl.lambda$load$1(IdentifierLoadAccessImpl.java:160)
        at org.hibernate.loader.internal.IdentifierLoadAccessImpl.perform(IdentifierLoadAccessImpl.java:107)
        at org.hibernate.loader.internal.IdentifierLoadAccessImpl.load(IdentifierLoadAccessImpl.java:160)
        at org.hibernate.internal.SessionImpl.find(SessionImpl.java:2406)
        at org.hibernate.internal.SessionImpl.find(SessionImpl.java:2372)
        at org.hibernate.search.mapper.orm.coordination.outboxpolling.cluster.impl.DefaultAgentRepository.find(DefaultAgentRepository.java:31)
        at org.hibernate.search.mapper.orm.coordination.outboxpolling.cluster.impl.AgentPersister.leaveCluster(AgentPersister.java:61)
        at org.hibernate.search.mapper.orm.coordination.outboxpolling.event.impl.AbstractAgentClusterLink.leaveCluster(AbstractAgentClusterLink.java:138)
        at org.hibernate.search.mapper.orm.coordination.outboxpolling.event.impl.AgentClusterLinkContextProvider.lambda$inTransaction$0(AgentClusterLinkContextProvider.java:31)
        at org.hibernate.search.mapper.orm.coordination.outboxpolling.event.impl.AgentClusterLinkContextProvider.inTransaction(AgentClusterLinkContextProvider.java:42)
        at org.hibernate.search.mapper.orm.coordination.outboxpolling.event.impl.AgentClusterLinkContextProvider.inTransaction(AgentClusterLinkContextProvider.java:30)
        at org.hibernate.search.mapper.orm.coordination.outboxpolling.event.impl.OutboxPollingEventProcessor.leaveCluster(OutboxPollingEventProcessor.java:216)
        at org.hibernate.search.util.common.impl.AbstractCloser.push(AbstractCloser.java:53)
        at org.hibernate.search.util.common.impl.AbstractCloser.push(AbstractCloser.java:33)
        at org.hibernate.search.mapper.orm.coordination.outboxpolling.event.impl.OutboxPollingEventProcessor.stop(OutboxPollingEventProcessor.java:211)
        at org.hibernate.search.util.common.impl.AbstractCloser.push(AbstractCloser.java:53)
        at org.hibernate.search.util.common.impl.AbstractCloser.pushAll(AbstractCloser.java:92)
        at org.hibernate.search.util.common.impl.AbstractCloser.pushAll(AbstractCloser.java:73)
        at org.hibernate.search.mapper.orm.coordination.outboxpolling.impl.OutboxPollingCoordinationStrategy.stop(OutboxPollingCoordinationStrategy.java:197)
        at org.hibernate.search.util.common.impl.AbstractCloser.push(AbstractCloser.java:53)
        at org.hibernate.search.mapper.orm.mapping.impl.HibernateOrmMapping.doStop(HibernateOrmMapping.java:196)
        at org.hibernate.search.util.common.impl.AbstractCloser.push(AbstractCloser.java:53)
        at org.hibernate.search.util.common.impl.AbstractCloser.push(AbstractCloser.java:33)
        at org.hibernate.search.mapper.pojo.mapping.spi.AbstractPojoMappingImplementor.stop(AbstractPojoMappingImplementor.java:68)
        at org.hibernate.search.engine.common.impl.SearchIntegrationImpl.lambda$close$2(SearchIntegrationImpl.java:132)
        at org.hibernate.search.engine.common.impl.SearchIntegrationImpl.stopAllSafely(SearchIntegrationImpl.java:178)
        at org.hibernate.search.engine.common.impl.SearchIntegrationImpl.close(SearchIntegrationImpl.java:131)
        at org.hibernate.search.mapper.orm.mapping.impl.HibernateSearchContextProviderService.close(HibernateSearchContextProviderService.java:36)
        at org.hibernate.search.mapper.orm.bootstrap.impl.HibernateOrmIntegrationBooterImpl.lambda$orchestrateBootAndShutdown$5(HibernateOrmIntegrationBooterImpl.java:156)
        at java.base/java.util.concurrent.CompletableFuture.biAccept(CompletableFuture.java:1386)
        at java.base/java.util.concurrent.CompletableFuture$BiAccept.tryFire(CompletableFuture.java:1355)
        at java.base/java.util.concurrent.CompletableFuture$CoCompletion.tryFire(CompletableFuture.java:1219)
        at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
        at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2147)
        at org.hibernate.search.mapper.orm.bootstrap.impl.HibernateSearchSessionFactoryObserver.sessionFactoryClosing(HibernateSearchSessionFactoryObserver.java:50)
        at org.hibernate.internal.SessionFactoryObserverChain.sessionFactoryClosing(SessionFactoryObserverChain.java:48)
        at org.hibernate.internal.SessionFactoryImpl.close(SessionFactoryImpl.java:869)
        at io.quarkus.hibernate.orm.runtime.JPAConfig$LazyPersistenceUnit.close(JPAConfig.java:168)
        at io.quarkus.hibernate.orm.runtime.JPAConfig$Destroyer.destroy(JPAConfig.java:130)
        at io.quarkus.hibernate.orm.runtime.JPAConfig$Destroyer.destroy(JPAConfig.java:125)
        at io.quarkus.hibernate.orm.runtime.JPAConfig_b44400079827d7cae558c390d8d94cadbd711498_Synthetic_Bean.doDestroy(Unknown Source)
        at io.quarkus.hibernate.orm.runtime.JPAConfig_b44400079827d7cae558c390d8d94cadbd711498_Synthetic_Bean.destroy(Unknown Source)
        at io.quarkus.hibernate.orm.runtime.JPAConfig_b44400079827d7cae558c390d8d94cadbd711498_Synthetic_Bean.destroy(Unknown Source)
        at io.quarkus.arc.impl.AbstractInstanceHandle.destroyInternal(AbstractInstanceHandle.java:82)
        at io.quarkus.arc.impl.ContextInstanceHandleImpl.destroy(ContextInstanceHandleImpl.java:21)
        at io.quarkus.arc.impl.AbstractSharedContext.destroy(AbstractSharedContext.java:96)
        at io.quarkus.arc.impl.ArcContainerImpl.shutdown(ArcContainerImpl.java:468)
        at io.quarkus.arc.Arc.shutdown(Arc.java:66)
        at io.quarkus.arc.runtime.ArcRecorder$1.run(ArcRecorder.java:53)
        at io.quarkus.runtime.StartupContext.runAllInReverseOrder(StartupContext.java:84)
        at io.quarkus.runtime.StartupContext.close(StartupContext.java:73)
        at io.quarkus.runner.ApplicationImpl.doStop(Unknown Source)
        at io.quarkus.runtime.Application.stop(Application.java:208)
        at io.quarkus.runtime.Application.stop(Application.java:155)
        at io.quarkus.runtime.ApplicationLifecycleManager$ShutdownHookThread.run(ApplicationLifecycleManager.java:436)
Caused by: java.sql.SQLException: This pool is closed and does not handle any more connections!
        at io.agroal.pool.ConnectionPool.beforeAcquire(ConnectionPool.java:209)
        at io.agroal.pool.ConnectionPool.getConnection(ConnectionPool.java:235)
        at io.agroal.pool.DataSource.getConnection(DataSource.java:86)
        at io.quarkus.hibernate.orm.runtime.customized.QuarkusConnectionProvider.getConnection(QuarkusConnectionProvider.java:23)
        at org.hibernate.internal.NonContextualJdbcConnectionAccess.obtainConnection(NonContextualJdbcConnectionAccess.java:38)
        at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.acquireConnectionIfNeeded(LogicalConnectionManagedImpl.java:113)
        ... 92 more

2023-07-05 13:57:12,581 ERROR [org.hib.sea.map.orm.boo.imp.HibernateOrmIntegrationBooterImpl] (Shutdown thread) HSEARCH800035: Unable to shut down Hibernate Search: %1$s [Error Occurred After Shutdown]: org.hibernate.search.util.common.SearchException: HSEARCH000520: Hibernate Search encountered failures during shutdown. Failures:

    Hibernate ORM mapping: 
        failures: 
          - Unable to acquire JDBC Connection [This pool is closed and does not handle any more connections!] [n/a]
        at org.hibernate.search.engine.reporting.spi.RootFailureCollector.checkNoFailure(RootFailureCollector.java:53)
        at org.hibernate.search.engine.common.impl.SearchIntegrationImpl.close(SearchIntegrationImpl.java:168)
        at org.hibernate.search.mapper.orm.mapping.impl.HibernateSearchContextProviderService.close(HibernateSearchContextProviderService.java:36)
        at org.hibernate.search.mapper.orm.bootstrap.impl.HibernateOrmIntegrationBooterImpl.lambda$orchestrateBootAndShutdown$5(HibernateOrmIntegrationBooterImpl.java:156)
        at java.base/java.util.concurrent.CompletableFuture.biAccept(CompletableFuture.java:1386)
        at java.base/java.util.concurrent.CompletableFuture$BiAccept.tryFire(CompletableFuture.java:1355)
        at java.base/java.util.concurrent.CompletableFuture$CoCompletion.tryFire(CompletableFuture.java:1219)
        at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
        at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2147)
        at org.hibernate.search.mapper.orm.bootstrap.impl.HibernateSearchSessionFactoryObserver.sessionFactoryClosing(HibernateSearchSessionFactoryObserver.java:50)
        at org.hibernate.internal.SessionFactoryObserverChain.sessionFactoryClosing(SessionFactoryObserverChain.java:48)
        at org.hibernate.internal.SessionFactoryImpl.close(SessionFactoryImpl.java:869)
        at io.quarkus.hibernate.orm.runtime.JPAConfig$LazyPersistenceUnit.close(JPAConfig.java:168)
        at io.quarkus.hibernate.orm.runtime.JPAConfig$Destroyer.destroy(JPAConfig.java:130)
        at io.quarkus.hibernate.orm.runtime.JPAConfig$Destroyer.destroy(JPAConfig.java:125)
        at io.quarkus.hibernate.orm.runtime.JPAConfig_b44400079827d7cae558c390d8d94cadbd711498_Synthetic_Bean.doDestroy(Unknown Source)
        at io.quarkus.hibernate.orm.runtime.JPAConfig_b44400079827d7cae558c390d8d94cadbd711498_Synthetic_Bean.destroy(Unknown Source)
        at io.quarkus.hibernate.orm.runtime.JPAConfig_b44400079827d7cae558c390d8d94cadbd711498_Synthetic_Bean.destroy(Unknown Source)
        at io.quarkus.arc.impl.AbstractInstanceHandle.destroyInternal(AbstractInstanceHandle.java:82)
        at io.quarkus.arc.impl.ContextInstanceHandleImpl.destroy(ContextInstanceHandleImpl.java:21)
        at io.quarkus.arc.impl.AbstractSharedContext.destroy(AbstractSharedContext.java:96)
        at io.quarkus.arc.impl.ArcContainerImpl.shutdown(ArcContainerImpl.java:468)
        at io.quarkus.arc.Arc.shutdown(Arc.java:66)
        at io.quarkus.arc.runtime.ArcRecorder$1.run(ArcRecorder.java:53)
        at io.quarkus.runtime.StartupContext.runAllInReverseOrder(StartupContext.java:84)
        at io.quarkus.runtime.StartupContext.close(StartupContext.java:73)
        at io.quarkus.runner.ApplicationImpl.doStop(Unknown Source)
        at io.quarkus.runtime.Application.stop(Application.java:208)
        at io.quarkus.runtime.Application.stop(Application.java:155)

How to Reproduce?

No response

Output of uname -a or ver

No response

Output of java -version

No response

GraalVM version (if different from Java)

No response

Quarkus version or git rev

No response

Build tool (ie. output of mvnw --version or gradlew --version)

No response

Additional information

We should probably shut down Agroal after Hibernate ORM to avoid this exception.

quarkus-bot[bot] commented 1 year ago

/cc @gsmet (hibernate-search)

marko-bekhta commented 3 weeks ago

Hey @yrodiere I've tried looking into this one, and from what it seems this is related to CDI more than to outbox polling and Hibernate Search. And it also looks like a more general problem. As you've suspected io.quarkus.agroal.runtime.DataSources sometimes gets closed sooner than the io.quarkus.hibernate.orm.runtime.JPAConfig leading to that exception.

We are adding an injection point: https://github.com/quarkusio/quarkus/blob/66429bddeacc3ea8cd8e40a03b9906d283553d92/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java#L146-L160

and based on the comment, we expect that the JPAConfig will get closed before the DataSources. I went through the Arc code a bit and didn't find that this would be "enforced" (but I also didn't want to spend too much time looking so maybe I've missed it 🙈)

To check this assumption, I've created two simple app-scoped beans (that is if my understanding was correct):

@ApplicationScoped
public class MyService1 {
    @Inject
    MyService2 service2;

    public String string() {
        return service2.string();
    }
    @PreDestroy
    public void destroy() {
        System.err.println("===========" + this.getClass().getName() + " destroy");
    }
}
@ApplicationScoped
public class MyService2 {

    public String string() {
        return "string2";
    }

    @PreDestroy
    public void destroy() {
        System.err.println("===========" + this.getClass().getName() + " destroy");
    }
}

and from time to time, MyService1 is closed before MyService2... But if I understand the assumption correctly, we'd expect that MyService2 is always closed before MyService1

I see that here, where the beans are destroyed (in the case of the JPAConfig and DataSources we are talking about the singleton context):

https://github.com/quarkusio/quarkus/blob/66429bddeacc3ea8cd8e40a03b9906d283553d92/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractSharedContext.java#L89-L104

we get a set of beans to destroy, and if we are "unlucky", DataSources are getting ahead of the JPAConfig in the iteration, and things fail: E.g.:

Destroy order io.quarkus.arc.impl.SingletonContext@1c231768:
class io.quarkus.agroal.runtime.DataSources                     DeclaringBean: null
class io.quarkus.narayana.jta.runtime.NarayanaJtaProducers                  DeclaringBean: CLASS bean [class=io.quarkus.narayana.jta.runtime.NarayanaJtaProducers, id=ol81PNK5K-jPtG8Wb8uxqBBB9cM]
class io.quarkus.arc.runtime.ConfigStaticInitValues                 DeclaringBean: null
class io.quarkus.it.hibernate.search.orm.elasticsearch.coordination.outboxpolling.HibernateSearchOutboxPollingTestResource               DeclaringBean: null
class io.quarkus.datasource.runtime.DataSourceSupport                   DeclaringBean: null
class io.quarkus.hibernate.orm.runtime.cdi.QuarkusArcBeanContainer                  DeclaringBean: null
class io.quarkus.hibernate.orm.runtime.JPAConfig                    DeclaringBean: null
class io.quarkus.arc.runtime.LoggerProducer                 DeclaringBean: null
class io.quarkus.narayana.jta.runtime.NarayanaJtaProducers                  DeclaringBean: CLASS bean [class=io.quarkus.narayana.jta.runtime.NarayanaJtaProducers, id=ol81PNK5K-jPtG8Wb8uxqBBB9cM]

Neither JPAConfig nor DataSources have a DeclaringBean, hence DataSources are not destroyed in the first loop (with the // Destroy the producers first comment) and both are destroyed in the second one in the "random" order 😕

yrodiere commented 3 weeks ago

Thanks for looking into this @marko-bekhta .

and based on the comment, we expect that the JPAConfig will get closed before the DataSources. I went through the Arc code a bit and didn't find that this would be "enforced" (but I also didn't want to spend too much time looking so maybe I've missed it 🙈)

@mkouba @Ladicek can you confirm that Arc beans are closed in an order that is consistent with the dependency graph -- at least when there is no cycle? If not, can you please advise on the best way to ensure Arc beans get closed in a specific order?

FWIW, if closing order is not guaranteed, other issue may have the same root cause; see for example https://github.com/quarkusio/quarkus/issues/36265#issuecomment-1750632017

Ladicek commented 3 weeks ago

There is no order in which beans are destroyed, and I believe there is no way to affect that.

I think @mkouba mentioned this several times: @PreDestroy listeners should be "local", that is, they should not expect other beans to exist. I believe observing the ShutdownEvent is a better choice in this regard -- when the observers run, it is guaranteed that the contexts still exist, and you can define observer priority (ordering).

marko-bekhta commented 3 weeks ago

Thanks @Ladicek I'll take a look at how we can update things then 🥲

yrodiere commented 3 weeks ago

Thanks.

Thanks @Ladicek I'll take a look at how we can update things then 🥲

@marko-bekhta I'm currently making changes in this area in #41929 ; see in particular https://github.com/quarkusio/quarkus/pull/41929/files#diff-ae22f0b05b345fab3d41e46504afedf53b79b6b3b4a12411bde2c8ae60c2c234L445-L453 and https://github.com/quarkusio/quarkus/pull/41929/files#diff-bc8502db2a346fd80de961c88ff8274ddfa740cdc9b462d33021d25cce7c452eR292.

You may want to wait until I'm done before you work on this? I'll probably be less likely to lead to conflicts. Can't promise when I'll be done though :/

I believe observing the ShutdownEvent is a better choice in this regard -- when the observers run, it is guaranteed that the contexts still exist, and you can define observer priority (ordering).

So I guess we could keep the existing @PreDestroy thing for Agroal, and shut down Hibernate ORM/Reactive with a ShutdownEvent observer? Which would guarantee that Hibernate ORM/Reactive shut down before Datasources.

For #36265 we basically want Agroal to shut down after Vertx... that probably means the Vertx extension needs to use ShutdownEvent? Maybe it already does, IDK.

marko-bekhta commented 3 weeks ago

yeah, sure, I can wait 😃 no worries.

mkouba commented 1 week ago

There is no order in which beans are destroyed, and I believe there is no way to affect that.

I think @mkouba mentioned this several times: @PreDestroy listeners should be "local", that is, they should not expect other beans to exist. I believe observing the ShutdownEvent is a better choice in this regard -- when the observers run, it is guaranteed that the contexts still exist, and you can define observer priority (ordering).

Correct. For @ApplicationScoped you can also consider an observer of @BeforeDestroyed(ApplicationScoped.class).