diff --git a/docs/src/main/asciidoc/cdi.adoc b/docs/src/main/asciidoc/cdi.adoc index 7d393c12f7b38..0cdc9da9d2fb3 100644 --- a/docs/src/main/asciidoc/cdi.adoc +++ b/docs/src/main/asciidoc/cdi.adoc @@ -253,7 +253,7 @@ Client proxies allow for: * Circular dependencies in the dependency graph. Having circular dependencies is often an indication that a redesign should be considered, but sometimes it's inevitable. * In rare cases it's practical to destroy the beans manually. A direct injected reference would lead to a stale bean instance. - +[[ok-you-said-that-there-are-several-kinds-of-beans]] == OK. You said that there are several kinds of beans? Yes. In general, we distinguish: diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index 8d68f8462abdf..286d6f5601177 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -329,6 +329,7 @@ By default, Quarkus provides three profiles, that activate automatically in cert * *test* - Activated when running tests * *prod* - The default profile when not running in development or test mode +[[custom-profiles]] === Custom Profiles It is also possible to create additional profiles and activate them with the `quarkus.profile` configuration property. A diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index 81fbb90de0eba..d2ebd461f7b4d 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -405,6 +405,99 @@ AgroalDataSource usersDataSource; AgroalDataSource inventoryDataSource; ---- +[[datasource-active]] +=== Activate/deactivate datasources + +If a datasource is configured at build time, +by default it is active at runtime, +that is Quarkus will start the corresponding JDBC connection pool or reactive client on application startup. + +To deactivate a datasource at runtime, set `quarkus.datasource[.optional name].active` to `false`. +Then Quarkus will not start the corresponding JDBC connection pool or reactive client on application startup. +Any attempt to use the corresponding datasource at runtime will fail with a clear error message. + +This is in particular useful when you want an application to be able +to use one of a pre-determined set of datasources at runtime. + +[WARNING] +==== +If another Quarkus extension relies on an inactive datasource, +that extension might fail to start. + +In such case, you will need to deactivate that other extension too. +For example see xref:hibernate-orm.adoc#persistence-unit-active[here for Hibernate ORM]. +==== + +For example, with the following configuration: + +[source,properties] +---- +quarkus.datasource."pg".db-kind=postgres +quarkus.datasource."pg".active=false +quarkus.datasource."pg".jdbc.url=jdbc:postgresql:///your_database + +quarkus.datasource."oracle".db-kind=oracle +quarkus.datasource."oracle".active=false +quarkus.datasource."oracle".jdbc.url=jdbc:oracle:///your_database +---- + +Setting `quarkus.datasource."pg".active=true` xref:config-reference.adoc#configuration-sources[at runtime] +will make only the PostgreSQL datasource available, +and setting `quarkus.datasource."oracle".active=true` at runtime +will make only the Oracle datasource available. + +[TIP] +==== +xref:config-reference.adoc#custom-profiles[Custom configuration profiles] can help simplify such a setup. +By appending the following profile-specific configuration to the one above, +you can select a persistence unit/datasource at runtime simply by +xref:config-reference.adoc#multiple-profiles[setting `quarkus.profile`]: +`quarkus.profile=prod,pg` or `quarkus.profile=prod,oracle`. + +[source,properties] +---- +%pg.quarkus.hibernate-orm."pg".active=true +%pg.quarkus.datasource."pg".active=true +# Add any pg-related runtime configuration here, prefixed with "%pg." + +%oracle.quarkus.hibernate-orm."oracle".active=true +%oracle.quarkus.datasource."oracle".active=true +# Add any pg-related runtime configuration here, prefixed with "%pg." +---- +==== + +[TIP] +==== +It can also be useful to define a xref:cdi.adoc#ok-you-said-that-there-are-several-kinds-of-beans[CDI bean producer] redirecting to the currently active datasource, +like this: + +[source,java,indent=0] +---- +public class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @DataSource("pg") + AgroalDataSource pgDataSourceBean; + + @Inject + @DataSource("oracle") + AgroalDataSource oracleDataSourceBean; + + @Produces + @ApplicationScoped + public AgroalDataSource dataSource() { + if (dataSourceSupport.getInactiveNames().contains("pg")) { + return oracleDataSourceBean; + } else { + return pgDataSourceBean; + } + } +} +---- +==== + == Datasource integrations === Datasource health check diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc index 3439cbae1e469..ff704928e292f 100644 --- a/docs/src/main/asciidoc/hibernate-orm.adoc +++ b/docs/src/main/asciidoc/hibernate-orm.adoc @@ -464,6 +464,98 @@ You can inject the `EntityManagerFactory` of a named persistence unit using the EntityManagerFactory entityManagerFactory; ---- +[[persistence-unit-active]] +=== Activate/deactivate persistence units + +If a persistence unit is configured at build time, +by default it is active at runtime, +that is Quarkus will start the corresponding Hibernate ORM `SessionFactory` on application startup. + +To deactivate a persistence unit at runtime, set `quarkus.hibernate-orm[.optional name].active` to `false`. +Then Quarkus will not start the corresponding Hibernate ORM `SessionFactory` on application startup. +Any attempt to use the corresponding persistence unit at runtime will fail with a clear error message. + +This is in particular useful when you want an application to be able +to xref:datasource.adoc#datasource-active[use one of a pre-determined set of datasources at runtime]. + +For example, with the following configuration: + +[source,properties] +---- +quarkus.hibernate-orm."pg".packages=org.acme.model.shared +quarkus.hibernate-orm."pg".datasource=pg +quarkus.hibernate-orm."pg".database.generation=drop-and-create +quarkus.hibernate-orm."pg".active=false +quarkus.datasource."pg".db-kind=h2 +quarkus.datasource."pg".active=false +quarkus.datasource."pg".jdbc.url=jdbc:postgresql:///your_database + +quarkus.hibernate-orm."oracle".packages=org.acme.model.shared +quarkus.hibernate-orm."oracle".datasource=oracle +quarkus.hibernate-orm."oracle".database.generation=drop-and-create +quarkus.hibernate-orm."oracle".active=false +quarkus.datasource."oracle".db-kind=oracle +quarkus.datasource."oracle".active=false +quarkus.datasource."oracle".jdbc.url=jdbc:oracle:///your_database +---- + +xref:config-reference.adoc#configuration-sources[Setting] `quarkus.hibernate-orm."pg".active=true` and `quarkus.datasource."pg".active=true` at runtime +will make only the PostgreSQL persistence unit and datasource available, +and setting `quarkus.hibernate-orm."oracle".active=true` and `quarkus.datasource."oracle".active=true` at runtime +will make only the Oracle persistence unit and datasource available. + +[TIP] +==== +xref:config-reference.adoc#custom-profiles[Custom configuration profiles] can help simplify such a setup. +By appending the following profile-specific configuration to the one above, +you can select a persistence unit/datasource at runtime simply by +xref:config-reference.adoc#multiple-profiles[setting `quarkus.profile`]: +`quarkus.profile=prod,pg` or `quarkus.profile=prod,oracle`. + +[source,properties] +---- +%pg.quarkus.hibernate-orm."pg".active=true +%pg.quarkus.datasource."pg".active=true +# Add any pg-related runtime configuration here, prefixed with "%pg." + +%oracle.quarkus.hibernate-orm."oracle".active=true +%oracle.quarkus.datasource."oracle".active=true +# Add any pg-related runtime configuration here, prefixed with "%pg." +---- +==== + +[TIP] +==== +It can also be useful to define a xref:cdi.adoc#ok-you-said-that-there-are-several-kinds-of-beans[CDI bean producer] redirecting to the currently active persistence unit, +like this: + +[source,java,indent=0] +---- +public class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @PersistenceUnit("pg") + Session pgSessionBean; + + @Inject + @PersistenceUnit("oracle") + Session oracleSessionBean; + + @Produces + @ApplicationScoped + public Session session() { + if (dataSourceSupport.getInactiveNames().contains("pg")) { + return oracleSessionBean; + } else { + return pgSessionBean; + } + } +} +---- +==== + [[persistence-xml]] == Setting up and configuring Hibernate ORM with a `persistence.xml` diff --git a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java index b52cb438b06dc..740b255164904 100644 --- a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java +++ b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java @@ -24,10 +24,10 @@ import io.agroal.api.AgroalDataSource; import io.agroal.api.AgroalPoolInterceptor; import io.quarkus.agroal.DataSource; +import io.quarkus.agroal.runtime.AgroalDataSourceSupport; import io.quarkus.agroal.runtime.AgroalDataSourcesInitializer; import io.quarkus.agroal.runtime.AgroalRecorder; import io.quarkus.agroal.runtime.DataSourceJdbcBuildTimeConfig; -import io.quarkus.agroal.runtime.DataSourceSupport; import io.quarkus.agroal.runtime.DataSources; import io.quarkus.agroal.runtime.DataSourcesJdbcBuildTimeConfig; import io.quarkus.agroal.runtime.JdbcDriver; @@ -202,20 +202,20 @@ private static void validateBuildTimeConfig(AggregatedDataSourceBuildTimeConfigB } } - private DataSourceSupport getDataSourceSupport( + private AgroalDataSourceSupport getDataSourceSupport( List aggregatedBuildTimeConfigBuildItems, SslNativeConfigBuildItem sslNativeConfig, Capabilities capabilities) { - Map dataSourceSupportEntries = new HashMap<>(); + Map dataSourceSupportEntries = new HashMap<>(); for (AggregatedDataSourceBuildTimeConfigBuildItem aggregatedDataSourceBuildTimeConfig : aggregatedBuildTimeConfigBuildItems) { String dataSourceName = aggregatedDataSourceBuildTimeConfig.getName(); dataSourceSupportEntries.put(dataSourceName, - new DataSourceSupport.Entry(dataSourceName, aggregatedDataSourceBuildTimeConfig.getDbKind(), + new AgroalDataSourceSupport.Entry(dataSourceName, aggregatedDataSourceBuildTimeConfig.getDbKind(), aggregatedDataSourceBuildTimeConfig.getDataSourceConfig().dbVersion(), aggregatedDataSourceBuildTimeConfig.getResolvedDriverClass(), aggregatedDataSourceBuildTimeConfig.isDefault())); } - return new DataSourceSupport(sslNativeConfig.isExplicitlyDisabled(), + return new AgroalDataSourceSupport(sslNativeConfig.isExplicitlyDisabled(), capabilities.isPresent(Capability.METRICS), dataSourceSupportEntries); } @@ -247,10 +247,11 @@ void generateDataSourceSupportBean(AgroalRecorder recorder, unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(AgroalPoolInterceptor.class)); // create the DataSourceSupport bean that DataSourceProducer uses as a dependency - DataSourceSupport dataSourceSupport = getDataSourceSupport(aggregatedBuildTimeConfigBuildItems, sslNativeConfig, + AgroalDataSourceSupport agroalDataSourceSupport = getDataSourceSupport(aggregatedBuildTimeConfigBuildItems, + sslNativeConfig, capabilities); - syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem.configure(DataSourceSupport.class) - .supplier(recorder.dataSourceSupportSupplier(dataSourceSupport)) + syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem.configure(AgroalDataSourceSupport.class) + .supplier(recorder.dataSourceSupportSupplier(agroalDataSourceSupport)) .unremovable() .done()); } diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/AgroalMetricsConfigActiveFalseTest.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/AgroalMetricsConfigActiveFalseTest.java new file mode 100644 index 0000000000000..b680914997d6a --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/AgroalMetricsConfigActiveFalseTest.java @@ -0,0 +1,51 @@ +package io.quarkus.agroal.test; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.Gauge; +import org.eclipse.microprofile.metrics.MetricID; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.Tag; +import org.eclipse.microprofile.metrics.annotation.RegistryType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class AgroalMetricsConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-metrics-enabled.properties") + .overrideConfigKey("quarkus.datasource.active", "false") + .overrideConfigKey("quarkus.datasource.ds1.active", "false"); + + @Inject + @RegistryType(type = MetricRegistry.Type.VENDOR) + MetricRegistry registry; + + @Test + public void testMetricsOfDefaultDS() { + Counter acquireCount = registry.getCounters() + .get(new MetricID("agroal.acquire.count", new Tag("datasource", "default"))); + Gauge maxUsed = registry.getGauges() + .get(new MetricID("agroal.max.used.count", new Tag("datasource", "default"))); + + Assertions.assertNull(acquireCount, "Agroal metrics should not be registered for deactivated datasources eagerly"); + Assertions.assertNull(maxUsed, "Agroal metrics should not be registered for deactivated datasources eagerly"); + } + + @Test + public void testMetricsOfDs1() { + Counter acquireCount = registry.getCounters().get(new MetricID("agroal.acquire.count", + new Tag("datasource", "ds1"))); + Gauge maxUsed = registry.getGauges().get(new MetricID("agroal.max.used.count", + new Tag("datasource", "ds1"))); + + Assertions.assertNull(acquireCount, "Agroal metrics should not be registered for deactivated datasources eagerly"); + Assertions.assertNull(maxUsed, "Agroal metrics should not be registered for deactivated datasources eagerly"); + } + +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseDefaultDatasourceTest.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..4e71a02d8503e --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,91 @@ +package io.quarkus.agroal.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.CreationException; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class ConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + MyBean myBean; + + @Test + public void dataSource() { + DataSource ds = Arc.container().instance(DataSource.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(ds).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> ds.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void agroalDataSource() { + AgroalDataSource ds = Arc.container().instance(AgroalDataSource.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(ds).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> ds.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.useDatasource()) + .isInstanceOf(CreationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + DataSource ds; + + public void useDatasource() throws SQLException { + ds.getConnection(); + } + } +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseNamedDatasourceTest.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..812aeadd02514 --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,96 @@ +package io.quarkus.agroal.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.CreationException; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class ConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.users.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2"); + + @Inject + MyBean myBean; + + @Test + public void dataSource() { + DataSource ds = Arc.container().instance(DataSource.class, + new io.quarkus.agroal.DataSource.DataSourceLiteral("users")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(ds).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> ds.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void agroalDataSource() { + DataSource ds = Arc.container().instance(DataSource.class, + new io.quarkus.agroal.DataSource.DataSourceLiteral("users")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(ds).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> ds.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.useDatasource()) + .isInstanceOf(CreationException.class) + .hasMessageContainingAll("Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @io.quarkus.agroal.DataSource("users") + DataSource ds; + + public void useDatasource() throws SQLException { + ds.getConnection(); + } + } +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/DataSourceHealthCheckConfigActiveFalseTest.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/DataSourceHealthCheckConfigActiveFalseTest.java new file mode 100644 index 0000000000000..a6a4fe8f5c03e --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/DataSourceHealthCheckConfigActiveFalseTest.java @@ -0,0 +1,30 @@ +package io.quarkus.agroal.test; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class DataSourceHealthCheckConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .withConfigurationResource("application-default-datasource.properties") + .overrideConfigKey("quarkus.datasource.health.enabled", "true") + // this data source is broken, but will be deactivated, + // so the overall check should pass + .overrideConfigKey("quarkus.datasource.brokenDS.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.brokenDS.jdbc.url", "BROKEN") + .overrideConfigKey("quarkus.datasource.brokenDS.active", "false"); + + @Test + public void testDataSourceHealthCheckExclusion() { + RestAssured.when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } + +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS1Test.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS1Test.java new file mode 100644 index 0000000000000..208241cef1d8c --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS1Test.java @@ -0,0 +1,98 @@ +package io.quarkus.agroal.test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.agroal.DataSource; +import io.quarkus.datasource.runtime.DataSourceSupport; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests a use case where multiple datasources are defined at build time, + * but only one is used at runtime. + *

