Skip to content

Commit

Permalink
Fail on startup when inactive datasources are injected into user beans
Browse files Browse the repository at this point in the history
By reimplementing Datasource inactive/active handling and eager startup
through Arc's native features, which is better integrated and gives us
this behavior.
  • Loading branch information
yrodiere committed Jul 19, 2024
1 parent 1bb43dc commit 5ffad3c
Show file tree
Hide file tree
Showing 69 changed files with 1,724 additions and 1,573 deletions.
23 changes: 14 additions & 9 deletions docs/src/main/asciidoc/datasource.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -497,28 +497,33 @@ like this:
[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 (dataSource1Bean.getHandle().getBean().isActive()) { // <3>
return dataSource1Bean.get();
} else if (dataSource2Bean.getHandle().getBean().isActive()) { // <3>
return dataSource2Bean.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 524 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": 524, "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
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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 @@ -35,6 +34,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 @@ -241,8 +241,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 @@ -286,9 +284,12 @@ void generateDataSourceBeans(AgroalRecorder recorder,
.setRuntimeInit()
.unremovable()
.addInjectionPoint(ClassType.create(DotName.createSimple(DataSources.class)))
.startup()
.isActive(recorder.agroalDataSourceActiveSupplier(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
@@ -0,0 +1,59 @@
package io.quarkus.agroal.test;

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

import javax.sql.DataSource;

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.InactiveBeanException;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableInstance;
import io.quarkus.test.QuarkusUnitTest;

public class ConfigActiveFalseDefaultDatasourceDynamicInjectionTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.overrideConfigKey("quarkus.datasource.active", "false");

@Inject
InjectableInstance<DataSource> dataSource;

@Inject
InjectableInstance<AgroalDataSource> agroalDataSource;

@Test
public void dataSource() {
doTest(dataSource);
}

@Test
public void agroalDataSource() {
doTest(agroalDataSource);
}

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(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 'true' and configure datasource '<default>'",
"Refer to https://quarkus.io/guides/datasource for guidance.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.quarkus.agroal.test;

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

import javax.sql.DataSource;

import jakarta.enterprise.context.ApplicationScoped;
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")
.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 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.",
"This bean is injected into",
MyBean.class.getName() + "#ds"));

@Inject
MyBean myBean;

@Test
public void test() {
Assertions.fail("Startup should have failed");
}

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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.quarkus.agroal.test;

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

import javax.sql.DataSource;

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.InactiveBeanException;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableInstance;
import io.quarkus.test.QuarkusUnitTest;

public class ConfigActiveFalseNamedDatasourceDynamicInjectionTest {

@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
@io.quarkus.agroal.DataSource("users")
InjectableInstance<DataSource> dataSource;

@Inject
@io.quarkus.agroal.DataSource("users")
InjectableInstance<AgroalDataSource> agroalDataSource;

@Test
public void dataSource() {
doTest(dataSource);
}

@Test
public void agroalDataSource() {
doTest(agroalDataSource);
}

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(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 'true' and configure datasource 'users'",
"Refer to https://quarkus.io/guides/datasource for guidance.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.quarkus.agroal.test;

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

import javax.sql.DataSource;

import jakarta.enterprise.context.ApplicationScoped;
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 {

@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")
.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 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.",
"This bean is injected into",
MyBean.class.getName() + "#ds"));

@Inject
MyBean myBean;

@Test
public void test() {
Assertions.fail("Startup should have failed");
}

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

0 comments on commit 5ffad3c

Please sign in to comment.