diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java index ad40723138484a..ec5391f5db3841 100644 --- a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java @@ -216,6 +216,8 @@ void createBeans(FlywayRecorder recorder, .addInjectionPoint(ClassType.create(DotName.createSimple(FlywayContainerProducer.class))) .addInjectionPoint(ClassType.create(DotName.createSimple(DataSource.class)), AgroalDataSourceBuildUtil.qualifier(dataSourceName)) + .startup() + .isActive(recorder.flywayContainerActiveSupplier(dataSourceName)) .createWith(recorder.flywayContainerFunction(dataSourceName, hasMigrations, createPossible)); AnnotationInstance flywayContainerQualifier; @@ -249,6 +251,11 @@ void createBeans(FlywayRecorder recorder, .setRuntimeInit() .unremovable() .addInjectionPoint(ClassType.create(DotName.createSimple(FlywayContainer.class)), flywayContainerQualifier) + // TODO uncomment this once we remove UnconfiguredDataSourceFlywayContainer + // Right now we can't, because UnconfiguredDataSourceFlywayContainer#getFlyway would throw an exception on startup, + // and unfortunately this also means we won't detect user beans being injected with Flyway for deactivated datasources... + //.startup() + .isActive(recorder.flywayActiveSupplier(dataSourceName)) .createWith(recorder.flywayFunction(dataSourceName)); if (DataSourceUtil.isDefault(dataSourceName)) { 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 index efc51c707bfd6f..e97858fb6f2cc3 100644 --- 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 @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import jakarta.enterprise.inject.CreationException; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -11,6 +10,7 @@ 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 FlywayExtensionConfigActiveFalseDefaultDatasourceTest { @@ -26,10 +26,9 @@ public class FlywayExtensionConfigActiveFalseDefaultDatasourceTest { @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.", + .isInstanceOf(InactiveBeanException.class) + .hasMessageContainingAll( + "Flyway for datasource '' was deactivated automatically because this datasource was deactivated.", "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 ''", 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 index 4049c88c3cb3b8..95edb4944974b1 100644 --- 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 @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import jakarta.enterprise.inject.CreationException; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -11,6 +10,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.arc.InactiveBeanException; import io.quarkus.flyway.FlywayDataSource; import io.quarkus.test.QuarkusUnitTest; @@ -37,10 +37,9 @@ public class FlywayExtensionConfigActiveFalseNamedDataSourceTest { @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.", + .isInstanceOf(InactiveBeanException.class) + .hasMessageContainingAll( + "Flyway for datasource 'users' was deactivated automatically because this datasource was deactivated.", "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'", diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigEmptyDefaultDatasourceTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigEmptyDefaultDatasourceTest.java index c172cdecb9a73b..8fbea9aa3eb793 100644 --- a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigEmptyDefaultDatasourceTest.java +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigEmptyDefaultDatasourceTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.CreationException; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -12,6 +11,7 @@ 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 FlywayExtensionConfigEmptyDefaultDatasourceTest { @@ -31,9 +31,9 @@ public class FlywayExtensionConfigEmptyDefaultDatasourceTest { @DisplayName("If there is no config for the default datasource, 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", + .isInstanceOf(InactiveBeanException.class) + .hasMessageContainingAll( + "Flyway for datasource '' was deactivated automatically because this datasource was not configured", "Datasource '' is not configured.", "To solve this, configure datasource ''.", "Refer to https://quarkus.io/guides/datasource for guidance."); @@ -44,7 +44,8 @@ public void testBootSucceedsButFlywayDeactivated() { public void testBootSucceedsWithInjectedBeanDependingOnFlywayButFlywayDeactivated() { assertThatThrownBy(() -> myBean.useFlyway()) .cause() - .hasMessageContainingAll("Unable to find datasource '' for Flyway", + .hasMessageContainingAll( + "Flyway for datasource '' was deactivated automatically because this datasource was not configured", "Datasource '' is not configured.", "To solve this, configure datasource ''.", "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 index 8dd1e1ff410f45..9dd2b8ca04876c 100644 --- 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 @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import jakarta.enterprise.inject.CreationException; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -11,6 +10,7 @@ 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 FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest { @@ -29,10 +29,9 @@ public class FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest @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.", + .isInstanceOf(InactiveBeanException.class) + .hasMessageContainingAll( + "Flyway for datasource '' was deactivated automatically because this datasource was deactivated", "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 ''", diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest.java index 0f9f506d16d0ae..8e351e6648b338 100644 --- a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest.java +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import jakarta.enterprise.inject.CreationException; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -11,6 +10,7 @@ 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 FlywayExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest { @@ -30,10 +30,9 @@ public class FlywayExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest { @DisplayName("If there is no config for the default datasource, 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 '' is not configured.", + .isInstanceOf(InactiveBeanException.class) + .hasMessageContainingAll( + "Flyway for datasource '' was deactivated automatically because this datasource was not configured", "To solve this, 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 index 7905ffaee31a74..aeb470c76a3ff4 100644 --- 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 @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import jakarta.enterprise.inject.CreationException; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -11,6 +10,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.arc.InactiveBeanException; import io.quarkus.flyway.FlywayDataSource; import io.quarkus.test.QuarkusUnitTest; @@ -40,10 +40,9 @@ public class FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest { @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.", + .isInstanceOf(InactiveBeanException.class) + .hasMessageContainingAll( + "Flyway for datasource 'users' was deactivated automatically because this datasource was deactivated", "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'", 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 47a87265ebf125..8381ca989d4f99 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 @@ -4,6 +4,7 @@ import java.util.Locale; import java.util.Map; import java.util.function.Function; +import java.util.function.Supplier; import javax.sql.DataSource; @@ -22,15 +23,14 @@ import io.quarkus.agroal.runtime.AgroalDataSourceUtil; import io.quarkus.agroal.runtime.UnconfiguredDataSource; +import io.quarkus.arc.ActiveResult; import io.quarkus.arc.Arc; import io.quarkus.arc.ClientProxy; -import io.quarkus.arc.InactiveBeanException; -import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; -import io.quarkus.runtime.configuration.ConfigurationException; @Recorder public class FlywayRecorder { @@ -58,27 +58,53 @@ public void setApplicationCallbackClasses(Map> call QuarkusPathLocationScanner.setApplicationCallbackClasses(callbackClasses); } + public Supplier flywayContainerActiveSupplier(String dataSourceName) { + return new Supplier() { + @Override + public ActiveResult get() { + FlywayDataSourceRuntimeConfig dataSourceConfig = config.getValue() + .getConfigForDataSourceName(dataSourceName); + + // Flyway beans are inactive when the datasource itself is inactive. + var dataSourceBean = AgroalDataSourceUtil.dataSourceInstance(dataSourceName).getHandle().getBean(); + var dataSourceActive = dataSourceBean.isActive(); + if (!dataSourceActive.result()) { + return ActiveResult.inactive( + String.format(Locale.ROOT, + "Flyway for datasource '%s' was deactivated automatically because this datasource was deactivated.", + dataSourceName), + dataSourceActive); + } + + // Note: When quarkus.flyway.active is set to false, Flyway beans are still available. + // The property only controls automatic execution on startup. + // TODO should we change quarkus.flyway.active (see ^) to align on other extensions? + // See https://github.com/quarkusio/quarkus/issues/42244. + // We'd have something like quarkus.flyway.startup.enabled controlling startup behavior, + // and *if necessary* quarkus.flyway.active controlling bean availability + // (though IMO controlling that at the datasource level would be enough). + return ActiveResult.active(); + } + }; + } + public Function, FlywayContainer> flywayContainerFunction(String dataSourceName, boolean hasMigrations, boolean createPossible) { return new Function<>() { @Override public FlywayContainer apply(SyntheticCreationalContext context) { - DataSource dataSource; - try { - // ClientProxy.unwrap is necessary to trigger exceptions on inactive datasources - dataSource = ClientProxy.unwrap(context.getInjectedReference( - DataSource.class, AgroalDataSourceUtil.qualifier(dataSourceName))); - if (dataSource instanceof UnconfiguredDataSource) { - throw DataSourceUtil.dataSourceNotConfigured(dataSourceName); - } - } catch (ConfigurationException | InactiveBeanException e) { + // ClientProxy.unwrap is necessary for the instanceof check below. + DataSource dataSource = ClientProxy.unwrap(context.getInjectedReference(DataSource.class, + AgroalDataSourceUtil.qualifier(dataSourceName))); + if (dataSource instanceof UnconfiguredDataSource) { + var cause = DataSourceUtil.dataSourceNotConfigured(dataSourceName); // TODO do we really want to enable retrieval of a FlywayContainer for an unconfigured/inactive datasource? // Marking the FlywayContainer bean as inactive when the datasource is inactive/unconfigured // 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); + dataSourceName, cause.getMessage()), cause); } FlywayContainerProducer flywayProducer = context.getInjectedReference(FlywayContainerProducer.class); @@ -87,6 +113,40 @@ public FlywayContainer apply(SyntheticCreationalContext context }; } + // This is the same as flywayContainerActiveSupplier(...), with an extra condition that the datasource be configured. + // TODO merge this with flywayContainerSupplier once we remove UnconfiguredDataSourceFlywayContainer + public Supplier flywayActiveSupplier(String dataSourceName) { + return new Supplier() { + @Override + public ActiveResult get() { + // Flyway beans are inactive when the datasource itself is inactive. + var dataSourceBean = AgroalDataSourceUtil.dataSourceInstance(dataSourceName).getHandle().getBean(); + var dataSourceActive = dataSourceBean.isActive(); + if (!dataSourceActive.result()) { + return ActiveResult.inactive( + String.format(Locale.ROOT, + "Flyway for datasource '%s' was deactivated automatically because this datasource was deactivated.", + dataSourceName), + dataSourceActive); + } + + // Flyway beans are inactive when the datasource is unconfigured. + var dataSource = ClientProxy.unwrap(AgroalDataSourceUtil.dataSourceInstance(dataSourceName).get()); + if (dataSource instanceof UnconfiguredDataSource) { + var cause = DataSourceUtil.dataSourceNotConfigured(dataSourceName); + return ActiveResult.inactive(String.format(Locale.ROOT, + "Flyway for datasource '%s' was deactivated automatically because this datasource was not configured. " + + cause.getMessage(), + dataSourceName)); + } + + // Note: When quarkus.flyway.active is set to false, Flyway beans are still available. + // The property only controls automatic execution on startup. + return ActiveResult.active(); + } + }; + } + public Function, Flyway> flywayFunction(String dataSourceName) { return new Function<>() { @Override @@ -102,20 +162,18 @@ public void doStartActions(String dataSourceName) { FlywayDataSourceRuntimeConfig flywayDataSourceRuntimeConfig = config.getValue() .getConfigForDataSourceName(dataSourceName); - if (!flywayDataSourceRuntimeConfig.active - // If not specified explicitly, Flyway is active when the datasource itself is active. - .orElseGet(() -> AgroalDataSourceUtil.dataSourceIfActive(dataSourceName).isPresent())) { + if (flywayDataSourceRuntimeConfig.active.isPresent() + && !flywayDataSourceRuntimeConfig.active.get()) { return; } - InstanceHandle flywayContainerInstanceHandle = Arc.container().instance(FlywayContainer.class, + InjectableInstance flywayContainerInstance = Arc.container().select(FlywayContainer.class, FlywayContainerUtil.getFlywayContainerQualifier(dataSourceName)); - - if (!flywayContainerInstanceHandle.isAvailable()) { + if (!flywayContainerInstance.isResolvable() + || !flywayContainerInstance.getHandle().getBean().isActive().result()) { return; } - - FlywayContainer flywayContainer = flywayContainerInstanceHandle.get(); + FlywayContainer flywayContainer = flywayContainerInstance.get(); if (flywayContainer instanceof UnconfiguredDataSourceFlywayContainer) { return; 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 c7967d1ca4e4d1..5268357f7672be 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 @@ -268,6 +268,8 @@ void createBeans(LiquibaseRecorder recorder, .addInjectionPoint(ClassType.create(DotName.createSimple(LiquibaseFactoryProducer.class))) .addInjectionPoint(ClassType.create(DotName.createSimple(DataSource.class)), AgroalDataSourceBuildUtil.qualifier(dataSourceName)) + .startup() + .isActive(recorder.liquibaseActiveSupplier(dataSourceName)) .createWith(recorder.liquibaseFunction(dataSourceName)); if (DataSourceUtil.isDefault(dataSourceName)) { 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 index 17c3f8706d8f6b..da0fe6c414f71c 100644 --- 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 @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import jakarta.enterprise.inject.CreationException; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -10,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.arc.InactiveBeanException; import io.quarkus.liquibase.LiquibaseFactory; import io.quarkus.test.QuarkusUnitTest; @@ -26,9 +26,9 @@ public class LiquibaseExtensionConfigActiveFalseDefaultDatasourceTest { @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", + .isInstanceOf(InactiveBeanException.class) + .hasMessageContainingAll( + "Liquibase for datasource '' was deactivated automatically because this datasource was deactivated", "Datasource '' 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'" 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 index bd7b6af7a55c9e..71271000195f53 100644 --- 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 @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import jakarta.enterprise.inject.CreationException; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -10,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.arc.InactiveBeanException; import io.quarkus.liquibase.LiquibaseDataSource; import io.quarkus.liquibase.LiquibaseFactory; import io.quarkus.test.QuarkusUnitTest; @@ -36,9 +36,9 @@ public class LiquibaseExtensionConfigActiveFalseNamedDatasourceTest { @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", + .isInstanceOf(InactiveBeanException.class) + .hasMessageContainingAll( + "Liquibase for datasource 'users' was deactivated automatically because this datasource was deactivated", "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'" diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigEmptyDefaultDatasourceTest.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigEmptyDefaultDatasourceTest.java index ac28eb20acf10f..2d6b46e6fa9faa 100644 --- a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigEmptyDefaultDatasourceTest.java +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigEmptyDefaultDatasourceTest.java @@ -3,10 +3,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +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 LiquibaseExtensionConfigEmptyDefaultDatasourceTest { @@ -15,14 +18,19 @@ 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).cause().cause() - .hasMessageContainingAll("Unable to find datasource '' for Liquibase", + .assertException(t -> assertThat(t) + .hasMessageContainingAll( + "Liquibase for datasource '' was deactivated automatically because this datasource was not configured", "Datasource '' is not configured.", "To solve this, configure datasource ''.", "Refer to https://quarkus.io/guides/datasource for guidance.")); + @Inject + LiquibaseFactory liquibaseFactory; + @Test - @DisplayName("If there is no config for the default datasource, the application should fail to boot") + @DisplayName("If there is no config for the default datasource, " + + "and LiquibaseFactory for the default datasource is injected, the application should fail to boot") public void testBootFails() { // Should not be reached because boot should fail. assertTrue(false); 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 index 1b5b05d35587e4..a6b88c6ec5a35c 100644 --- 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 @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import jakarta.enterprise.inject.CreationException; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -10,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.arc.InactiveBeanException; import io.quarkus.liquibase.LiquibaseFactory; import io.quarkus.test.QuarkusUnitTest; @@ -29,9 +29,9 @@ public class LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseT @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", + .isInstanceOf(InactiveBeanException.class) + .hasMessageContainingAll( + "Liquibase for datasource '' was deactivated automatically because this datasource was deactivated", "Datasource '' 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'" diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest.java index bad255b85ccf30..6e9a4cec38b27d 100644 --- a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest.java +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest.java @@ -1,12 +1,16 @@ package io.quarkus.liquibase.test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +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.arc.InactiveBeanException; +import io.quarkus.liquibase.LiquibaseFactory; import io.quarkus.test.QuarkusUnitTest; public class LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest { @@ -17,18 +21,20 @@ public class LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigEmptyTest { .addAsResource("db/changeLog.xml", "db/changeLog.xml")) .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).cause().cause() - .hasMessageContainingAll("Unable to find datasource '' for Liquibase", - "Datasource '' is not configured.", - "To solve this, configure datasource ''.", - "Refer to https://quarkus.io/guides/datasource for guidance.")); + .overrideConfigKey("quarkus.devservices.enabled", "false"); + + @Inject + Instance liquibaseForDefaultDatasource; @Test - @DisplayName("If there is no config for the default datasource, and if migrate-at-start is enabled, the application should fail to boot") - public void testBootFails() { - // Should not be reached because boot should fail. - assertTrue(false); + @DisplayName("If the there is no config for the default datasource, 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(InactiveBeanException.class) + .hasMessageContainingAll( + "Liquibase for datasource '' was deactivated automatically because this datasource was not configured", + "To solve this, 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 index a1a0925826e713..e2305c6218d9c9 100644 --- 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 @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import jakarta.enterprise.inject.CreationException; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -10,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.arc.InactiveBeanException; import io.quarkus.liquibase.LiquibaseDataSource; import io.quarkus.liquibase.LiquibaseFactory; import io.quarkus.test.QuarkusUnitTest; @@ -40,9 +40,9 @@ public class LiquibaseExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTes @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", + .isInstanceOf(InactiveBeanException.class) + .hasMessageContainingAll( + "Liquibase for datasource 'users' was deactivated automatically because this datasource was deactivated", "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'" 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 58174592136bb1..5e1c388570fb05 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 @@ -2,15 +2,17 @@ import java.util.Locale; import java.util.function.Function; +import java.util.function.Supplier; import javax.sql.DataSource; -import jakarta.enterprise.inject.UnsatisfiedResolutionException; - import io.quarkus.agroal.runtime.AgroalDataSourceUtil; import io.quarkus.agroal.runtime.UnconfiguredDataSource; +import io.quarkus.arc.ActiveResult; +import io.quarkus.arc.Arc; import io.quarkus.arc.ClientProxy; -import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.InactiveBeanException; +import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.liquibase.LiquibaseFactory; @@ -29,24 +31,49 @@ public LiquibaseRecorder(RuntimeValue config) { this.config = config; } + public Supplier liquibaseActiveSupplier(String dataSourceName) { + return new Supplier() { + @Override + public ActiveResult get() { + // Flyway beans are inactive when the datasource itself is inactive. + var dataSourceBean = AgroalDataSourceUtil.dataSourceInstance(dataSourceName).getHandle().getBean(); + var dataSourceActive = dataSourceBean.isActive(); + if (!dataSourceActive.result()) { + return ActiveResult.inactive( + String.format(Locale.ROOT, + "Liquibase for datasource '%s' was deactivated automatically because this datasource was deactivated.", + dataSourceName), + dataSourceActive); + } + + // Liquibase beans are inactive when the datasource is unconfigured. + var dataSource = ClientProxy.unwrap(AgroalDataSourceUtil.dataSourceInstance(dataSourceName).get()); + if (dataSource instanceof UnconfiguredDataSource) { + var cause = DataSourceUtil.dataSourceNotConfigured(dataSourceName); + return ActiveResult.inactive(String.format(Locale.ROOT, + "Liquibase for datasource '%s' was deactivated automatically because this datasource was not configured. " + + cause.getMessage(), + dataSourceName)); + } + + // Note: When quarkus.liquibase.enabled is set to false, Liquibase beans are still available. + // The property only controls automatic execution on startup. + // TODO should we change quarkus.liquibase.enabled (see ^) to align on other extensions? + // See https://github.com/quarkusio/quarkus/issues/42244. + // We'd have something like quarkus.liquibase.startup.enabled controlling startup behavior, + // and *if necessary* quarkus.liquibase.active controlling bean availability + // (though IMO controlling that at the datasource level would be enough). + return ActiveResult.active(); + } + }; + } + public Function, LiquibaseFactory> liquibaseFunction(String dataSourceName) { return new Function, LiquibaseFactory>() { @Override public LiquibaseFactory apply(SyntheticCreationalContext context) { - DataSource dataSource; - try { - // ClientProxy.unwrap is necessary to trigger exceptions on inactive datasources - dataSource = ClientProxy.unwrap(context.getInjectedReference(DataSource.class, - AgroalDataSourceUtil.qualifier(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); - } - + DataSource dataSource = context.getInjectedReference(DataSource.class, + AgroalDataSourceUtil.qualifier(dataSourceName)); LiquibaseFactoryProducer liquibaseProducer = context.getInjectedReference(LiquibaseFactoryProducer.class); return liquibaseProducer.createLiquibaseFactory(dataSource, dataSourceName); } @@ -57,40 +84,41 @@ public void doStartActions(String dataSourceName) { if (!config.getValue().enabled) { return; } - // Liquibase is only active when the datasource itself is active. - if (AgroalDataSourceUtil.dataSourceIfActive(dataSourceName).isEmpty()) { + + var dataSourceConfig = config.getValue().getConfigForDataSourceName(dataSourceName); + if (!dataSourceConfig.cleanAtStart && !dataSourceConfig.migrateAtStart) { return; } - InstanceHandle liquibaseFactoryHandle = LiquibaseFactoryUtil.getLiquibaseFactory(dataSourceName); - try { - LiquibaseFactory liquibaseFactory = liquibaseFactoryHandle.get(); - var config = liquibaseFactory.getConfiguration(); - if (!config.cleanAtStart && !config.migrateAtStart) { - return; + InjectableInstance liquibaseFactoryInstance = Arc.container().select(LiquibaseFactory.class, + LiquibaseFactoryUtil.getLiquibaseFactoryQualifier(dataSourceName)); + if (!liquibaseFactoryInstance.isResolvable() + || !liquibaseFactoryInstance.getHandle().getBean().isActive().result()) { + return; + } + LiquibaseFactory liquibaseFactory = liquibaseFactoryInstance.get(); + try (Liquibase liquibase = liquibaseFactory.createLiquibase(); + ResettableSystemProperties resettableSystemProperties = liquibaseFactory + .createResettableSystemProperties()) { + if (dataSourceConfig.cleanAtStart) { + liquibase.dropAll(); } - try (Liquibase liquibase = liquibaseFactory.createLiquibase(); - ResettableSystemProperties resettableSystemProperties = liquibaseFactory - .createResettableSystemProperties()) { - 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(); + if (dataSourceConfig.migrateAtStart) { + var lockService = LockServiceFactory.getInstance() + .getLockService(liquibase.getDatabase()); + lockService.waitForLock(); + try { + if (dataSourceConfig.validateOnMigrate) { + liquibase.validate(); } + liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels()); + } finally { + lockService.releaseLock(); } } - } catch (UnsatisfiedResolutionException e) { - //ignore, the DS is not configured + } catch (InactiveBeanException e) { + // These exceptions should be self-explanatory + throw e; } catch (Exception e) { throw new IllegalStateException("Error starting Liquibase", e); }