+ * This is mostly useful when each datasource has a distinct db-kind, but in theory that shouldn't matter, + * so we use the h2 db-kind everywhere here to keep test dependencies simpler. + *

+ * See {@link MultipleDataSourcesAsAlternativesWithActiveDS1Test} for the counterpart where PU2 is used at runtime. + */ +public class MultipleDataSourcesAsAlternativesWithActiveDS1Test { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(MyProducer.class)) + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + .overrideConfigKey("quarkus.datasource.ds-2.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-2.active", "false") + // This is where we select datasource 1 + .overrideRuntimeConfigKey("quarkus.datasource.ds-1.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-1.jdbc.url", "jdbc:h2:mem:testds1"); + + @Inject + @DataSource("ds-1") + AgroalDataSource explicitDatasourceBean; + + @Inject + AgroalDataSource customIndirectDatasourceBean; + + @Inject + @DataSource("ds-2") + AgroalDataSource inactiveDatasourceBean; + + @Test + public void testExplicitDatasourceBeanUsable() { + doTestDatasource(explicitDatasourceBean); + } + + @Test + public void testCustomIndirectDatasourceBeanUsable() { + doTestDatasource(customIndirectDatasourceBean); + } + + @Test + public void testInactiveDatasourceBeanUnusable() { + assertThatThrownBy(() -> inactiveDatasourceBean.getConnection()) + .hasMessageContaining("Datasource 'ds-2' was deactivated through configuration properties."); + } + + private static void doTestDatasource(AgroalDataSource dataSource) { + assertThatCode(() -> { + try (var connection = dataSource.getConnection()) { + } + }) + .doesNotThrowAnyException(); + } + + private static class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @DataSource("ds-1") + AgroalDataSource dataSource1Bean; + + @Inject + @DataSource("ds-2") + AgroalDataSource dataSource2Bean; + + @Produces + @ApplicationScoped + public AgroalDataSource dataSource() { + if (dataSourceSupport.getInactiveNames().contains("ds-1")) { + return dataSource2Bean; + } else { + return dataSource1Bean; + } + } + } +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS2Test.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS2Test.java new file mode 100644 index 0000000000000..7159381c15143 --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS2Test.java @@ -0,0 +1,98 @@ +package io.quarkus.agroal.test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.agroal.DataSource; +import io.quarkus.datasource.runtime.DataSourceSupport; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests a use case where multiple datasources are defined at build time, + * but only one is used at runtime. + *

+ * This is mostly useful when each datasource has a distinct db-kind, but in theory that shouldn't matter, + * so we use the h2 db-kind everywhere here to keep test dependencies simpler. + *

