Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Use Arc features in datasource extensions for eager startup and active/inactive #41929

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
48 changes: 29 additions & 19 deletions docs/src/main/asciidoc/datasource.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -445,22 +445,26 @@
[[datasource-active]]
=== Activate or deactivate datasources

When a datasource is configured at build time, it is active by default at runtime.
When a datasource is configured at build time, it is active at runtime as soon as its URL is set.

Check warning on line 448 in docs/src/main/asciidoc/datasource.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/datasource.adoc", "range": {"start": {"line": 448, "column": 67}}}, "severity": "INFO"}
This means that Quarkus will start the corresponding JDBC connection pool or reactive client when the application starts.

To deactivate a datasource at runtime, set `quarkus.datasource[.optional name].active` to `false`.
Quarkus will then skip starting the JDBC connection pool or reactive client during application startup.
Any attempt to use the deactivated datasource at runtime results in an exception.
To deactivate a datasource at runtime, either:

This feature is especially useful when you need the application to select one datasource from a predefined set at runtime.
* Do not set `quarkus.datasource[.optional name].jdbc.url`/`quarkus.datasource[.optional name].reactive.url`.
* Or set `quarkus.datasource[.optional name].active` to `false`.

[WARNING]
====
If another Quarkus extension relies on an inactive datasource, that extension might fail to start.
If a datasource is not active:

* The datasource will not attempt to connect to the database during application startup.
* The datasource will not contribute a <<datasource-health-check,health check>>.
* Static CDI injection points involving the datasource (`@Inject DataSource ds` or `@Inject Pool pool`) will cause application startup to fail.
* Dynamic retrieval of the datasource (e.g. through `CDI.getBeanContainer()`/`Arc.instance()`, or by injecting an `Instance<DataSource>`) will cause an exception to be thrown.

Check warning on line 461 in docs/src/main/asciidoc/datasource.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'for example' rather than 'e.g.' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'for example' rather than 'e.g.' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/datasource.adoc", "range": {"start": {"line": 461, "column": 35}}}, "severity": "WARNING"}
* Other Quarkus extensions consuming the datasource may cause application startup to fail.

Check warning on line 462 in docs/src/main/asciidoc/datasource.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possibility)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possibility)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/datasource.adoc", "range": {"start": {"line": 462, "column": 48}}}, "severity": "WARNING"}
+
In such a case, you will need to deactivate that other extension as well.

Check warning on line 464 in docs/src/main/asciidoc/datasource.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/datasource.adoc", "range": {"start": {"line": 464, "column": 21}}}, "severity": "INFO"}

Check warning on line 464 in docs/src/main/asciidoc/datasource.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/datasource.adoc", "range": {"start": {"line": 464, "column": 61}}}, "severity": "INFO"}
For an example of this scenario, see the xref:hibernate-orm.adoc#persistence-unit-active[Hibernate ORM] section.
====
For an example of this scenario, see xref:hibernate-orm.adoc#persistence-unit-active[this section of the Hibernate ORM guide].

This feature is especially useful when you need the application to select one datasource from a predefined set at runtime.

For example, with the following configuration:

