Skip to content

Commit

Permalink
More consistent exceptions for unconfigured datasources
Browse files Browse the repository at this point in the history
Critically, this opens the way to handling other datasource access
problems, e.g. deactivated datasources.
  • Loading branch information
yrodiere committed Jan 8, 2024
1 parent c857b76 commit 989ce28
Show file tree
Hide file tree
Showing 16 changed files with 114 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ public AgroalDataSource doCreateDataSource(String dataSourceName) {
DataSourceJdbcBuildTimeConfig dataSourceJdbcBuildTimeConfig = dataSourcesJdbcBuildTimeConfig
.dataSources().get(dataSourceName).jdbc();
DataSourceRuntimeConfig dataSourceRuntimeConfig = dataSourcesRuntimeConfig.dataSources().get(dataSourceName);

DataSourceJdbcRuntimeConfig dataSourceJdbcRuntimeConfig = dataSourcesJdbcRuntimeConfig
.getDataSourceJdbcRuntimeConfig(dataSourceName);

Expand Down
4 changes: 4 additions & 0 deletions extensions/datasource/common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
<artifactId>quarkus-datasource-common</artifactId>
<name>Quarkus - Datasource - Common</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import io.quarkus.runtime.configuration.ConfigurationException;

public final class DataSourceUtil {

Expand Down Expand Up @@ -34,6 +38,18 @@ public static List<String> dataSourcePropertyKeys(String datasourceName, String
}
}

public static ConfigurationException dataSourceNotConfigured(String dataSourceName) {
return new ConfigurationException(String.format(Locale.ROOT,
"Datasource '%s' is not configured."
+ " To solve this, configure datasource '%s'."
+ " Refer to https://quarkus.io/guides/datasource for guidance.",
dataSourceName, dataSourceName),
Set.of(dataSourcePropertyKey(dataSourceName, "db-kind"),
dataSourcePropertyKey(dataSourceName, "username"),
dataSourcePropertyKey(dataSourceName, "password"),
dataSourcePropertyKey(dataSourceName, "jdbc.url")));
}

private DataSourceUtil() {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,22 @@ public class FlywayExtensionConfigEmptyDefaultDatasourceTest {
public void testBootSucceedsButFlywayDeactivated() {
assertThatThrownBy(flywayForDefaultDatasource::get)
.isInstanceOf(CreationException.class)
.hasMessageContaining("Cannot get a Flyway instance for unconfigured datasource <default>");
.cause()
.hasMessageContainingAll("Unable to find datasource '<default>' for Flyway",
"Datasource '<default>' is not configured.",
"To solve this, configure datasource '<default>'.",
"Refer to https://quarkus.io/guides/datasource for guidance.");
}

@Test
@DisplayName("If there is no config for the default datasource, the application should boot even if we inject a bean that depends on Liquibase, but actually using Liquibase should fail")
public void testBootSucceedsWithInjectedBeanDependingOnFlywayButFlywayDeactivated() {
assertThatThrownBy(() -> myBean.useFlyway())
.cause()
.hasMessageContaining("Cannot get a Flyway instance for unconfigured datasource <default>");
.hasMessageContainingAll("Unable to find datasource '<default>' for Flyway",
"Datasource '<default>' is not configured.",
"To solve this, configure datasource '<default>'.",
"Refer to https://quarkus.io/guides/datasource for guidance.");
}

@ApplicationScoped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ public class FlywayExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest {
public void testBootSucceedsButFlywayDeactivated() {
assertThatThrownBy(flywayForDefaultDatasource::get)
.isInstanceOf(CreationException.class)
.hasMessageContaining("Cannot get a Flyway instance for unconfigured datasource <default>");
.cause()
.hasMessageContainingAll("Unable to find datasource '<default>' for Flyway",
"Datasource '<default>' is not configured.",
"To solve this, configure datasource '<default>'.",
"Refer to https://quarkus.io/guides/datasource for guidance.");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;

Expand Down Expand Up @@ -31,6 +32,7 @@
import io.quarkus.flyway.FlywayDataSource.FlywayDataSourceLiteral;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.runtime.configuration.ConfigurationException;

@Recorder
public class FlywayRecorder {
Expand Down Expand Up @@ -64,15 +66,24 @@ public Function<SyntheticCreationalContext<FlywayContainer>, FlywayContainer> fl
return new Function<>() {
@Override
public FlywayContainer apply(SyntheticCreationalContext<FlywayContainer> context) {
DataSource dataSource = context.getInjectedReference(DataSources.class).getDataSource(dataSourceName);
if (dataSource instanceof UnconfiguredDataSource) {
return new UnconfiguredDataSourceFlywayContainer(dataSourceName);
DataSource dataSource;
try {
dataSource = context.getInjectedReference(DataSources.class).getDataSource(dataSourceName);
if (dataSource instanceof UnconfiguredDataSource) {
throw DataSourceUtil.dataSourceNotConfigured(dataSourceName);
}
} catch (ConfigurationException e) {
// TODO do we really want to enable retrieval of a FlywayContainer for an unconfigured datasource?
// Assigning ApplicationScoped to the FlywayContainer
// and throwing UnsatisfiedResolutionException on bean creation (first access)
// would probably make more sense.
return new UnconfiguredDataSourceFlywayContainer(dataSourceName, String.format(Locale.ROOT,
"Unable to find datasource '%s' for Flyway: %s",
dataSourceName, e.getMessage()), e);
}

FlywayContainerProducer flywayProducer = context.getInjectedReference(FlywayContainerProducer.class);
FlywayContainer flywayContainer = flywayProducer.createFlyway(dataSource, dataSourceName, hasMigrations,
createPossible);
return flywayContainer;
return flywayProducer.createFlyway(dataSource, dataSourceName, hasMigrations, createPossible);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@

public class UnconfiguredDataSourceFlywayContainer extends FlywayContainer {

public UnconfiguredDataSourceFlywayContainer(String dataSourceName) {
private final String message;
private final Throwable cause;

public UnconfiguredDataSourceFlywayContainer(String dataSourceName, String message, Throwable cause) {
super(null, false, false, false, false, false, dataSourceName, false, false);
this.message = message;
this.cause = cause;
}

@Override
public Flyway getFlyway() {
throw new UnsupportedOperationException(
"Cannot get a Flyway instance for unconfigured datasource " + getDataSourceName());
throw new UnsupportedOperationException(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -871,13 +871,10 @@ private void handleHibernateORMWithNoPersistenceXml(
&& (!hibernateOrmConfig.defaultPersistenceUnit.datasource.isPresent()
|| DataSourceUtil.isDefault(hibernateOrmConfig.defaultPersistenceUnit.datasource.get()))
&& !defaultJdbcDataSource.isPresent()) {
throw new ConfigurationException(
"Model classes are defined for the default persistence unit, but no default datasource was found."
+ " The default EntityManagerFactory will not be created."
+ " To solve this, configure the default datasource."
+ " Refer to https://quarkus.io/guides/datasource for guidance.",
new HashSet<>(Arrays.asList("quarkus.datasource.db-kind", "quarkus.datasource.username",
"quarkus.datasource.password", "quarkus.datasource.jdbc.url")));
String persistenceUnitName = PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME;
String dataSourceName = DataSourceUtil.DEFAULT_DATASOURCE_NAME;
throw PersistenceUnitUtil.unableToFindDataSource(persistenceUnitName, dataSourceName,
DataSourceUtil.dataSourceNotConfigured(dataSourceName));
}

for (Entry<String, HibernateOrmConfigPersistenceUnit> persistenceUnitEntry : hibernateOrmConfig.persistenceUnits
Expand Down Expand Up @@ -1228,14 +1225,12 @@ private static void collectDialectConfigForPersistenceXml(String persistenceUnit
private static Optional<JdbcDataSourceBuildItem> findJdbcDataSource(String persistenceUnitName,
HibernateOrmConfigPersistenceUnit persistenceUnitConfig, List<JdbcDataSourceBuildItem> jdbcDataSources) {
if (persistenceUnitConfig.datasource.isPresent()) {
String dataSourceName = persistenceUnitConfig.datasource.get();
return Optional.of(jdbcDataSources.stream()
.filter(i -> persistenceUnitConfig.datasource.get().equals(i.getName()))
.filter(i -> dataSourceName.equals(i.getName()))
.findFirst()
.orElseThrow(() -> new ConfigurationException(String.format(Locale.ROOT,
"The datasource '%1$s' is not configured but the persistence unit '%2$s' uses it."
+ " To solve this, configure datasource '%1$s'."
+ " Refer to https://quarkus.io/guides/datasource for guidance.",
persistenceUnitConfig.datasource.get(), persistenceUnitName))));
.orElseThrow(() -> PersistenceUnitUtil.unableToFindDataSource(persistenceUnitName, dataSourceName,
DataSourceUtil.dataSourceNotConfigured(dataSourceName))));
} else if (PersistenceUnitUtil.isDefaultPersistenceUnit(persistenceUnitName)) {
return jdbcDataSources.stream()
.filter(i -> i.isDefault())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public class EntitiesInDefaultPUWithExplicitUnconfiguredDatasourceTest {
.assertException(t -> assertThat(t)
.isInstanceOf(ConfigurationException.class)
.hasMessageContainingAll(
"The datasource 'ds-1' is not configured but the persistence unit '<default>' uses it.",
"Unable to find datasource 'ds-1' for persistence unit '<default>'",
"Datasource 'ds-1' is not configured.",
"To solve this, configure datasource 'ds-1'.",
"Refer to https://quarkus.io/guides/datasource for guidance."));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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 EntitiesInDefaultPUWithImplicitUnconfiguredDatasourceTest {
Expand All @@ -18,10 +19,11 @@ public class EntitiesInDefaultPUWithImplicitUnconfiguredDatasourceTest {
// The datasource won't be truly "unconfigured" if dev services are enabled
.overrideConfigKey("quarkus.devservices.enabled", "false")
.assertException(t -> assertThat(t)
.isInstanceOf(RuntimeException.class)
.isInstanceOf(ConfigurationException.class)
.hasMessageContainingAll(
"Model classes are defined for persistence unit <default> but configured datasource <default> not found",
"To solve this, configure the default datasource.",
"Unable to find datasource '<default>' for persistence unit '<default>'",
"Datasource '<default>' is not configured.",
"To solve this, configure datasource '<default>'.",
"Refer to https://quarkus.io/guides/datasource for guidance."));

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public class EntitiesInNamedPUWithExplicitUnconfiguredDatasourceTest {
.assertException(t -> assertThat(t)
.isInstanceOf(ConfigurationException.class)
.hasMessageContainingAll(
"The datasource 'ds-1' is not configured but the persistence unit 'pu-1' uses it.",
"Unable to find datasource 'ds-1' for persistence unit 'pu-1'",
"Datasource 'ds-1' is not configured.",
"To solve this, configure datasource 'ds-1'.",
"Refer to https://quarkus.io/guides/datasource for guidance."));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import io.quarkus.agroal.runtime.DataSources;
import io.quarkus.agroal.runtime.UnconfiguredDataSource;
import io.quarkus.arc.Arc;
import io.quarkus.datasource.common.runtime.DataSourceUtil;
import io.quarkus.hibernate.orm.runtime.RuntimeSettings.Builder;
import io.quarkus.hibernate.orm.runtime.boot.FastBootEntityManagerFactoryBuilder;
import io.quarkus.hibernate.orm.runtime.boot.RuntimePersistenceUnitDescriptor;
Expand Down Expand Up @@ -389,16 +390,11 @@ private static void injectDataSource(String persistenceUnitName, String dataSour
DataSource dataSource;
try {
dataSource = Arc.container().instance(DataSources.class).get().getDataSource(dataSourceName);
} catch (IllegalArgumentException e) {
throw new IllegalStateException(
"No datasource " + dataSourceName + " has been defined for persistence unit " + persistenceUnitName);
}

if (dataSource instanceof UnconfiguredDataSource) {
throw new IllegalStateException(
"Model classes are defined for persistence unit " + persistenceUnitName
+ " but configured datasource " + dataSourceName
+ " not found: the default EntityManagerFactory will not be created. To solve this, configure the default datasource. Refer to https://quarkus.io/guides/datasource for guidance.");
if (dataSource instanceof UnconfiguredDataSource) {
throw DataSourceUtil.dataSourceNotConfigured(dataSourceName);
}
} catch (RuntimeException e) {
throw PersistenceUnitUtil.unableToFindDataSource(persistenceUnitName, dataSourceName, e);
}
runtimeSettingsBuilder.put(AvailableSettings.DATASOURCE, dataSource);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.quarkus.arc.InjectableInstance;
import io.quarkus.hibernate.orm.PersistenceUnit;
import io.quarkus.hibernate.orm.PersistenceUnitExtension;
import io.quarkus.runtime.configuration.ConfigurationException;

public class PersistenceUnitUtil {
private static final Logger LOG = Logger.getLogger(PersistenceUnitUtil.class);
Expand Down Expand Up @@ -104,4 +105,13 @@ public static <T> InjectableInstance<T> legacySingleExtensionInstanceForPersiste
private static <T> boolean isDefaultBean(InjectableInstance<T> instance) {
return instance.isResolvable() && instance.getHandle().getBean().isDefaultBean();
}

public static ConfigurationException unableToFindDataSource(String persistenceUnitName,
String dataSourceName,
Throwable cause) {
return new ConfigurationException(String.format(Locale.ROOT,
"Unable to find datasource '%s' for persistence unit '%s': %s",
dataSourceName, persistenceUnitName, cause.getMessage()),
cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ public class LiquibaseExtensionConfigEmptyDefaultDatasourceTest {
static final QuarkusUnitTest config = new QuarkusUnitTest()
// The datasource won't be truly "unconfigured" if dev services are enabled
.overrideConfigKey("quarkus.devservices.enabled", "false")
.assertException(t -> assertThat(t).rootCause()
.hasMessageContaining("No datasource has been configured"));
.assertException(t -> assertThat(t).cause().cause()
.hasMessageContainingAll("Unable to find datasource '<default>' for Liquibase",
"Datasource '<default>' is not configured.",
"To solve this, configure datasource '<default>'.",
"Refer to https://quarkus.io/guides/datasource for guidance."));

@Test
@DisplayName("If there is no config for the default datasource, the application should fail to boot")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ public class LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest {
.overrideConfigKey("quarkus.liquibase.migrate-at-start", "true")
// The datasource won't be truly "unconfigured" if dev services are enabled
.overrideConfigKey("quarkus.devservices.enabled", "false")
.assertException(t -> assertThat(t).rootCause()
.hasMessageContaining("No datasource has been configured"));
.assertException(t -> assertThat(t).cause().cause()
.hasMessageContainingAll("Unable to find datasource '<default>' for Liquibase",
"Datasource '<default>' is not configured.",
"To solve this, configure datasource '<default>'.",
"Refer to https://quarkus.io/guides/datasource for guidance."));

@Test
@DisplayName("If there is no config for the default datasource, and if migrate-at-start is enabled, the application should fail to boot")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.liquibase.runtime;

import java.util.Locale;
import java.util.function.Function;

import javax.sql.DataSource;
Expand All @@ -13,6 +14,7 @@
import io.quarkus.arc.InjectableInstance;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.arc.SyntheticCreationalContext;
import io.quarkus.datasource.common.runtime.DataSourceUtil;
import io.quarkus.liquibase.LiquibaseFactory;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
Expand All @@ -32,9 +34,16 @@ public Function<SyntheticCreationalContext<LiquibaseFactory>, LiquibaseFactory>
return new Function<SyntheticCreationalContext<LiquibaseFactory>, LiquibaseFactory>() {
@Override
public LiquibaseFactory apply(SyntheticCreationalContext<LiquibaseFactory> context) {
DataSource dataSource = context.getInjectedReference(DataSources.class).getDataSource(dataSourceName);
if (dataSource instanceof UnconfiguredDataSource) {
throw new UnsatisfiedResolutionException("No datasource has been configured");
DataSource dataSource;
try {
dataSource = context.getInjectedReference(DataSources.class).getDataSource(dataSourceName);
if (dataSource instanceof UnconfiguredDataSource) {
throw DataSourceUtil.dataSourceNotConfigured(dataSourceName);
}
} catch (RuntimeException e) {
throw new UnsatisfiedResolutionException(String.format(Locale.ROOT,
"Unable to find datasource '%s' for Liquibase: %s",
dataSourceName, e.getMessage()), e);
}

LiquibaseFactoryProducer liquibaseProducer = context.getInjectedReference(LiquibaseFactoryProducer.class);
Expand Down

0 comments on commit 989ce28

Please sign in to comment.