+ * See {@link MultipleDataSourcesAsAlternativesWithActiveDS1Test} for the counterpart where PU2 is used at runtime. + */ +public class MultipleDataSourcesAsAlternativesWithActiveDS2Test { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(MyProducer.class)) + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + .overrideConfigKey("quarkus.datasource.ds-2.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-2.active", "false") + // This is where we select datasource 2 + .overrideRuntimeConfigKey("quarkus.datasource.ds-2.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-2.jdbc.url", "jdbc:h2:mem:testds2"); + + @Inject + @DataSource("ds-2") + AgroalDataSource explicitDatasourceBean; + + @Inject + AgroalDataSource customIndirectDatasourceBean; + + @Inject + @DataSource("ds-1") + AgroalDataSource inactiveDatasourceBean; + + @Test + public void testExplicitDatasourceBeanUsable() { + doTestDatasource(explicitDatasourceBean); + } + + @Test + public void testCustomIndirectDatasourceBeanUsable() { + doTestDatasource(customIndirectDatasourceBean); + } + + @Test + public void testInactiveDatasourceBeanUnusable() { + assertThatThrownBy(() -> inactiveDatasourceBean.getConnection()) + .hasMessageContaining("Datasource 'ds-1' was deactivated through configuration properties."); + } + + private static void doTestDatasource(AgroalDataSource dataSource) { + assertThatCode(() -> { + try (var connection = dataSource.getConnection()) { + } + }) + .doesNotThrowAnyException(); + } + + private static class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @DataSource("ds-1") + AgroalDataSource dataSource1Bean; + + @Inject + @DataSource("ds-2") + AgroalDataSource dataSource2Bean; + + @Produces + @ApplicationScoped + public AgroalDataSource dataSource() { + if (dataSourceSupport.getInactiveNames().contains("ds-1")) { + return dataSource2Bean; + } else { + return dataSource1Bean; + } + } + } +} diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceSupport.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalDataSourceSupport.java similarity index 87% rename from extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceSupport.java rename to extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalDataSourceSupport.java index 886085b97f797..fdf6f9c77e793 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceSupport.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalDataSourceSupport.java @@ -3,13 +3,13 @@ import java.util.Map; import java.util.Optional; -public class DataSourceSupport { +public class AgroalDataSourceSupport { public final boolean disableSslSupport; public final boolean mpMetricsPresent; public final Map entries; - public DataSourceSupport(boolean disableSslSupport, boolean mpMetricsPresent, Map entries) { + public AgroalDataSourceSupport(boolean disableSslSupport, boolean mpMetricsPresent, Map entries) { this.disableSslSupport = disableSslSupport; this.mpMetricsPresent = mpMetricsPresent; this.entries = entries; diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java index a69ce8f788d32..52b7d1a20907d 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java @@ -11,11 +11,11 @@ @Recorder public class AgroalRecorder { - public Supplier dataSourceSupportSupplier(DataSourceSupport dataSourceSupport) { - return new Supplier() { + public Supplier dataSourceSupportSupplier(AgroalDataSourceSupport agroalDataSourceSupport) { + return new Supplier() { @Override - public DataSourceSupport get() { - return dataSourceSupport; + public AgroalDataSourceSupport get() { + return agroalDataSourceSupport; } }; } diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java index 0e6e827b06a42..3fc83fad2d675 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java @@ -8,6 +8,7 @@ import java.util.Iterator; import java.util.Map; import java.util.ServiceLoader; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; @@ -45,6 +46,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.narayana.jta.runtime.TransactionManagerConfiguration; @@ -76,6 +78,7 @@ public class DataSources { private final XAResourceRecoveryRegistry xaResourceRecoveryRegistry; private final TransactionSynchronizationRegistry transactionSynchronizationRegistry; private final DataSourceSupport dataSourceSupport; + private final AgroalDataSourceSupport agroalDataSourceSupport; private final Instance agroalPoolInterceptors; private final Instance agroalOpenTelemetryWrapper; @@ -89,6 +92,7 @@ public DataSources(DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, XAResourceRecoveryRegistry xaResourceRecoveryRegistry, TransactionSynchronizationRegistry transactionSynchronizationRegistry, DataSourceSupport dataSourceSupport, + AgroalDataSourceSupport agroalDataSourceSupport, @Any Instance agroalPoolInterceptors, Instance agroalOpenTelemetryWrapper) { this.dataSourcesBuildTimeConfig = dataSourcesBuildTimeConfig; @@ -100,6 +104,7 @@ public DataSources(DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, this.xaResourceRecoveryRegistry = xaResourceRecoveryRegistry; this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; this.dataSourceSupport = dataSourceSupport; + this.agroalDataSourceSupport = agroalDataSourceSupport; this.agroalPoolInterceptors = agroalPoolInterceptors; this.agroalOpenTelemetryWrapper = agroalOpenTelemetryWrapper; } @@ -127,25 +132,36 @@ public boolean isDataSourceCreated(String dataSourceName) { return dataSources.containsKey(dataSourceName); } + public Set getActiveDataSourceNames() { + // Datasources are created on startup, + // and we only create active datasources. + return dataSources.keySet(); + } + public AgroalDataSource getDataSource(String dataSourceName) { return dataSources.computeIfAbsent(dataSourceName, new Function() { @Override public AgroalDataSource apply(String s) { - return doCreateDataSource(s); + return doCreateDataSource(s, true); } }); } @PostConstruct public void start() { - for (String dataSourceName : dataSourceSupport.entries.keySet()) { - getDataSource(dataSourceName); + for (String dataSourceName : agroalDataSourceSupport.entries.keySet()) { + dataSources.computeIfAbsent(dataSourceName, new Function() { + @Override + public AgroalDataSource apply(String s) { + return doCreateDataSource(s, false); + } + }); } } @SuppressWarnings("resource") - public AgroalDataSource doCreateDataSource(String dataSourceName) { - if (!dataSourceSupport.entries.containsKey(dataSourceName)) { + public AgroalDataSource doCreateDataSource(String dataSourceName, boolean failIfInactive) { + if (!agroalDataSourceSupport.entries.containsKey(dataSourceName)) { throw new IllegalArgumentException("No datasource named '" + dataSourceName + "' exists"); } @@ -153,10 +169,18 @@ public AgroalDataSource doCreateDataSource(String dataSourceName) { .dataSources().get(dataSourceName).jdbc(); DataSourceRuntimeConfig dataSourceRuntimeConfig = dataSourcesRuntimeConfig.dataSources().get(dataSourceName); + if (dataSourceSupport.getInactiveNames().contains(dataSourceName)) { + if (failIfInactive) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } else { + // This only happens on startup, and effectively cancels the creation + // so that we only throw an exception on first actual use. + return null; + } + } + DataSourceJdbcRuntimeConfig dataSourceJdbcRuntimeConfig = dataSourcesJdbcRuntimeConfig .getDataSourceJdbcRuntimeConfig(dataSourceName); - - DataSourceSupport.Entry matchingSupportEntry = dataSourceSupport.entries.get(dataSourceName); if (!dataSourceJdbcRuntimeConfig.url().isPresent()) { //this is not an error situation, because we want to allow the situation where a JDBC extension //is installed but has not been configured @@ -167,6 +191,7 @@ public AgroalDataSource doCreateDataSource(String dataSourceName) { // we first make sure that all available JDBC drivers are loaded in the current TCCL loadDriversInTCCL(); + AgroalDataSourceSupport.Entry matchingSupportEntry = agroalDataSourceSupport.entries.get(dataSourceName); String resolvedDriverClass = matchingSupportEntry.resolvedDriverClass; Class driver; try { @@ -233,13 +258,13 @@ public AgroalDataSource doCreateDataSource(String dataSourceName) { AgroalConnectionFactoryConfigurationSupplier connectionFactoryConfiguration = poolConfiguration .connectionFactoryConfiguration(); - boolean mpMetricsPresent = dataSourceSupport.mpMetricsPresent; + boolean mpMetricsPresent = agroalDataSourceSupport.mpMetricsPresent; applyNewConfiguration(dataSourceName, dataSourceConfiguration, poolConfiguration, connectionFactoryConfiguration, driver, jdbcUrl, dataSourceJdbcBuildTimeConfig, dataSourceRuntimeConfig, dataSourceJdbcRuntimeConfig, transactionRuntimeConfig, mpMetricsPresent); - if (dataSourceSupport.disableSslSupport) { + if (agroalDataSourceSupport.disableSslSupport) { agroalConnectionConfigurer.disableSslSupport(resolvedDbKind, dataSourceConfiguration); } //we use a custom cache for two reasons: diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java index 36f7fd7d7a2fd..7a6d03fee425a 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java @@ -9,6 +9,8 @@ import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; @@ -16,28 +18,37 @@ import org.eclipse.microprofile.health.Readiness; import io.agroal.api.AgroalDataSource; -import io.quarkus.agroal.DataSource.DataSourceLiteral; +import io.quarkus.agroal.runtime.DataSources; import io.quarkus.arc.Arc; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; @Readiness @ApplicationScoped public class DataSourceHealthCheck implements HealthCheck { - private final Map dataSources = new HashMap<>(); + + @Inject + Instance dataSources; + + private final Map checkedDataSources = new HashMap<>(); @PostConstruct protected void init() { - DataSourcesHealthSupport support = Arc.container().instance(DataSourcesHealthSupport.class) + if (!dataSources.isResolvable()) { + // No configured Agroal datasource at build time. + return; + } + DataSourceSupport support = Arc.container().instance(DataSourceSupport.class) .get(); Set names = support.getConfiguredNames(); - Set excludedNames = support.getExcludedNames(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (String name : names) { - DataSource ds = DataSourceUtil.isDefault(name) - ? (DataSource) Arc.container().instance(DataSource.class).get() - : (DataSource) Arc.container().instance(DataSource.class, new DataSourceLiteral(name)).get(); - if (!excludedNames.contains(name) && ds != null) { - dataSources.put(name, ds); + if (excludedNames.contains(name)) { + continue; + } + DataSource ds = dataSources.get().getDataSource(name); + if (ds != null) { + checkedDataSources.put(name, ds); } } } @@ -45,7 +56,7 @@ protected void init() { @Override public HealthCheckResponse call() { HealthCheckResponseBuilder builder = HealthCheckResponse.named("Database connections health check").up(); - for (Map.Entry dataSource : dataSources.entrySet()) { + for (Map.Entry dataSource : checkedDataSources.entrySet()) { boolean isDefault = DataSourceUtil.isDefault(dataSource.getKey()); AgroalDataSource ads = (AgroalDataSource) dataSource.getValue(); String dsName = dataSource.getKey(); diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/metrics/AgroalMetricsRecorder.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/metrics/AgroalMetricsRecorder.java index 081ef0c0b6d95..726a0c0512e5a 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/metrics/AgroalMetricsRecorder.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/metrics/AgroalMetricsRecorder.java @@ -5,9 +5,10 @@ import java.util.function.Function; import java.util.function.Supplier; -import io.agroal.api.AgroalDataSource; +import org.jboss.logging.Logger; + import io.agroal.api.AgroalDataSourceMetrics; -import io.quarkus.agroal.DataSource; +import io.quarkus.agroal.runtime.DataSources; import io.quarkus.arc.Arc; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.runtime.annotations.Recorder; @@ -19,6 +20,8 @@ */ @Recorder public class AgroalMetricsRecorder { + private static final Logger log = Logger.getLogger(AgroalMetricsRecorder.class); + static Function, Long> convertToMillis = new Function, Long>() { @Override public Long apply(Supplier durationSupplier) { @@ -31,8 +34,15 @@ public Consumer registerDataSourceMetrics(String dataSourceName) return new Consumer() { @Override public void accept(MetricsFactory metricsFactory) { + DataSources dataSources = Arc.container().instance(DataSources.class).get(); + if (!dataSources.getActiveDataSourceNames().contains(dataSourceName)) { + log.debug("Not registering metrics for datasource '" + dataSourceName + "'" + + " as the datasource has been deactivated in the configuration"); + return; + } + String tagValue = DataSourceUtil.isDefault(dataSourceName) ? "default" : dataSourceName; - AgroalDataSourceMetrics metrics = getDataSource(dataSourceName).getMetrics(); + AgroalDataSourceMetrics metrics = dataSources.getDataSource(dataSourceName).getMetrics(); metricsFactory.builder("agroal.active.count") .description( @@ -114,14 +124,4 @@ public void accept(MetricsFactory metricsFactory) { } }; } - - private AgroalDataSource getDataSource(String dataSourceName) { - if (dataSourceName == null || DataSourceUtil.isDefault(dataSourceName)) { - return Arc.container().instance(AgroalDataSource.class).get(); - } else { - return Arc.container() - .instance(AgroalDataSource.class, new DataSource.DataSourceLiteral(dataSourceName)) - .get(); - } - } } diff --git a/extensions/datasource/common/src/main/java/io/quarkus/datasource/common/runtime/DataSourceUtil.java b/extensions/datasource/common/src/main/java/io/quarkus/datasource/common/runtime/DataSourceUtil.java index 7b11b9e4aab7a..8158ad25b7e3c 100644 --- a/extensions/datasource/common/src/main/java/io/quarkus/datasource/common/runtime/DataSourceUtil.java +++ b/extensions/datasource/common/src/main/java/io/quarkus/datasource/common/runtime/DataSourceUtil.java @@ -50,6 +50,16 @@ public static ConfigurationException dataSourceNotConfigured(String dataSourceNa dataSourcePropertyKey(dataSourceName, "jdbc.url"))); } + public static ConfigurationException dataSourceInactive(String dataSourceName) { + return new ConfigurationException(String.format(Locale.ROOT, + "Datasource '%s' was deactivated through configuration properties." + + " To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...)." + + " Alternatively, activate the datasource by setting configuration property '%s' to 'true' and configure datasource '%s'." + + " Refer to https://quarkus.io/guides/datasource for guidance.", + dataSourceName, dataSourcePropertyKey(dataSourceName, "active"), dataSourceName), + Set.of(dataSourcePropertyKey(dataSourceName, "active"))); + } + private DataSourceUtil() { } diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/DataSourcesExcludedFromHealthChecksProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/DataSourcesExcludedFromHealthChecksProcessor.java index 80b43e8033719..22f45aefa2ed9 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/DataSourcesExcludedFromHealthChecksProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/DataSourcesExcludedFromHealthChecksProcessor.java @@ -1,15 +1,15 @@ package io.quarkus.datasource.deployment; -import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import jakarta.inject.Singleton; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.datasource.runtime.DataSourceRecorder; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; -import io.quarkus.datasource.runtime.DataSourcesHealthSupportRecorder; +import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; -import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; @@ -17,18 +17,17 @@ public class DataSourcesExcludedFromHealthChecksProcessor { @BuildStep - @Record(STATIC_INIT) + @Record(RUNTIME_INIT) void produceBean( Capabilities capabilities, - DataSourcesHealthSupportRecorder recorder, - DataSourcesBuildTimeConfig config, + DataSourceRecorder recorder, + DataSourcesBuildTimeConfig buildTimeConfig, DataSourcesRuntimeConfig runtimeConfig, BuildProducer syntheticBeans) { - if (capabilities.isPresent(Capability.SMALLRYE_HEALTH)) { - syntheticBeans.produce(SyntheticBeanBuildItem.configure(DataSourcesHealthSupport.class) - .scope(Singleton.class) - .unremovable() - .runtimeValue(recorder.configureDataSourcesHealthSupport(config)) - .done()); - } + syntheticBeans.produce(SyntheticBeanBuildItem.configure(DataSourceSupport.class) + .scope(Singleton.class) + .unremovable() + .runtimeValue(recorder.createDataSourceSupport(buildTimeConfig, runtimeConfig)) + .setRuntimeInit() + .done()); } } diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java index 22b6650110044..559fcbe6fa3d0 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java @@ -198,11 +198,17 @@ private RunningDevService startDevDb( DockerStatusBuildItem dockerStatusBuildItem, LaunchMode launchMode, Optional consoleInstalledBuildItem, LoggingSetupBuildItem loggingSetupBuildItem, GlobalDevServicesConfig globalDevServicesConfig) { - boolean explicitlyDisabled = !(dataSourceBuildTimeConfig.devservices().enabled().orElse(true)); String dataSourcePrettyName = DataSourceUtil.isDefault(dbName) ? "default datasource" : "datasource " + dbName; - if (explicitlyDisabled) { - //explicitly disabled + if (!ConfigUtils.getFirstOptionalValue( + DataSourceUtil.dataSourcePropertyKeys(dbName, "active"), Boolean.class) + .orElse(true)) { + log.debug("Not starting Dev Services for " + dataSourcePrettyName + + " as the datasource has been deactivated in the configuration"); + return null; + } + + if (!(dataSourceBuildTimeConfig.devservices().enabled().orElse(true))) { log.debug("Not starting Dev Services for " + dataSourcePrettyName + " as it has been disabled in the configuration"); return null; diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRecorder.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRecorder.java new file mode 100644 index 0000000000000..87a39c554f6c3 --- /dev/null +++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRecorder.java @@ -0,0 +1,44 @@ +package io.quarkus.datasource.runtime; + +import static java.util.stream.Collectors.toUnmodifiableSet; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class DataSourceRecorder { + + public RuntimeValue createDataSourceSupport( + DataSourcesBuildTimeConfig buildTimeConfig, + DataSourcesRuntimeConfig runtimeConfig) { + Stream.Builder configured = Stream.builder(); + Stream.Builder excludedForHealthChecks = Stream.builder(); + for (Map.Entry dataSource : buildTimeConfig.dataSources().entrySet()) { + // TODO this is wrong, as the default datasource could be configured without db-kind being set: + // it's inferred automatically for the default datasource when possible. + // See https://github.com/quarkusio/quarkus/issues/37779 + if (dataSource.getValue().dbKind().isPresent()) { + configured.add(dataSource.getKey()); + } + if (dataSource.getValue().healthExclude()) { + excludedForHealthChecks.add(dataSource.getKey()); + } + } + Set names = configured.build().collect(toUnmodifiableSet()); + Set excludedNames = excludedForHealthChecks.build().collect(toUnmodifiableSet()); + + Stream.Builder inactive = Stream.builder(); + for (Map.Entry entry : runtimeConfig.dataSources().entrySet()) { + if (!entry.getValue().active()) { + inactive.add(entry.getKey()); + } + } + Set inactiveNames = inactive.build().collect(toUnmodifiableSet()); + + return new RuntimeValue<>(new DataSourceSupport(names, excludedNames, inactiveNames)); + } +} diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRuntimeConfig.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRuntimeConfig.java index dc9cd2b972e99..58fb761d34767 100644 --- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRuntimeConfig.java +++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRuntimeConfig.java @@ -5,10 +5,26 @@ import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.configuration.TrimmedStringConverter; import io.smallrye.config.WithConverter; +import io.smallrye.config.WithDefault; @ConfigGroup public interface DataSourceRuntimeConfig { + /** + * Whether this datasource should be active at runtime. + * + * See xref:datasource.adoc#datasource-active[this section of the documentation]. + * + * If the datasource is not active, it won't start with the application, + * and accessing the corresponding Datasource CDI bean will fail, + * meaning in particular that consumers of this datasource + * (e.g. Hibernate ORM persistence units) will fail to start unless they are inactive too. + * + * @asciidoclet + */ + @WithDefault("true") + boolean active(); + /** * The datasource username */ diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceSupport.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceSupport.java new file mode 100644 index 0000000000000..96b4b0f1fa9a9 --- /dev/null +++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceSupport.java @@ -0,0 +1,42 @@ +package io.quarkus.datasource.runtime; + +import java.util.HashSet; +import java.util.Set; + +/** + * Helper class that holds the names of all configured data sources, + * along with the names of those that are inactive or excluded from health checks. + *