Expand Down Expand Up @@ -504,28 +508,33 @@
[source,java,indent=0]
----
public class MyProducer {
@Inject
DataSourceSupport dataSourceSupport;

@Inject
@DataSource("pg")
AgroalDataSource pgDataSourceBean;
InjectableInstance<AgroalDataSource> pgDataSourceBean; // <1>

@Inject
@DataSource("oracle")
AgroalDataSource oracleDataSourceBean;
InjectableInstance<AgroalDataSource> oracleDataSourceBean;

@Produces
@Produces // <2>
@ApplicationScoped
public AgroalDataSource dataSource() {
if (dataSourceSupport.getInactiveNames().contains("pg")) {
return oracleDataSourceBean;
if (pgDataSourceBean.getHandle().getBean().isActive()) { // <3>
return pgDataSourceBean.get();
} else if (oracleDataSourceBean.getHandle().getBean().isActive()) { // <3>
return oracleDataSourceBean.get();
} else {
return pgDataSourceBean;
throw new RuntimeException("No active datasource!");
}
}
}
----
<1> Don't inject a `DataSource` or `AgroalDatasource` directly,
because that would lead to a failure on startup (can't inject inactive beans).
Instead, inject `InjectableInstance<DataSource>` or `InjectableInstance<AgroalDataSource>`
<2> Declare a CDI producer method that will define the default datasource

Check warning on line 535 in docs/src/main/asciidoc/datasource.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/datasource.adoc", "range": {"start": {"line": 535, "column": 65}}}, "severity": "INFO"}
as either PostgreSQL or Oracle, depending on what is active.
<3> Check whether beans are active before retrieving them.
====

[[datasource-multiple-single-transaction]]
Expand Down Expand Up @@ -589,6 +598,7 @@

== Datasource integrations

[[datasource-health-check]]
=== Datasource health check

If you use the link:https://quarkus.io/extensions/io.quarkus/quarkus-smallrye-health[`quarkus-smallrye-health`] extension, the `quarkus-agroal` and reactive client extensions automatically add a readiness health check to validate the datasource.
Expand Down
19 changes: 14 additions & 5 deletions docs/src/main/asciidoc/hibernate-orm.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -497,11 +497,13 @@
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.
If a persistence unit is not active:

* The `SessionFactory` will not start during application startup.
* Accessing the `EntityManagerFactory`/`EntityManager` or `SessionFactory`/`Session` will cause an exception to be thrown.

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].

Check warning on line 506 in docs/src/main/asciidoc/hibernate-orm.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/hibernate-orm.adoc", "range": {"start": {"line": 506, "column": 12}}}, "severity": "INFO"}

For example, with the following configuration:

Expand Down Expand Up @@ -558,7 +560,12 @@
----
public class MyProducer {
@Inject
DataSourceSupport dataSourceSupport;
@DataSource("pg")
InjectableInstance<AgroalDataSource> pgDataSourceBean;

@Inject
@DataSource("oracle")
InjectableInstance<AgroalDataSource> oracleDataSourceBean;

@Inject
@PersistenceUnit("pg")
Expand All @@ -571,10 +578,12 @@
@Produces
@ApplicationScoped
public Session session() {
if (dataSourceSupport.getInactiveNames().contains("pg")) {
if (pgDataSourceBean.getHandle().getBean().isActive()) {
return pgSessionBean;
} else if (oracleDataSourceBean.getHandle().getBean().isActive()) {
return oracleSessionBean;
} else {
return pgSessionBean;
throw new RuntimeException("No active datasource!");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
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.DataSources;
Expand All @@ -36,6 +35,7 @@
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
import io.quarkus.agroal.spi.JdbcDriverBuildItem;
import io.quarkus.agroal.spi.OpenTelemetryInitBuildItem;
import io.quarkus.arc.BeanDestroyer;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
Expand Down Expand Up @@ -243,8 +243,6 @@ void generateDataSourceSupportBean(AgroalRecorder recorder,
.setDefaultScope(DotNames.SINGLETON).build());
// add the @DataSource class otherwise it won't be registered as a qualifier
additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClass(DataSource.class).build());
// make sure datasources are initialized at startup
additionalBeans.produce(new AdditionalBeanBuildItem(AgroalDataSourcesInitializer.class));

// make AgroalPoolInterceptor beans unremovable, users still have to make them beans
unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(AgroalPoolInterceptor.class));
Expand Down Expand Up @@ -288,9 +286,12 @@ void generateDataSourceBeans(AgroalRecorder recorder,
.setRuntimeInit()
.unremovable()
.addInjectionPoint(ClassType.create(DotName.createSimple(DataSources.class)))
.startup()
.checkActive(recorder.agroalDataSourceCheckActiveSupplier(dataSourceName))
// pass the runtime config into the recorder to ensure that the DataSource related beans
// are created after runtime configuration has been set up
.createWith(recorder.agroalDataSourceSupplier(dataSourceName, dataSourcesRuntimeConfig));
.createWith(recorder.agroalDataSourceSupplier(dataSourceName, dataSourcesRuntimeConfig))
.destroyer(BeanDestroyer.AutoCloseableDestroyer.class);

if (!DataSourceUtil.isDefault(dataSourceName)) {
// this definitely not ideal, but 'elytron-jdbc-security' uses it (although it could be easily changed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
import org.junit.jupiter.api.extension.RegisterExtension;

import io.agroal.api.AgroalDataSource;
import io.quarkus.arc.InactiveBeanException;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableInstance;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.test.QuarkusUnitTest;

public class ConfigActiveFalseDefaultDatasourceDynamicInjectionTest {
Expand Down Expand Up @@ -41,16 +42,17 @@ private void doTest(InjectableInstance<? extends DataSource> instance) {
// 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 proxy cannot be null.
assertThat(instance.getHandle().getBean())
.isNotNull()
.returns(false, InjectableBean::isActive);
var ds = instance.get();
assertThat(ds).isNotNull();
// However, any attempt to use it at runtime will fail.
assertThatThrownBy(() -> ds.getConnection())
.isInstanceOf(RuntimeException.class)
.cause()
.isInstanceOf(ConfigurationException.class)
.isInstanceOf(InactiveBeanException.class)
.hasMessageContainingAll("Datasource '<default>' 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 avoid this exception while keeping the bean inactive", // Message from Arc with generic hints
"To activate the datasource, set configuration property 'quarkus.datasource.active'"
+ " to 'true' and configure datasource '<default>'",
"Refer to https://quarkus.io/guides/datasource for guidance.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,46 @@
package io.quarkus.agroal.test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.sql.SQLException;
import static org.assertj.core.api.Assertions.assertThat;

import javax.sql.DataSource;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.CreationException;
import jakarta.inject.Inject;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.InactiveBeanException;
import io.quarkus.test.QuarkusUnitTest;

public class ConfigActiveFalseDefaultDatasourceStaticInjectionTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.overrideConfigKey("quarkus.datasource.active", "false");
.overrideConfigKey("quarkus.datasource.active", "false")
.assertException(e -> assertThat(e)
// Can't use isInstanceOf due to weird classloading in tests
.satisfies(t -> assertThat(t.getClass().getName()).isEqualTo(InactiveBeanException.class.getName()))
.hasMessageContainingAll("Datasource '<default>' was deactivated through configuration properties.",
"To avoid this exception while keeping the bean inactive", // Message from Arc with generic hints
"To activate the datasource, set configuration property 'quarkus.datasource.active'"
+ " to 'true' and configure datasource '<default>'",
"Refer to https://quarkus.io/guides/datasource for guidance.",
"This bean is injected into",
MyBean.class.getName() + "#ds"));

@Inject
MyBean myBean;

@Test
public void test() {
assertThatThrownBy(() -> myBean.useDatasource())
.isInstanceOf(CreationException.class)
.hasMessageContainingAll("Datasource '<default>' 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 '<default>'",
"Refer to https://quarkus.io/guides/datasource for guidance.");
Assertions.fail("Startup should have failed");
}

@ApplicationScoped
public static class MyBean {
@Inject
DataSource ds;

public void useDatasource() throws SQLException {
ds.getConnection();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
import org.junit.jupiter.api.extension.RegisterExtension;

import io.agroal.api.AgroalDataSource;
import io.quarkus.arc.InactiveBeanException;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableInstance;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.test.QuarkusUnitTest;

public class ConfigActiveFalseNamedDatasourceDynamicInjectionTest {
Expand Down Expand Up @@ -46,16 +47,17 @@ private void doTest(InjectableInstance<? extends DataSource> instance) {
// 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(instance.getHandle().getBean())
.isNotNull()
.returns(false, InjectableBean::isActive);
var ds = instance.get();
assertThat(ds).isNotNull();
// However, any attempt to use it at runtime will fail.
assertThatThrownBy(() -> ds.getConnection())
.isInstanceOf(RuntimeException.class)
.cause()
.isInstanceOf(ConfigurationException.class)
.isInstanceOf(InactiveBeanException.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 avoid this exception while keeping the bean inactive", // Message from Arc with generic hints
"To activate the datasource, set configuration property 'quarkus.datasource.\"users\".active'"
+ " to 'true' and configure datasource 'users'",
"Refer to https://quarkus.io/guides/datasource for guidance.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
package io.quarkus.agroal.test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.sql.SQLException;
import static org.assertj.core.api.Assertions.assertThat;

import javax.sql.DataSource;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.CreationException;
import jakarta.inject.Inject;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.InactiveBeanException;
import io.quarkus.test.QuarkusUnitTest;

public class ConfigActiveFalseNamedDatasourceStaticInjectionTest {
Expand All @@ -22,30 +21,30 @@ public class ConfigActiveFalseNamedDatasourceStaticInjectionTest {
.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");
.overrideConfigKey("quarkus.datasource.users.db-kind", "h2")
.assertException(e -> assertThat(e)
// Can't use isInstanceOf due to weird classloading in tests
.satisfies(t -> assertThat(t.getClass().getName()).isEqualTo(InactiveBeanException.class.getName()))
.hasMessageContainingAll("Datasource 'users' was deactivated through configuration properties.",
"To avoid this exception while keeping the bean inactive", // Message from Arc with generic hints
"To activate the datasource, set configuration property 'quarkus.datasource.\"users\".active'"
+ " to 'true' and configure datasource 'users'",
"Refer to https://quarkus.io/guides/datasource for guidance.",
"This bean is injected into",
MyBean.class.getName() + "#ds"));

@Inject
MyBean myBean;

@Test
public void test() {
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.");
Assertions.fail("Startup should have failed");
}

@ApplicationScoped
public static class MyBean {
@Inject
@io.quarkus.agroal.DataSource("users")
DataSource ds;

public void useDatasource() throws SQLException {
ds.getConnection();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
import org.junit.jupiter.api.extension.RegisterExtension;

import io.agroal.api.AgroalDataSource;
import io.quarkus.arc.InactiveBeanException;
import io.quarkus.arc.InjectableInstance;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.test.QuarkusUnitTest;

public class ConfigUrlMissingDefaultDatasourceDynamicInjectionTest {
Expand Down Expand Up @@ -46,7 +46,10 @@ private void doTest(InjectableInstance<? extends DataSource> instance) {
assertThat(ds).isNotNull();
// However, any attempt to use it at runtime will fail.
assertThatThrownBy(() -> ds.getConnection())
.isInstanceOf(ConfigurationException.class)
.hasMessageContainingAll("quarkus.datasource.jdbc.url has not been defined");
.isInstanceOf(InactiveBeanException.class)
.hasMessageContainingAll("Datasource '<default>' was deactivated automatically because its URL is not set.",
"To avoid this exception while keeping the bean inactive", // Message from Arc with generic hints
"To activate the datasource, set configuration property 'quarkus.datasource.jdbc.url'",
"Refer to https://quarkus.io/guides/datasource for guidance.");
}
}
Loading