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

Merged
merged 10 commits into from
Oct 18, 2024
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/cdi-integration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ public class TestRecorder {
----
<1> Pass a contextual reference of `Bar` to the constructor of `Foo`.

[[inactive-synthetic-beans]]
=== Inactive Synthetic Beans

In the case when one needs to register multiple synthetic beans at build time but only wants a subset of them active at runtime, it is useful to be able to mark a synthetic bean as _inactive_.
Expand Down
87 changes: 62 additions & 25 deletions docs/src/main/asciidoc/datasource.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -445,22 +445,26 @@ AgroalDataSource inventoryDataSource;
[[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 and its URL is set at runtime, it is active by default.
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:

In such a case, you will need to deactivate that other extension as well.
For an example of this scenario, see the xref:hibernate-orm.adoc#persistence-unit-active[Hibernate ORM] section.
====
* 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.
* Other Quarkus extensions consuming the datasource may cause application startup to fail.
+
In such a case, you will also need to deactivate those other extensions.
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 @@ -497,36 +501,68 @@ xref:config-reference.adoc#multiple-profiles[setting `quarkus.profile`]:
----
====

[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:
With such a setup, you will need to take care to only ever access the _active_ datasource.
To do so, you can inject an `InjectableInstance<DataSource>` or `InjectableInstance<Pool>` with an `@Any` qualifier, and call xref:cdi-integration.adoc#inactive-synthetic-beans[`getActive()`]:

[source,java,indent=0]
[source,java]
----
public class MyProducer {
import io.quarkus.arc.InjectableInstance;
@ApplicationScoped
public class MyConsumer {
@Inject
DataSourceSupport dataSourceSupport;
@Any
InjectableInstance<DataSource> dataSource;

public void doSomething() {
DataSource activeDataSource = dataSource.getActive();
// ...
}
}
----

Alternatively, you may define a xref:cdi.adoc#ok-you-said-that-there-are-several-kinds-of-beans[CDI bean producer] for the default datasource redirecting to the currently active named datasource, so that it can be injected directly, like this:

[source,java,indent=0]
----
public class MyProducer {
@Inject
@DataSource("pg")
AgroalDataSource pgDataSourceBean;
InjectableInstance<DataSource> pgDataSourceBean; // <1>

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

@Produces
@Produces // <2>
@ApplicationScoped
public AgroalDataSource dataSource() {
if (dataSourceSupport.getInactiveNames().contains("pg")) {
return oracleDataSourceBean;
public DataSource dataSource() {
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!");
}
}
}

@ApplicationScoped
public class MyConsumer {
@Inject
DataSource dataSource; // <4>

public void doSomething() {
// .. just use the injected 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
as either PostgreSQL or Oracle, depending on what is active.
<3> Check whether beans are active before retrieving them.
<4> This will get injected with the (only) active datasource.

[[datasource-multiple-single-transaction]]
=== Use multiple datasources in a single transaction
Expand Down Expand Up @@ -589,6 +625,7 @@ explaining why.

== 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
45 changes: 34 additions & 11 deletions docs/src/main/asciidoc/hibernate-orm.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -506,8 +506,10 @@ 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.
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].
Expand Down Expand Up @@ -558,16 +560,19 @@ xref:config-reference.adoc#multiple-profiles[setting `quarkus.profile`]:
----
====

[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:
With such a setup, you will need to take care to only ever access the _active_ persistence unit.
To do so, you may define a xref:cdi.adoc#ok-you-said-that-there-are-several-kinds-of-beans[CDI bean producer] for the default `Session` redirecting to the currently active named `Session`, so that it can be injected directly, like this:

[source,java,indent=0]
----
public class MyProducer {
@Inject
DataSourceSupport dataSourceSupport;
@DataSource("pg")
InjectableInstance<AgroalDataSource> pgDataSourceBean; // <1>

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

@Inject
@PersistenceUnit("pg")
Expand All @@ -577,18 +582,36 @@ public class MyProducer {
@PersistenceUnit("oracle")
Session oracleSessionBean;

@Produces
@Produces // <2>
@ApplicationScoped
public Session session() {
if (dataSourceSupport.getInactiveNames().contains("pg")) {
if (pgDataSourceBean.getHandle().getBean().isActive()) { // <3>
return pgSessionBean;
} else if (oracleDataSourceBean.getHandle().getBean().isActive()) { // <3>
return oracleSessionBean;
} else {
return pgSessionBean;
throw new RuntimeException("No active datasource!");
}
}
}

@ApplicationScoped
public class MyConsumer {
@Inject
Session session; // <4>

public void doSomething() {
// .. just use the injected session ...
}
}
----
====
<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 session
as either PostgreSQL or Oracle, depending on what is active.
<3> Check whether datasource beans are active before retrieving the corresponding session.
<4> This will get injected with the (only) active session.

[[persistence-xml]]
== Setting up and configuring Hibernate ORM with a `persistence.xml`
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
Loading
Loading