+ * This is used by any feature that needs runtime access to data sources, + * e.g. Flyway/Liquibase or health check implementation classes. + */ +public class DataSourceSupport { + + private final Set configuredNames; + private final Set inactiveNames; + private final Set inactiveOrHealthCheckExcludedNames; + + public DataSourceSupport(Set configuredNames, Set healthCheckExcludedNames, + Set inactiveNames) { + this.configuredNames = configuredNames; + this.inactiveOrHealthCheckExcludedNames = new HashSet<>(); + inactiveOrHealthCheckExcludedNames.addAll(inactiveNames); + inactiveOrHealthCheckExcludedNames.addAll(healthCheckExcludedNames); + this.inactiveNames = inactiveNames; + } + + // TODO careful when using this, as it might (incorrectly) not include the default datasource. + // See TODO in code that calls the constructor of this class. + // See https://github.com/quarkusio/quarkus/issues/37779 + public Set getConfiguredNames() { + return configuredNames; + } + + public Set getInactiveNames() { + return inactiveNames; + } + + public Set getInactiveOrHealthCheckExcludedNames() { + return inactiveOrHealthCheckExcludedNames; + } +} diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupport.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupport.java deleted file mode 100644 index 00a3c19a91b90..0000000000000 --- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupport.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.quarkus.datasource.runtime; - -import java.util.Set; - -/** - * Helper class that holds the names of all configured data sources, along with the names of those - * that are excluded from health checks. This is used by health check implementation classes. - */ -public class DataSourcesHealthSupport { - - private final Set configuredNames; - private final Set excludedNames; - - public DataSourcesHealthSupport(Set configuredNames, Set excludedNames) { - this.configuredNames = configuredNames; - this.excludedNames = excludedNames; - } - - public Set getConfiguredNames() { - return configuredNames; - } - - public Set getExcludedNames() { - return excludedNames; - } -} diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupportRecorder.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupportRecorder.java deleted file mode 100644 index e39d700a6ed64..0000000000000 --- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupportRecorder.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.quarkus.datasource.runtime; - -import static java.util.stream.Collectors.toUnmodifiableSet; - -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -import io.quarkus.runtime.RuntimeValue; -import io.quarkus.runtime.annotations.Recorder; - -@Recorder -public class DataSourcesHealthSupportRecorder { - - public RuntimeValue configureDataSourcesHealthSupport( - DataSourcesBuildTimeConfig config) { - Stream.Builder configured = Stream.builder(); - Stream.Builder excluded = Stream.builder(); - for (Map.Entry dataSource : config.dataSources().entrySet()) { - if (dataSource.getValue().dbKind().isPresent()) { - configured.add(dataSource.getKey()); - } - if (dataSource.getValue().healthExclude()) { - excluded.add(dataSource.getKey()); - } - } - Set names = configured.build().collect(toUnmodifiableSet()); - Set excludedNames = excluded.build().collect(toUnmodifiableSet()); - return new RuntimeValue<>(new DataSourcesHealthSupport(names, excludedNames)); - } -} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseDefaultDatasourceTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..79ff4aad04ff4 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,38 @@ +package io.quarkus.flyway.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + Instance flywayForDefaultDatasource; + + @Test + @DisplayName("If the default datasource is deactivated, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flywayForDefaultDatasource::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Flyway", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseNamedDataSourceTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseNamedDataSourceTest.java new file mode 100644 index 0000000000000..a00dd3f237b13 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseNamedDataSourceTest.java @@ -0,0 +1,49 @@ +package io.quarkus.flyway.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigActiveFalseNamedDataSourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.users.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise the *default* datasource may impact this test + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + @FlywayDataSource("users") + Instance flywayForNamedDatasource; + + @Test + @DisplayName("If a named datasource is deactivated, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flywayForNamedDatasource::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Flyway", + "Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..240237ff5d727 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java @@ -0,0 +1,42 @@ +package io.quarkus.flyway.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql")) + .overrideConfigKey("quarkus.datasource.active", "false") + .overrideConfigKey("quarkus.flyway.migrate-at-start", "true"); + + @Inject + Instance flywayForDefaultDatasource; + + @Test + @DisplayName("If the default datasource is deactivated, even if migrate-at-start is enabled, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flywayForDefaultDatasource::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Flyway", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..7c2e303c9d4ce --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java @@ -0,0 +1,52 @@ +package io.quarkus.flyway.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql")) + .overrideConfigKey("quarkus.datasource.users.active", "false") + .overrideConfigKey("quarkus.flyway.users.migrate-at-start", "true") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise the *default* datasource may impact this test + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + @FlywayDataSource("users") + Instance flywayForNamedDatasource; + + @Test + @DisplayName("If a named datasource is deactivated, even if migrate-at-start is enabled, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flywayForNamedDatasource::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Flyway", + "Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainerUtil.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainerUtil.java new file mode 100644 index 0000000000000..34e6a4629f295 --- /dev/null +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainerUtil.java @@ -0,0 +1,44 @@ +package io.quarkus.flyway.runtime; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.inject.Default; + +import io.quarkus.agroal.runtime.DataSources; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.flyway.FlywayDataSource; + +public final class FlywayContainerUtil { + private FlywayContainerUtil() { + } + + public static FlywayContainer getFlywayContainer(String dataSourceName) { + return Arc.container().instance(FlywayContainer.class, + getFlywayContainerQualifier(dataSourceName)).get(); + } + + public static List getActiveFlywayContainers() { + List result = new ArrayList<>(); + for (String datasourceName : Arc.container().instance(DataSources.class).get().getActiveDataSourceNames()) { + InstanceHandle handle = Arc.container().instance(FlywayContainer.class, + getFlywayContainerQualifier(datasourceName)); + if (!handle.isAvailable()) { + continue; + } + result.add(handle.get()); + } + return result; + } + + public static Annotation getFlywayContainerQualifier(String dataSourceName) { + if (DataSourceUtil.isDefault(dataSourceName)) { + return Default.Literal.INSTANCE; + } + + return FlywayDataSource.FlywayDataSourceLiteral.of(dataSourceName); + } +} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java index 11261c88c30a1..d44d997f01964 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java @@ -2,29 +2,18 @@ import java.util.Collection; import java.util.Comparator; -import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.function.Supplier; -import io.quarkus.arc.Arc; -import io.quarkus.arc.InstanceHandle; import io.quarkus.datasource.common.runtime.DataSourceUtil; public class FlywayContainersSupplier implements Supplier> { @Override public Collection get() { - List> flywayContainerHandles = Arc.container().listAll(FlywayContainer.class); - - if (flywayContainerHandles.isEmpty()) { - return Set.of(); - } - Set containers = new TreeSet<>(FlywayContainerComparator.INSTANCE); - for (InstanceHandle flywayContainerHandle : flywayContainerHandles) { - containers.add(flywayContainerHandle.get()); - } + containers.addAll(FlywayContainerUtil.getActiveFlywayContainers()); return containers; } diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java index 726ada0de89fd..2e9d9aa3dd35e 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java @@ -25,8 +25,8 @@ public static FlywayDataSourceRuntimeConfig defaultConfig() { /** * Flag to activate/deactivate Flyway for a specific datasource at runtime. */ - @ConfigItem(defaultValue = "true") - public boolean active = true; + @ConfigItem(defaultValueDocumentation = "'true' if the datasource is active; 'false' otherwise") + public Optional active = Optional.empty(); /** * The maximum number of retries when attempting to connect to the database. diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java index 4e858dcf71a3b..3af211f682891 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java @@ -1,6 +1,5 @@ package io.quarkus.flyway.runtime; -import java.lang.annotation.Annotation; import java.util.Collection; import java.util.Locale; import java.util.Map; @@ -8,8 +7,6 @@ import javax.sql.DataSource; -import jakarta.enterprise.inject.Default; - import org.flywaydb.core.Flyway; import org.flywaydb.core.FlywayExecutor; import org.flywaydb.core.api.callback.Callback; @@ -29,7 +26,6 @@ import io.quarkus.arc.InstanceHandle; import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.flyway.FlywayDataSource.FlywayDataSourceLiteral; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigurationException; @@ -73,7 +69,7 @@ public FlywayContainer apply(SyntheticCreationalContext context throw DataSourceUtil.dataSourceNotConfigured(dataSourceName); } } catch (ConfigurationException e) { - // TODO do we really want to enable retrieval of a FlywayContainer for an unconfigured datasource? + // TODO do we really want to enable retrieval of a FlywayContainer for an unconfigured/inactive datasource? // Assigning ApplicationScoped to the FlywayContainer // and throwing UnsatisfiedResolutionException on bean creation (first access) // would probably make more sense. @@ -93,7 +89,7 @@ public Function, Flyway> flywayFunction(Strin @Override public Flyway apply(SyntheticCreationalContext context) { FlywayContainer flywayContainer = context.getInjectedReference(FlywayContainer.class, - getFlywayContainerQualifier(dataSourceName)); + FlywayContainerUtil.getFlywayContainerQualifier(dataSourceName)); return flywayContainer.getFlyway(); } }; @@ -103,12 +99,15 @@ public void doStartActions(String dataSourceName) { FlywayDataSourceRuntimeConfig flywayDataSourceRuntimeConfig = config.getValue() .getConfigForDataSourceName(dataSourceName); - if (!config.getValue().getConfigForDataSourceName(dataSourceName).active) { + if (!flywayDataSourceRuntimeConfig.active + // If not specified explicitly, Flyway is active when the datasource itself is active. + .orElseGet(() -> Arc.container().instance(DataSources.class).get().getActiveDataSourceNames() + .contains(dataSourceName))) { return; } InstanceHandle flywayContainerInstanceHandle = Arc.container().instance(FlywayContainer.class, - getFlywayContainerQualifier(dataSourceName)); + FlywayContainerUtil.getFlywayContainerQualifier(dataSourceName)); if (!flywayContainerInstanceHandle.isAvailable()) { return; @@ -138,14 +137,6 @@ public void doStartActions(String dataSourceName) { } } - private static Annotation getFlywayContainerQualifier(String dataSourceName) { - if (DataSourceUtil.isDefault(dataSourceName)) { - return Default.Literal.INSTANCE; - } - - return FlywayDataSourceLiteral.of(dataSourceName); - } - static class BaselineCommand implements FlywayExecutor.Command { BaselineCommand(Flyway flyway) { this.flyway = flyway; diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java index 57dab412c16d1..529b80a67c33e 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java @@ -1,23 +1,19 @@ package io.quarkus.flyway.runtime; -import io.quarkus.arc.Arc; import io.quarkus.datasource.runtime.DatabaseSchemaProvider; public class FlywaySchemaProvider implements DatabaseSchemaProvider { @Override public void resetDatabase(String dbName) { - for (FlywayContainer flywayContainer : Arc.container().select(FlywayContainer.class)) { - if (flywayContainer.getDataSourceName().equals(dbName)) { - flywayContainer.getFlyway().clean(); - flywayContainer.getFlyway().migrate(); - } - } + FlywayContainer flywayContainer = FlywayContainerUtil.getFlywayContainer(dbName); + flywayContainer.getFlyway().clean(); + flywayContainer.getFlyway().migrate(); } @Override public void resetAllDatabases() { - for (FlywayContainer flywayContainer : Arc.container().select(FlywayContainer.class)) { + for (FlywayContainer flywayContainer : FlywayContainerUtil.getActiveFlywayContainers()) { flywayContainer.getFlyway().clean(); flywayContainer.getFlyway().migrate(); } diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithExplicitDatasourceConfigActiveFalseTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithExplicitDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..570fdd3e6368f --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithExplicitDatasourceConfigActiveFalseTest.java @@ -0,0 +1,41 @@ +package io.quarkus.hibernate.orm.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.config.MyEntity; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class EntitiesInDefaultPUWithExplicitDatasourceConfigActiveFalseTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(MyEntity.class)) + .overrideConfigKey("quarkus.hibernate-orm.datasource", "ds-1") + .overrideConfigKey("quarkus.hibernate-orm.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.datasource.\"ds-1\".active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.\"ds-1\".db-kind", "h2") + .assertException(t -> assertThat(t) + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll( + "Unable to find datasource 'ds-1' for persistence unit ''", + "Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance.")); + + @Test + public void testInvalidConfiguration() { + // deployment exception should happen first + Assertions.fail(); + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..04e0448723341 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java @@ -0,0 +1,36 @@ +package io.quarkus.hibernate.orm.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.config.MyEntity; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(MyEntity.class)) + .overrideConfigKey("quarkus.datasource.active", "false") + .assertException(t -> assertThat(t) + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll( + "Unable to find datasource '' for persistence unit ''", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance.")); + + @Test + public void testInvalidConfiguration() { + // deployment exception should happen first + Assertions.fail(); + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInNamedPUWithExplicitDatasourceConfigActiveFalseTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInNamedPUWithExplicitDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..8f5f8c6904fc5 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInNamedPUWithExplicitDatasourceConfigActiveFalseTest.java @@ -0,0 +1,41 @@ +package io.quarkus.hibernate.orm.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.config.namedpu.MyEntity; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class EntitiesInNamedPUWithExplicitDatasourceConfigActiveFalseTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addPackage(MyEntity.class.getPackage().getName())) + .overrideConfigKey("quarkus.hibernate-orm.pu-1.datasource", "ds-1") + .overrideConfigKey("quarkus.hibernate-orm.pu-1.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.datasource.\"ds-1\".active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.\"ds-1\".db-kind", "h2") + .assertException(t -> assertThat(t) + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll( + "Unable to find datasource 'ds-1' for persistence unit 'pu-1'", + "Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance.")); + + @Test + public void testInvalidConfiguration() { + // deployment exception should happen first + Assertions.fail(); + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU1Test.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU1Test.java new file mode 100644 index 0000000000000..298c13516a675 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU1Test.java @@ -0,0 +1,119 @@ +package io.quarkus.hibernate.orm.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +import org.hibernate.Session; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.datasource.runtime.DataSourceSupport; +import io.quarkus.hibernate.orm.PersistenceUnit; +import io.quarkus.hibernate.orm.config.namedpu.MyEntity; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests a use case where multiple PU/datasources are defined at build time, + * but only one is used at runtime. + *

+ * This is mostly useful when each datasource has a distinct db-kind, but in theory that shouldn't matter, + * so we use the h2 db-kind everywhere here to keep test dependencies simpler. + *

+ * See {@link MultiplePUAsAlternativesWithActivePU2Test} for the counterpart where PU2 is used at runtime. + */ +public class MultiplePUAsAlternativesWithActivePU1Test { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addPackage(MyEntity.class.getPackage().getName()) + .addClass(MyProducer.class)) + .overrideConfigKey("quarkus.hibernate-orm.pu-1.packages", MyEntity.class.getPackageName()) + .overrideConfigKey("quarkus.hibernate-orm.pu-1.datasource", "ds-1") + .overrideConfigKey("quarkus.hibernate-orm.pu-1.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.hibernate-orm.pu-1.active", "false") + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.packages", MyEntity.class.getPackageName()) + .overrideConfigKey("quarkus.hibernate-orm.pu-2.datasource", "ds-2") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.active", "false") + .overrideConfigKey("quarkus.datasource.ds-2.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-2.active", "false") + // This is where we select PU1 / datasource 1 + .overrideRuntimeConfigKey("quarkus.hibernate-orm.pu-1.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-1.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-1.jdbc.url", "jdbc:h2:mem:testds1"); + + @Inject + @PersistenceUnit("pu-1") + Session explicitSessionBean; + + @Inject + Session customIndirectSessionBean; + + @Inject + @PersistenceUnit("pu-2") + Session inactiveSessionBean; + + @Test + public void testExplicitSessionBeanUsable() { + doTestPersistRetrieve(explicitSessionBean, 1L); + } + + @Test + public void testCustomIndirectSessionBeanUsable() { + doTestPersistRetrieve(customIndirectSessionBean, 2L); + } + + @Test + public void testInactiveSessionBeanUnusable() { + QuarkusTransaction.requiringNew().run(() -> { + assertThatThrownBy(() -> inactiveSessionBean.find(MyEntity.class, 3L)) + .hasMessageContainingAll( + "Cannot retrieve the EntityManagerFactory/SessionFactory for persistence unit pu-2", + "Hibernate ORM was deactivated through configuration properties"); + }); + } + + private static void doTestPersistRetrieve(Session session, long id) { + QuarkusTransaction.requiringNew().run(() -> { + MyEntity entity = new MyEntity(); + entity.setId(id); + entity.setName("text" + id); + session.persist(entity); + }); + QuarkusTransaction.requiringNew().run(() -> { + MyEntity entity = session.get(MyEntity.class, id); + assertThat(entity.getName()).isEqualTo("text" + id); + }); + } + + private static class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @PersistenceUnit("pu-1") + Session pu1SessionBean; + + @Inject + @PersistenceUnit("pu-2") + Session pu2SessionBean; + + @Produces + @ApplicationScoped + public Session session() { + if (dataSourceSupport.getInactiveNames().contains("ds-1")) { + return pu2SessionBean; + } else { + return pu1SessionBean; + } + } + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU2Test.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU2Test.java new file mode 100644 index 0000000000000..c9eb9d247962a --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU2Test.java @@ -0,0 +1,119 @@ +package io.quarkus.hibernate.orm.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +import org.hibernate.Session; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.datasource.runtime.DataSourceSupport; +import io.quarkus.hibernate.orm.PersistenceUnit; +import io.quarkus.hibernate.orm.config.namedpu.MyEntity; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests a use case where multiple PU/datasources are defined at build time, + * but only one is used at runtime. + *

+ * This is mostly useful when each datasource has a distinct db-kind, but in theory that shouldn't matter, + * so we use the h2 db-kind everywhere here to keep test dependencies simpler. + *

+ * See {@link MultiplePUAsAlternativesWithActivePU1Test} for the counterpart where PU1 is used at runtime. + */ +public class MultiplePUAsAlternativesWithActivePU2Test { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addPackage(MyEntity.class.getPackage().getName()) + .addClass(MyProducer.class)) + .overrideConfigKey("quarkus.hibernate-orm.pu-1.packages", MyEntity.class.getPackageName()) + .overrideConfigKey("quarkus.hibernate-orm.pu-1.datasource", "ds-1") + .overrideConfigKey("quarkus.hibernate-orm.pu-1.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.hibernate-orm.pu-1.active", "false") + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.packages", MyEntity.class.getPackageName()) + .overrideConfigKey("quarkus.hibernate-orm.pu-2.datasource", "ds-2") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.active", "false") + .overrideConfigKey("quarkus.datasource.ds-2.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-2.active", "false") + // This is where we select PU1 / datasource 2 + .overrideRuntimeConfigKey("quarkus.hibernate-orm.pu-2.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-2.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-2.jdbc.url", "jdbc:h2:mem:testds2"); + + @Inject + @PersistenceUnit("pu-2") + Session explicitSessionBean; + + @Inject + Session customIndirectSessionBean; + + @Inject + @PersistenceUnit("pu-1") + Session inactiveSessionBean; + + @Test + public void testExplicitSessionBeanUsable() { + doTestPersistRetrieve(explicitSessionBean, 1L); + } + + @Test + public void testCustomIndirectSessionBeanUsable() { + doTestPersistRetrieve(customIndirectSessionBean, 2L); + } + + @Test + public void testInactiveSessionBeanUnusable() { + QuarkusTransaction.requiringNew().run(() -> { + assertThatThrownBy(() -> inactiveSessionBean.find(MyEntity.class, 3L)) + .hasMessageContainingAll( + "Cannot retrieve the EntityManagerFactory/SessionFactory for persistence unit pu-1", + "Hibernate ORM was deactivated through configuration properties"); + }); + } + + private static void doTestPersistRetrieve(Session session, long id) { + QuarkusTransaction.requiringNew().run(() -> { + MyEntity entity = new MyEntity(); + entity.setId(id); + entity.setName("text" + id); + session.persist(entity); + }); + QuarkusTransaction.requiringNew().run(() -> { + MyEntity entity = session.get(MyEntity.class, id); + assertThat(entity.getName()).isEqualTo("text" + id); + }); + } + + private static class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @PersistenceUnit("pu-1") + Session pu1SessionBean; + + @Inject + @PersistenceUnit("pu-2") + Session pu2SessionBean; + + @Produces + @ApplicationScoped + public Session session() { + if (dataSourceSupport.getInactiveNames().contains("ds-1")) { + return pu2SessionBean; + } else { + return pu1SessionBean; + } + } + } +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java index 7ba8bb8141912..a272afe7f6cac 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java @@ -17,6 +17,8 @@ public class HibernateOrmRuntimeConfigPersistenceUnit { /** * Whether this persistence unit should be active at runtime. * + * See xref:hibernate-orm.adoc#persistence-unit-active[this section of the documentation]. + * * If the persistence unit is not active, it won't start with the application, * and accessing the corresponding EntityManagerFactory/EntityManager or SessionFactory/Session * will not be possible. diff --git a/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java b/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..80ed019c91ce2 --- /dev/null +++ b/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java @@ -0,0 +1,37 @@ +package io.quarkus.hibernate.reactive.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.config.MyEntity; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(MyEntity.class)) + .withConfigurationResource("application.properties") + .overrideConfigKey("quarkus.datasource.active", "false") + .assertException(t -> assertThat(t) + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll( + "Unable to find datasource '' for persistence unit 'default-reactive'", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance.")); + + @Test + public void testInvalidConfiguration() { + // deployment exception should happen first + Assertions.fail(); + } + +} diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java index 3e94c94fd1367..5faa419621b96 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java @@ -28,11 +28,14 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.InstanceHandle; +import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.hibernate.orm.runtime.BuildTimeSettings; import io.quarkus.hibernate.orm.runtime.FastBootHibernatePersistenceProvider; import io.quarkus.hibernate.orm.runtime.HibernateOrmRuntimeConfig; import io.quarkus.hibernate.orm.runtime.HibernateOrmRuntimeConfigPersistenceUnit; import io.quarkus.hibernate.orm.runtime.IntegrationSettings; +import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil; import io.quarkus.hibernate.orm.runtime.PersistenceUnitsHolder; import io.quarkus.hibernate.orm.runtime.RuntimeSettings; import io.quarkus.hibernate.orm.runtime.RuntimeSettings.Builder; @@ -76,31 +79,25 @@ public FastBootHibernateReactivePersistenceProvider(HibernateOrmRuntimeConfig hi public EntityManagerFactory createEntityManagerFactory(String emName, Map properties) { if (properties == null) properties = new HashMap(); - try { - // These are pre-parsed during image generation: - final List units = PersistenceUnitsHolder.getPersistenceUnitDescriptors(); - - for (PersistenceUnitDescriptor unit : units) { - //if the provider is not set, don't use it as people might want to use Hibernate ORM - if (IMPLEMENTATION_NAME.equalsIgnoreCase(unit.getProviderClassName()) || - unit.getProviderClassName() == null) { - EntityManagerFactoryBuilder builder = getEntityManagerFactoryBuilderOrNull(emName, properties); - if (builder == null) { - log.trace("Could not obtain matching EntityManagerFactoryBuilder, returning null"); - return null; - } else { - return builder.build(); - } + // These are pre-parsed during image generation: + final List units = PersistenceUnitsHolder.getPersistenceUnitDescriptors(); + + for (PersistenceUnitDescriptor unit : units) { + //if the provider is not set, don't use it as people might want to use Hibernate ORM + if (IMPLEMENTATION_NAME.equalsIgnoreCase(unit.getProviderClassName()) || + unit.getProviderClassName() == null) { + EntityManagerFactoryBuilder builder = getEntityManagerFactoryBuilderOrNull(emName, properties); + if (builder == null) { + log.trace("Could not obtain matching EntityManagerFactoryBuilder, returning null"); + return null; + } else { + return builder.build(); } } - - //not the right provider - return null; - } catch (PersistenceException pe) { - throw pe; - } catch (Exception e) { - throw new PersistenceException("Unable to build EntityManagerFactory", e); } + + //not the right provider + return null; } private EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String persistenceUnitName, @@ -286,12 +283,22 @@ private void registerVertxAndPool(String persistenceUnitName, } // for now, we only support one pool but this will change - InstanceHandle poolHandle = Arc.container().instance(Pool.class); - if (!poolHandle.isAvailable()) { - throw new IllegalStateException("No pool has been defined for persistence unit " + persistenceUnitName); + String datasourceName = DataSourceUtil.DEFAULT_DATASOURCE_NAME; + Pool pool; + try { + if (Arc.container().instance(DataSourceSupport.class).get().getInactiveNames().contains(datasourceName)) { + throw DataSourceUtil.dataSourceInactive(datasourceName); + } + InstanceHandle poolHandle = Arc.container().instance(Pool.class); + if (!poolHandle.isAvailable()) { + throw new IllegalStateException("No pool has been defined for persistence unit " + persistenceUnitName); + } + pool = poolHandle.get(); + } catch (RuntimeException e) { + throw PersistenceUnitUtil.unableToFindDataSource(persistenceUnitName, datasourceName, e); } - serviceRegistry.addInitiator(new QuarkusReactiveConnectionPoolInitiator(poolHandle.get())); + serviceRegistry.addInitiator(new QuarkusReactiveConnectionPoolInitiator(pool)); InstanceHandle vertxHandle = Arc.container().instance(Vertx.class); if (!vertxHandle.isAvailable()) { diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java index 630742c7adc6c..5eb2f7ae1094c 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java @@ -24,10 +24,14 @@ public FastBootReactiveEntityManagerFactoryBuilder(PrevalidatedQuarkusMetadata m @Override public EntityManagerFactory build() { - final SessionFactoryOptionsBuilder optionsBuilder = metadata.buildSessionFactoryOptionsBuilder(); - optionsBuilder.enableCollectionInDefaultFetchGroup(true); - populate(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME, optionsBuilder, standardServiceRegistry); - SessionFactoryOptions options = optionsBuilder.buildOptions(); - return new ReactiveSessionFactoryImpl(metadata, options, metadata.getBootstrapContext()); + try { + final SessionFactoryOptionsBuilder optionsBuilder = metadata.buildSessionFactoryOptionsBuilder(); + optionsBuilder.enableCollectionInDefaultFetchGroup(true); + populate(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME, optionsBuilder, standardServiceRegistry); + SessionFactoryOptions options = optionsBuilder.buildOptions(); + return new ReactiveSessionFactoryImpl(metadata, options, metadata.getBootstrapContext()); + } catch (Exception e) { + throw persistenceException("Unable to build Hibernate Reactive SessionFactory", e); + } } } diff --git a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java index 8c2a597126e5f..ab68ddd370514 100644 --- a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java +++ b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java @@ -306,10 +306,13 @@ ServiceStartBuildItem startLiquibase(LiquibaseRecorder recorder, BuildProducer initializationCompleteBuildItem, BuildProducer schemaReadyBuildItem) { - recorder.doStartActions(); + Set dataSourceNames = getDataSourceNames(jdbcDataSourceBuildItems); + for (String dataSourceName : dataSourceNames) { + recorder.doStartActions(dataSourceName); + } // once we are done running the migrations, we produce a build item indicating that the // schema is "ready" - schemaReadyBuildItem.produce(new JdbcDataSourceSchemaReadyBuildItem(getDataSourceNames(jdbcDataSourceBuildItems))); + schemaReadyBuildItem.produce(new JdbcDataSourceSchemaReadyBuildItem(dataSourceNames)); initializationCompleteBuildItem.produce(new InitTaskCompletedBuildItem("liquibase")); return new ServiceStartBuildItem("liquibase"); diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseDefaultDatasourceTest.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..ed35e12bfd237 --- /dev/null +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,39 @@ +package io.quarkus.liquibase.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.test.QuarkusUnitTest; + +public class LiquibaseExtensionConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + Instance liquibaseForDefaultDatasource; + + @Test + @DisplayName("If the default datasource is deactivated, the application should boot, but Liquibase should be deactivated for that datasource") + public void testBootSucceedsButLiquibaseDeactivated() { + assertThatThrownBy(() -> liquibaseForDefaultDatasource.get().getConfiguration()) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Liquibase", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + +} diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseNamedDatasourceTest.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..583e2d934dadc --- /dev/null +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,48 @@ +package io.quarkus.liquibase.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.liquibase.LiquibaseDataSource; +import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.test.QuarkusUnitTest; + +public class LiquibaseExtensionConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.users.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise it's going to be the *default* datasource making everything fail + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + @Inject + @LiquibaseDataSource("users") + Instance liquibaseForNamedDatasource; + + @Test + @DisplayName("If a named datasource is deactivated, the application should boot, but Liquibase should be deactivated for that datasource") + public void testBootSucceedsButLiquibaseDeactivated() { + assertThatThrownBy(() -> liquibaseForNamedDatasource.get().getConfiguration()) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Liquibase", + "Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..dccc77efe2e06 --- /dev/null +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java @@ -0,0 +1,42 @@ +package io.quarkus.liquibase.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.test.QuarkusUnitTest; + +public class LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/changeLog.xml", "db/changeLog.xml")) + .overrideConfigKey("quarkus.datasource.active", "false") + .overrideConfigKey("quarkus.liquibase.migrate-at-start", "true"); + + @Inject + Instance liquibaseForDefaultDatasource; + + @Test + @DisplayName("If the default datasource is deactivated, even if migrate-at-start is enabled, the application should boot, but Liquibase should be deactivated for that datasource") + public void testBootSucceedsButLiquibaseDeactivated() { + assertThatThrownBy(() -> liquibaseForDefaultDatasource.get().getConfiguration()) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Liquibase", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + +} diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..bfc68585954d6 --- /dev/null +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java @@ -0,0 +1,52 @@ +package io.quarkus.liquibase.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.liquibase.LiquibaseDataSource; +import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.test.QuarkusUnitTest; + +public class LiquibaseExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/changeLog.xml", "db/changeLog.xml")) + .overrideConfigKey("quarkus.datasource.users.active", "false") + .overrideConfigKey("quarkus.liquibase.users.migrate-at-start", "true") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise it's going to be the *default* datasource making everything fail + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + @LiquibaseDataSource("users") + Instance liquibaseForNamedDatasource; + + @Test + @DisplayName("If a named datasource is deactivated, even if migrate-at-start is enabled, the application should boot, but Liquibase should be deactivated for that datasource") + public void testBootSucceedsButLiquibaseDeactivated() { + assertThatThrownBy(() -> liquibaseForNamedDatasource.get().getConfiguration()) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Liquibase", + "Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseFactoryUtil.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseFactoryUtil.java new file mode 100644 index 0000000000000..c3d4698cfe4c6 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseFactoryUtil.java @@ -0,0 +1,45 @@ +package io.quarkus.liquibase.runtime; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.inject.Default; + +import io.quarkus.agroal.runtime.DataSources; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.liquibase.LiquibaseDataSource; +import io.quarkus.liquibase.LiquibaseFactory; + +public final class LiquibaseFactoryUtil { + private LiquibaseFactoryUtil() { + } + + public static InstanceHandle getLiquibaseFactory(String dataSourceName) { + return Arc.container().instance(LiquibaseFactory.class, + getLiquibaseFactoryQualifier(dataSourceName)); + } + + public static List> getActiveLiquibaseFactories() { + List> result = new ArrayList<>(); + for (String datasourceName : Arc.container().instance(DataSources.class).get().getActiveDataSourceNames()) { + InstanceHandle handle = Arc.container().instance(LiquibaseFactory.class, + getLiquibaseFactoryQualifier(datasourceName)); + if (!handle.isAvailable()) { + continue; + } + result.add(handle); + } + return result; + } + + public static Annotation getLiquibaseFactoryQualifier(String dataSourceName) { + if (DataSourceUtil.isDefault(dataSourceName)) { + return Default.Literal.INSTANCE; + } + + return LiquibaseDataSource.LiquibaseDataSourceLiteral.of(dataSourceName); + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java index 4b045a357417b..f368806383203 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java @@ -5,13 +5,11 @@ import javax.sql.DataSource; -import jakarta.enterprise.inject.Any; import jakarta.enterprise.inject.UnsatisfiedResolutionException; import io.quarkus.agroal.runtime.DataSources; import io.quarkus.agroal.runtime.UnconfiguredDataSource; import io.quarkus.arc.Arc; -import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.InstanceHandle; import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.datasource.common.runtime.DataSourceUtil; @@ -52,49 +50,45 @@ public LiquibaseFactory apply(SyntheticCreationalContext conte }; } - public void doStartActions() { + public void doStartActions(String dataSourceName) { if (!config.getValue().enabled) { return; } + // Liquibase is active when the datasource itself is active. + if (!Arc.container().instance(DataSources.class).get().getActiveDataSourceNames().contains(dataSourceName)) { + return; + } + InstanceHandle liquibaseFactoryHandle = LiquibaseFactoryUtil.getLiquibaseFactory(dataSourceName); try { - InjectableInstance liquibaseFactoryInstance = Arc.container() - .select(LiquibaseFactory.class, Any.Literal.INSTANCE); - if (liquibaseFactoryInstance.isUnsatisfied()) { + LiquibaseFactory liquibaseFactory = liquibaseFactoryHandle.get(); + var config = liquibaseFactory.getConfiguration(); + if (!config.cleanAtStart && !config.migrateAtStart) { return; } - - for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { - try { - LiquibaseFactory liquibaseFactory = liquibaseFactoryHandle.get(); - var config = liquibaseFactory.getConfiguration(); - if (!config.cleanAtStart && !config.migrateAtStart) { - continue; - } - try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { - if (config.cleanAtStart) { - liquibase.dropAll(); - } - if (config.migrateAtStart) { - var lockService = LockServiceFactory.getInstance() - .getLockService(liquibase.getDatabase()); - lockService.waitForLock(); - try { - if (config.validateOnMigrate) { - liquibase.validate(); - } - liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels()); - } finally { - lockService.releaseLock(); - } + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + if (config.cleanAtStart) { + liquibase.dropAll(); + } + if (config.migrateAtStart) { + var lockService = LockServiceFactory.getInstance() + .getLockService(liquibase.getDatabase()); + lockService.waitForLock(); + try { + if (config.validateOnMigrate) { + liquibase.validate(); } + liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels()); + } finally { + lockService.releaseLock(); } - } catch (UnsatisfiedResolutionException e) { - //ignore, the DS is not configured } } + } catch (UnsatisfiedResolutionException e) { + //ignore, the DS is not configured } catch (Exception e) { throw new IllegalStateException("Error starting Liquibase", e); } } + } diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseSchemaProvider.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseSchemaProvider.java index d3bc30ca0ed90..35701758bd6f1 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseSchemaProvider.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseSchemaProvider.java @@ -1,10 +1,7 @@ package io.quarkus.liquibase.runtime; -import jakarta.enterprise.inject.Any; import jakarta.enterprise.inject.UnsatisfiedResolutionException; -import io.quarkus.arc.Arc; -import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.InstanceHandle; import io.quarkus.datasource.runtime.DatabaseSchemaProvider; import io.quarkus.liquibase.LiquibaseFactory; @@ -15,20 +12,11 @@ public class LiquibaseSchemaProvider implements DatabaseSchemaProvider { @Override public void resetDatabase(String dbName) { try { - InjectableInstance liquibaseFactoryInstance = Arc.container() - .select(LiquibaseFactory.class, Any.Literal.INSTANCE); - if (liquibaseFactoryInstance.isUnsatisfied()) { - return; - } - for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { - try { - LiquibaseFactory liquibaseFactory = liquibaseFactoryHandle.get(); - if (liquibaseFactory.getDataSourceName().equals(dbName)) { - doReset(liquibaseFactory); - } - } catch (UnsatisfiedResolutionException e) { - //ignore, the DS is not configured - } + try { + LiquibaseFactory liquibaseFactory = LiquibaseFactoryUtil.getLiquibaseFactory(dbName).get(); + doReset(liquibaseFactory); + } catch (UnsatisfiedResolutionException e) { + //ignore, the DS is not configured } } catch (Exception e) { throw new IllegalStateException("Error starting Liquibase", e); @@ -38,12 +26,7 @@ public void resetDatabase(String dbName) { @Override public void resetAllDatabases() { try { - InjectableInstance liquibaseFactoryInstance = Arc.container() - .select(LiquibaseFactory.class, Any.Literal.INSTANCE); - if (liquibaseFactoryInstance.isUnsatisfied()) { - return; - } - for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { + for (InstanceHandle liquibaseFactoryHandle : LiquibaseFactoryUtil.getActiveLiquibaseFactories()) { try { LiquibaseFactory liquibaseFactory = liquibaseFactoryHandle.get(); doReset(liquibaseFactory); diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseFactoriesSupplier.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseFactoriesSupplier.java index ccd6b43863542..87a10e56483fd 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseFactoriesSupplier.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseFactoriesSupplier.java @@ -1,35 +1,25 @@ package io.quarkus.liquibase.runtime.devui; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.Set; import java.util.TreeSet; import java.util.function.Supplier; -import jakarta.enterprise.inject.Any; - -import io.quarkus.arc.Arc; -import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.InstanceHandle; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.liquibase.runtime.LiquibaseFactoryUtil; public class LiquibaseFactoriesSupplier implements Supplier> { @Override public Collection get() { - InjectableInstance liquibaseFactoryInstance = Arc.container().select(LiquibaseFactory.class, - Any.Literal.INSTANCE); - if (liquibaseFactoryInstance.isUnsatisfied()) { - return Collections.emptySet(); - } - - Set liquibaseFactories = new TreeSet<>(LiquibaseFactoryComparator.INSTANCE); - for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { - liquibaseFactories.add(liquibaseFactoryHandle.get()); + Set containers = new TreeSet<>(LiquibaseFactoryComparator.INSTANCE); + for (InstanceHandle handle : LiquibaseFactoryUtil.getActiveLiquibaseFactories()) { + containers.add(handle.get()); } - return liquibaseFactories; + return containers; } private static class LiquibaseFactoryComparator implements Comparator { diff --git a/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java b/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java index 60187addf5680..4c94cc18e68c5 100644 --- a/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java +++ b/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceConfigurationHandlerBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; @@ -208,6 +209,7 @@ private void createPoolIfDefined(DB2PoolRecorder recorder, .addType(Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -222,6 +224,7 @@ private void createPoolIfDefined(DB2PoolRecorder recorder, .addType(io.vertx.mutiny.sqlclient.Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(recorder.mutinyDB2Pool(poolFunction)) .unremovable() .setRuntimeInit(); diff --git a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java index d5e372b88779b..85e16402885f6 100644 --- a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java +++ b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java @@ -25,6 +25,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.ConnectOptionsSupplier; @@ -91,6 +92,9 @@ private DB2Pool initialize(VertxInternal vertx, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, DataSourceReactiveDB2Config dataSourceReactiveDB2Config, SyntheticCreationalContext context) { + if (context.getInjectedReference(DataSourceSupport.class).getInactiveNames().contains(dataSourceName)) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveDB2Config); DB2ConnectOptions db2ConnectOptions = toConnectOptions(dataSourceName, dataSourceRuntimeConfig, diff --git a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/health/ReactiveDB2DataSourcesHealthCheck.java b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/health/ReactiveDB2DataSourcesHealthCheck.java index 72c046f4c33cb..fe4d3a860c1a7 100644 --- a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/health/ReactiveDB2DataSourcesHealthCheck.java +++ b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/health/ReactiveDB2DataSourcesHealthCheck.java @@ -20,7 +20,7 @@ import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.vertx.mutiny.db2client.DB2Pool; @@ -37,8 +37,8 @@ class ReactiveDB2DataSourcesHealthCheck implements HealthCheck { @PostConstruct protected void init() { ArcContainer container = Arc.container(); - DataSourcesHealthSupport excluded = container.instance(DataSourcesHealthSupport.class).get(); - Set excludedNames = excluded.getExcludedNames(); + DataSourceSupport support = container.instance(DataSourceSupport.class).get(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (InstanceHandle handle : container.select(DB2Pool.class, Any.Literal.INSTANCE).handles()) { String db2PoolName = getDB2PoolName(handle.getBean()); if (!excludedNames.contains(db2PoolName)) { diff --git a/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java b/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java index 2707e8aba1424..4dcafa3c6ab01 100644 --- a/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java +++ b/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceConfigurationHandlerBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; @@ -207,6 +208,7 @@ private void createPoolIfDefined(MSSQLPoolRecorder recorder, .addType(Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -221,6 +223,7 @@ private void createPoolIfDefined(MSSQLPoolRecorder recorder, .addType(io.vertx.mutiny.sqlclient.Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(recorder.mutinyMSSQLPool(poolFunction)) .unremovable() .setRuntimeInit(); diff --git a/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseDefaultDatasourceTest.java b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..8ad9fbeb363d6 --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,133 @@ +package io.quarkus.reactive.mssql.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mssqlclient.MSSQLPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + MSSQLPool pool = Arc.container().instance(MSSQLPool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.mssqlclient.MSSQLPool pool = Arc.container().instance(io.vertx.mutiny.mssqlclient.MSSQLPool.class) + .get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseNamedDatasourceTest.java b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..352e3bb17086f --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,140 @@ +package io.quarkus.reactive.mssql.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mssqlclient.MSSQLPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "mssql"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + MSSQLPool pool = Arc.container().instance(MSSQLPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.mssqlclient.MSSQLPool pool = Arc.container().instance(io.vertx.mutiny.mssqlclient.MSSQLPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @ReactiveDataSource("ds-1") + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/DataSourceHealthCheckConfigActiveFalseTest.java b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/DataSourceHealthCheckConfigActiveFalseTest.java new file mode 100644 index 0000000000000..08f702e03f64d --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/DataSourceHealthCheckConfigActiveFalseTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.mssql.client; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class DataSourceHealthCheckConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .withConfigurationResource("application-default-datasource.properties") + .overrideConfigKey("quarkus.datasource.health.enabled", "true") + // this data source is broken, but will be deactivated, + // so the overall check should pass + .overrideConfigKey("quarkus.datasource.brokenDS.db-kind", "mssql") + .overrideConfigKey("quarkus.datasource.brokenDS.reactive.url", "BROKEN") + .overrideConfigKey("quarkus.datasource.brokenDS.active", "false"); + + @Test + public void testDataSourceHealthCheckExclusion() { + RestAssured.when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } + +} diff --git a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java index dad7ed85e5f86..b3c2c8cf1da72 100644 --- a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java +++ b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java @@ -25,6 +25,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.ConnectOptionsSupplier; @@ -61,6 +62,9 @@ public Function, MSSQLPool> configureMSSQL return new Function<>() { @Override public MSSQLPool apply(SyntheticCreationalContext context) { + if (context.getInjectedReference(DataSourceSupport.class).getInactiveNames().contains(dataSourceName)) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } MSSQLPool pool = initialize((VertxInternal) vertx.getValue(), eventLoopCount.get(), dataSourceName, diff --git a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/health/ReactiveMSSQLDataSourcesHealthCheck.java b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/health/ReactiveMSSQLDataSourcesHealthCheck.java index 35537917a3399..a9a2c28a34685 100644 --- a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/health/ReactiveMSSQLDataSourcesHealthCheck.java +++ b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/health/ReactiveMSSQLDataSourcesHealthCheck.java @@ -11,7 +11,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.reactive.datasource.runtime.ReactiveDatasourceHealthCheck; import io.vertx.mssqlclient.MSSQLPool; @@ -26,8 +26,8 @@ public ReactiveMSSQLDataSourcesHealthCheck() { @PostConstruct protected void init() { ArcContainer container = Arc.container(); - DataSourcesHealthSupport excluded = container.instance(DataSourcesHealthSupport.class).get(); - Set excludedNames = excluded.getExcludedNames(); + DataSourceSupport support = container.instance(DataSourceSupport.class).get(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (InstanceHandle handle : container.select(MSSQLPool.class, Any.Literal.INSTANCE).handles()) { String poolName = getPoolName(handle.getBean()); if (!excludedNames.contains(poolName)) { diff --git a/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java b/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java index 4bb36e3156dd0..2d03f42ef6e7a 100644 --- a/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java +++ b/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceConfigurationHandlerBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; @@ -208,6 +209,7 @@ private void createPoolIfDefined(MySQLPoolRecorder recorder, .addType(Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -222,6 +224,7 @@ private void createPoolIfDefined(MySQLPoolRecorder recorder, .addType(io.vertx.mutiny.sqlclient.Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(recorder.mutinyMySQLPool(poolFunction)) .unremovable() .setRuntimeInit(); diff --git a/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseDefaultDatasourceTest.java b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..0a7e67f1f6fe1 --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,133 @@ +package io.quarkus.reactive.mysql.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mysqlclient.MySQLPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + MySQLPool pool = Arc.container().instance(MySQLPool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.mysqlclient.MySQLPool pool = Arc.container().instance(io.vertx.mutiny.mysqlclient.MySQLPool.class) + .get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseNamedDatasourceTest.java b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..5a6ba912c67cd --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,140 @@ +package io.quarkus.reactive.mysql.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mysqlclient.MySQLPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "mysql"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + MySQLPool pool = Arc.container().instance(MySQLPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.mysqlclient.MySQLPool pool = Arc.container().instance(io.vertx.mutiny.mysqlclient.MySQLPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @ReactiveDataSource("ds-1") + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/DataSourceHealthCheckConfigActiveFalseTest.java b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/DataSourceHealthCheckConfigActiveFalseTest.java new file mode 100644 index 0000000000000..40f9830df3787 --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/DataSourceHealthCheckConfigActiveFalseTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.mysql.client; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class DataSourceHealthCheckConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .withConfigurationResource("application-default-datasource.properties") + .overrideConfigKey("quarkus.datasource.health.enabled", "true") + // this data source is broken, but will be deactivated, + // so the overall check should pass + .overrideConfigKey("quarkus.datasource.brokenDS.db-kind", "mysql") + .overrideConfigKey("quarkus.datasource.brokenDS.reactive.url", "BROKEN") + .overrideConfigKey("quarkus.datasource.brokenDS.active", "false"); + + @Test + public void testDataSourceHealthCheckExclusion() { + RestAssured.when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } + +} diff --git a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java index 8b0d101285326..fda8b5372f2a1 100644 --- a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java +++ b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java @@ -25,6 +25,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.ConnectOptionsSupplier; @@ -91,6 +92,9 @@ private MySQLPool initialize(VertxInternal vertx, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, DataSourceReactiveMySQLConfig dataSourceReactiveMySQLConfig, SyntheticCreationalContext context) { + if (context.getInjectedReference(DataSourceSupport.class).getInactiveNames().contains(dataSourceName)) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveMySQLConfig); List mySQLConnectOptions = toMySQLConnectOptions(dataSourceName, dataSourceRuntimeConfig, diff --git a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/health/ReactiveMySQLDataSourcesHealthCheck.java b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/health/ReactiveMySQLDataSourcesHealthCheck.java index 81470e5d9d0e6..656d585acce53 100644 --- a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/health/ReactiveMySQLDataSourcesHealthCheck.java +++ b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/health/ReactiveMySQLDataSourcesHealthCheck.java @@ -11,7 +11,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.reactive.datasource.runtime.ReactiveDatasourceHealthCheck; import io.vertx.mysqlclient.MySQLPool; @@ -26,8 +26,8 @@ public ReactiveMySQLDataSourcesHealthCheck() { @PostConstruct protected void init() { ArcContainer container = Arc.container(); - DataSourcesHealthSupport excluded = container.instance(DataSourcesHealthSupport.class).get(); - Set excludedNames = excluded.getExcludedNames(); + DataSourceSupport support = container.instance(DataSourceSupport.class).get(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (InstanceHandle handle : container.select(MySQLPool.class, Any.Literal.INSTANCE).handles()) { String poolName = getPoolName(handle.getBean()); if (!excludedNames.contains(poolName)) { diff --git a/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java b/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java index ab1cf2dceff79..ee5ed3fd99376 100644 --- a/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java +++ b/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceConfigurationHandlerBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; @@ -209,6 +210,7 @@ private void createPoolIfDefined(OraclePoolRecorder recorder, .addType(Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -223,6 +225,7 @@ private void createPoolIfDefined(OraclePoolRecorder recorder, .addType(io.vertx.mutiny.sqlclient.Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(recorder.mutinyOraclePool(poolFunction)) .unremovable() .setRuntimeInit(); diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseDefaultDatasourceTest.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..d71964ecbf67d --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,133 @@ +package io.quarkus.reactive.oracle.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.oracleclient.OraclePool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + OraclePool pool = Arc.container().instance(OraclePool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.oracleclient.OraclePool pool = Arc.container().instance(io.vertx.mutiny.oracleclient.OraclePool.class) + .get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseNamedDatasourceTest.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..546c7179e563a --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,140 @@ +package io.quarkus.reactive.oracle.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.oracleclient.OraclePool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "oracle"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + OraclePool pool = Arc.container().instance(OraclePool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.oracleclient.OraclePool pool = Arc.container().instance(io.vertx.mutiny.oracleclient.OraclePool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @ReactiveDataSource("ds-1") + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/DataSourceHealthCheckConfigActiveFalseTest.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/DataSourceHealthCheckConfigActiveFalseTest.java new file mode 100644 index 0000000000000..7791728100211 --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/DataSourceHealthCheckConfigActiveFalseTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.oracle.client; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class DataSourceHealthCheckConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .withConfigurationResource("application-default-datasource.properties") + .overrideConfigKey("quarkus.datasource.health.enabled", "true") + // this data source is broken, but will be deactivated, + // so the overall check should pass + .overrideConfigKey("quarkus.datasource.brokenDS.db-kind", "oracle") + .overrideConfigKey("quarkus.datasource.brokenDS.reactive.url", "BROKEN") + .overrideConfigKey("quarkus.datasource.brokenDS.active", "false"); + + @Test + public void testDataSourceHealthCheckExclusion() { + RestAssured.when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } + +} diff --git a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java index dc49a6eafd261..54ef9588855e6 100644 --- a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java +++ b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java @@ -19,6 +19,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.ConnectOptionsSupplier; @@ -87,6 +88,9 @@ private OraclePool initialize(VertxInternal vertx, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, DataSourceReactiveOracleConfig dataSourceReactiveOracleConfig, SyntheticCreationalContext context) { + if (context.getInjectedReference(DataSourceSupport.class).getInactiveNames().contains(dataSourceName)) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveOracleConfig); OracleConnectOptions oracleConnectOptions = toOracleConnectOptions(dataSourceName, dataSourceRuntimeConfig, diff --git a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/health/ReactiveOracleDataSourcesHealthCheck.java b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/health/ReactiveOracleDataSourcesHealthCheck.java index fa462c96ac2b5..cc3370616f4eb 100644 --- a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/health/ReactiveOracleDataSourcesHealthCheck.java +++ b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/health/ReactiveOracleDataSourcesHealthCheck.java @@ -11,7 +11,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.reactive.datasource.runtime.ReactiveDatasourceHealthCheck; import io.vertx.oracleclient.OraclePool; @@ -26,8 +26,8 @@ public ReactiveOracleDataSourcesHealthCheck() { @PostConstruct protected void init() { ArcContainer container = Arc.container(); - DataSourcesHealthSupport excluded = container.instance(DataSourcesHealthSupport.class).get(); - Set excludedNames = excluded.getExcludedNames(); + DataSourceSupport support = container.instance(DataSourceSupport.class).get(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (InstanceHandle handle : container.select(OraclePool.class, Any.Literal.INSTANCE).handles()) { String poolName = getPoolName(handle.getBean()); if (!excludedNames.contains(poolName)) { diff --git a/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java b/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java index cce55cfa31a5c..9f87c5535811b 100644 --- a/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java +++ b/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceConfigurationHandlerBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; @@ -213,6 +214,7 @@ private void createPoolIfDefined(PgPoolRecorder recorder, .addType(Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -227,6 +229,7 @@ private void createPoolIfDefined(PgPoolRecorder recorder, .addType(io.vertx.mutiny.sqlclient.Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(recorder.mutinyPgPool(poolFunction)) .unremovable() .setRuntimeInit(); diff --git a/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseDefaultDatasourceTest.java b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..8dbb1f8094e8a --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,133 @@ +package io.quarkus.reactive.pg.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.pgclient.PgPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + PgPool pool = Arc.container().instance(PgPool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.pgclient.PgPool pool = Arc.container().instance(io.vertx.mutiny.pgclient.PgPool.class) + .get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseNamedDatasourceTest.java b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..e40b129e6cabd --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,140 @@ +package io.quarkus.reactive.pg.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.pgclient.PgPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "postgresql"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + PgPool pool = Arc.container().instance(PgPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.pgclient.PgPool pool = Arc.container().instance(io.vertx.mutiny.pgclient.PgPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @ReactiveDataSource("ds-1") + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/DataSourceHealthCheckConfigActiveFalseTest.java b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/DataSourceHealthCheckConfigActiveFalseTest.java new file mode 100644 index 0000000000000..1b9c7d61639ff --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/DataSourceHealthCheckConfigActiveFalseTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.pg.client; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class DataSourceHealthCheckConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .withConfigurationResource("application-default-datasource.properties") + .overrideConfigKey("quarkus.datasource.health.enabled", "true") + // this data source is broken, but will be deactivated, + // so the overall check should pass + .overrideConfigKey("quarkus.datasource.brokenDS.db-kind", "postgresql") + .overrideConfigKey("quarkus.datasource.brokenDS.reactive.url", "BROKEN") + .overrideConfigKey("quarkus.datasource.brokenDS.active", "false"); + + @Test + public void testDataSourceHealthCheckExclusion() { + RestAssured.when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } + +} diff --git a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java index 18149f6e67b1a..4afab742f0163 100644 --- a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java +++ b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java @@ -24,6 +24,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.ConnectOptionsSupplier; @@ -90,6 +91,9 @@ private PgPool initialize(VertxInternal vertx, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, DataSourceReactivePostgreSQLConfig dataSourceReactivePostgreSQLConfig, SyntheticCreationalContext context) { + if (context.getInjectedReference(DataSourceSupport.class).getInactiveNames().contains(dataSourceName)) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactivePostgreSQLConfig); List pgConnectOptionsList = toPgConnectOptions(dataSourceName, dataSourceRuntimeConfig, diff --git a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/health/ReactivePgDataSourcesHealthCheck.java b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/health/ReactivePgDataSourcesHealthCheck.java index c0228a3c99869..9cfc47a61dc2a 100644 --- a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/health/ReactivePgDataSourcesHealthCheck.java +++ b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/health/ReactivePgDataSourcesHealthCheck.java @@ -11,7 +11,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.reactive.datasource.runtime.ReactiveDatasourceHealthCheck; import io.vertx.pgclient.PgPool; @@ -26,8 +26,8 @@ public ReactivePgDataSourcesHealthCheck() { @PostConstruct protected void init() { ArcContainer container = Arc.container(); - DataSourcesHealthSupport excluded = container.instance(DataSourcesHealthSupport.class).get(); - Set excludedNames = excluded.getExcludedNames(); + DataSourceSupport support = container.instance(DataSourceSupport.class).get(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (InstanceHandle handle : container.select(PgPool.class, Any.Literal.INSTANCE).handles()) { String poolName = getPoolName(handle.getBean()); if (!excludedNames.contains(poolName)) {